diff --git a/.github/workflows/action-buildpackage-verify-metadata.yml b/.github/workflows/action-buildpackage-verify-metadata.yml new file mode 100644 index 0000000..f7f0d54 --- /dev/null +++ b/.github/workflows/action-buildpackage-verify-metadata.yml @@ -0,0 +1,73 @@ +name: Action buildpackage-verify-metadata +"on": + pull_request: + paths: + - buildpackage/verify-metadata/** + push: + branches: + - main + paths: + - buildpackage/verify-metadata/** + release: + types: + - published +jobs: + create-action: + name: Create Action + runs-on: + - ubuntu-latest + steps: + - if: ${{ github.event_name != 'pull_request' || ! github.event.pull_request.head.repo.fork }} + name: Docker login ghcr.io + uses: docker/login-action@v1 + with: + password: ${{ secrets.IMPLEMENTATION_GITHUB_TOKEN }} + registry: ghcr.io + username: ${{ secrets.IMPLEMENTATION_GITHUB_USERNAME }} + - uses: actions/checkout@v2 + - id: version + name: Compute Version + run: | + #!/usr/bin/env bash + + set -euo pipefail + + if [[ ${GITHUB_REF} =~ refs/tags/v([0-9]+\.[0-9]+\.[0-9]+) ]]; then + VERSION=${BASH_REMATCH[1]} + elif [[ ${GITHUB_REF} =~ refs/heads/(.+) ]]; then + VERSION=${BASH_REMATCH[1]} + else + VERSION=$(git rev-parse --short HEAD) + fi + + echo "::set-output name=version::${VERSION}" + echo "Selected ${VERSION} from + * ref: ${GITHUB_REF} + * sha: ${GITHUB_SHA} + " + - name: Create Action + run: | + #!/usr/bin/env bash + + set -euo pipefail + + echo "::group::Building ${TARGET}:${VERSION}" + docker build \ + --file Dockerfile \ + --build-arg "SOURCE=${SOURCE}" \ + --tag "${TARGET}:${VERSION}" \ + . + echo "::endgroup::" + + if [[ "${PUSH}" == "true" ]]; then + echo "::group::Pushing ${TARGET}:${VERSION}" + docker push "${TARGET}:${VERSION}" + echo "::endgroup::" + else + echo "Skipping push" + fi + env: + PUSH: ${{ github.event_name != 'pull_request' }} + SOURCE: buildpackage/verify-metadata/cmd + TARGET: ghcr.io/buildpacks/actions/buildpackage/verify-metadata + VERSION: ${{ steps.version.outputs.version }} diff --git a/.github/workflows/create-verify-buildpackage-action.yml b/.github/workflows/create-verify-buildpackage-action.yml deleted file mode 100644 index 3d28e70..0000000 --- a/.github/workflows/create-verify-buildpackage-action.yml +++ /dev/null @@ -1,73 +0,0 @@ -name: Create Action verify-buildpackage -"on": - pull_request: - paths: - - verify/** - push: - branches: - - main - paths: - - verify/** - release: - types: - - published -jobs: - create-action: - name: Create Action - runs-on: - - ubuntu-latest - steps: - - if: ${{ github.event_name != 'pull_request' || ! github.event.pull_request.head.repo.fork }} - name: Docker login ghcr.io - uses: docker/login-action@v1 - with: - password: ${{ secrets.IMPLEMENTATION_GITHUB_TOKEN }} - registry: ghcr.io - username: ${{ secrets.IMPLEMENTATION_GITHUB_USERNAME }} - - uses: actions/checkout@v2 - - id: version - name: Compute Version - run: | - #!/usr/bin/env bash - - set -euo pipefail - - if [[ ${GITHUB_REF} =~ refs/tags/v([0-9]+\.[0-9]+\.[0-9]+) ]]; then - VERSION=${BASH_REMATCH[1]} - elif [[ ${GITHUB_REF} =~ refs/heads/(.+) ]]; then - VERSION=${BASH_REMATCH[1]} - else - VERSION=$(git rev-parse --short HEAD) - fi - - echo "::set-output name=version::${VERSION}" - echo "Selected ${VERSION} from - * ref: ${GITHUB_REF} - * sha: ${GITHUB_SHA} - " - - name: Create Action - run: | - #!/usr/bin/env bash - - set -euo pipefail - - echo "::group::Building ${TARGET}:${VERSION}" - docker build \ - --file Dockerfile \ - --build-arg "SOURCE=${SOURCE}" \ - --tag "${TARGET}:${VERSION}" \ - . - echo "::endgroup::" - - if [[ "${PUSH}" == "true" ]]; then - echo "::group::Pushing ${TARGET}:${VERSION}" - docker push "${TARGET}:${VERSION}" - echo "::endgroup::" - else - echo "Skipping push" - fi - env: - PUSH: ${{ github.event_name != 'pull_request' }} - SOURCE: verify/cmd - TARGET: ghcr.io/buildpacks/actions/verify-buildpackage - VERSION: ${{ steps.version.outputs.version }} diff --git a/README.md b/README.md index 51915cf..c9aaf75 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,8 @@ - [Compute Mmetadata Action](#compute-mmetadata-action) - [Inputs](#inputs) - [Outputs](#outputs) - - [Registry Action](#registry-action) - - [Add](#add) + - [Buildpackage](#buildpackage) + - [Verify Metadata Action](#verify-metadata-action) - [Inputs](#inputs-1) - [Yank](#yank) - [Inputs](#inputs-2) @@ -41,8 +41,30 @@ uses: docker://ghcr.io/buildpacks/actions/buildpack/compute-metadata | `version` | The contents of `buildpack.version` | `homepage` | The contents of `buildpack.homepage` -## Registry Action -The registry action adds and yanks buildpack releases in the [Buildpack Registry Index][bri]. +## Buildpackage + +### Verify Metadata Action +The `buildpackage/verify-metadata` action parses the metadata on a buildpackage and verifies that the `id` and `version` match expected values. + +```yaml +uses: docker://ghcr.io/buildpacks/actions/buildpackage/verify-metadata +with: + id: test-buildpack + version: "1.0.0" + address: ghcr.io/example/test-buildpack@sha256:04ba2d17480910bd340f0305d846b007148dafd64bc6fc2626870c174b7c7de7 +``` + +#### Inputs +| Parameter | Description +| :-------- | :---------- +| `id` | The expected `id` for the buildpackage +| `version` | The expected `version` for the buildpackage +| `address` | The digest-style address of the buildpackage to verify + +## Registry + +### Add Entry Action +The `registry/add-entry` action adds an entry to the [Buildpack Registry Index][bri]. [bri]: https://github.com/buildpacks/registry-index @@ -102,24 +124,6 @@ uses: buildpacks/github-actions/setup-pack | `pack-version` | Optional version of [`pack`][pack] to install. Defaults to latest release. | `yj-version` | Optional version of [`yj`][yj] to install. Defaults to latest release. -## Verify Buildpackage Action -The verify-buildpackage action parses the metadata on a buildpackage and verifies that the `id` and `version` match expected values. - -```yaml -uses: docker://ghcr.io/buildpacks/actions/verify-buildpackage -with: - id: test-buildpack - version: "1.0.0" - address: ghcr.io/example/test-buildpack@sha256:04ba2d17480910bd340f0305d846b007148dafd64bc6fc2626870c174b7c7de7 -``` - -#### Inputs -| Parameter | Description -| :-------- | :---------- -| `id` | The expected `id` for the buildpackage -| `version` | The expected `version` for the buildpackage -| `address` | The digest-style address of the buildpackage to verify - ## License This library is released under version 2.0 of the [Apache License][a]. diff --git a/verify/cmd/main.go b/buildpackage/verify-metadata/cmd/main.go similarity index 60% rename from verify/cmd/main.go rename to buildpackage/verify-metadata/cmd/main.go index 0ff02b5..6297336 100644 --- a/verify/cmd/main.go +++ b/buildpackage/verify-metadata/cmd/main.go @@ -22,32 +22,13 @@ import ( "github.com/google/go-containerregistry/pkg/v1/remote" - "github.com/buildpacks/github-actions/verify" + "github.com/buildpacks/github-actions/buildpackage/verify-metadata" + "github.com/buildpacks/github-actions/toolkit" ) func main() { - var ( - v verify.Verifier - - err error - ok bool - ) - - v.Image = remote.Image - - if v.ID, ok = os.LookupEnv("INPUT_ID"); !ok { - panic(fmt.Errorf("id must be specified")) - } - - if v.Version, ok = os.LookupEnv("INPUT_VERSION"); !ok { - panic(fmt.Errorf("version must be specified")) - } - - if v.Address, ok = os.LookupEnv("INPUT_ADDRESS"); !ok { - panic(fmt.Errorf("address must be specified")) - } - - if err = v.Verify(); err != nil { - panic(err) + if err := metadata.VerifyMetadata(&toolkit.DefaultToolkit{}, remote.Image); err != nil { + fmt.Println(err) + os.Exit(1) } } diff --git a/buildpackage/verify-metadata/mocks/image_function.go b/buildpackage/verify-metadata/mocks/image_function.go new file mode 100644 index 0000000..e13c86f --- /dev/null +++ b/buildpackage/verify-metadata/mocks/image_function.go @@ -0,0 +1,46 @@ +// Code generated by mockery v2.4.0-beta. DO NOT EDIT. + +package mocks + +import ( + name "github.com/google/go-containerregistry/pkg/name" + remote "github.com/google/go-containerregistry/pkg/v1/remote" + mock "github.com/stretchr/testify/mock" + + v1 "github.com/google/go-containerregistry/pkg/v1" +) + +// ImageFunction is an autogenerated mock type for the ImageFunction type +type ImageFunction struct { + mock.Mock +} + +// Execute provides a mock function with given fields: _a0, _a1 +func (_m *ImageFunction) Execute(_a0 name.Reference, _a1 ...remote.Option) (v1.Image, error) { + _va := make([]interface{}, len(_a1)) + for _i := range _a1 { + _va[_i] = _a1[_i] + } + var _ca []interface{} + _ca = append(_ca, _a0) + _ca = append(_ca, _va...) + ret := _m.Called(_ca...) + + var r0 v1.Image + if rf, ok := ret.Get(0).(func(name.Reference, ...remote.Option) v1.Image); ok { + r0 = rf(_a0, _a1...) + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(v1.Image) + } + } + + var r1 error + if rf, ok := ret.Get(1).(func(name.Reference, ...remote.Option) error); ok { + r1 = rf(_a0, _a1...) + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} diff --git a/verify/verify.go b/buildpackage/verify-metadata/verify_metadata.go similarity index 50% rename from verify/verify.go rename to buildpackage/verify-metadata/verify_metadata.go index 08ecc74..2c94a0e 100644 --- a/verify/verify.go +++ b/buildpackage/verify-metadata/verify_metadata.go @@ -14,7 +14,7 @@ * limitations under the License. */ -package verify +package metadata import ( "encoding/json" @@ -24,54 +24,67 @@ import ( "github.com/google/go-containerregistry/pkg/name" v1 "github.com/google/go-containerregistry/pkg/v1" "github.com/google/go-containerregistry/pkg/v1/remote" + + "github.com/buildpacks/github-actions/toolkit" ) const MetadataLabel = "io.buildpacks.buildpackage.metadata" -type Verifier struct { - Image func(name.Reference, ...remote.Option) (v1.Image, error) +//go:generate mockery --name ImageFunction --case=underscore - ID string - Version string - Address string -} +type ImageFunction func(name.Reference, ...remote.Option) (v1.Image, error) -func (v Verifier) Verify() error { - ref, err := name.ParseReference(v.Address) +func VerifyMetadata(tk toolkit.Toolkit, imageFn ImageFunction) error { + id, ok := tk.GetInput("id") + if !ok { + return toolkit.FailedError("id must be specified") + } + + version, ok := tk.GetInput("version") + if !ok { + return toolkit.FailedError("version must be specified") + } + + address, ok := tk.GetInput("address") + if !ok { + return toolkit.FailedError("address must be specified") + } + + ref, err := name.ParseReference(address) if err != nil { - return fmt.Errorf("unable to parse address %s as image reference\n%w", v.Address, err) + return toolkit.FailedErrorf("unable to parse address %s as image reference", address) } if _, ok := ref.(name.Digest); !ok { - return fmt.Errorf("address %s must be in digest form /@sh256:", v.Address) + return toolkit.FailedErrorf("address %s must be in digest form /@sh256:", address) } - image, err := v.Image(ref) + image, err := imageFn(ref) if err != nil { - return fmt.Errorf("unable to retrieve image %s\n%w", v.Address, err) + return toolkit.FailedErrorf("unable to retrieve image %s", address) } configFile, err := image.ConfigFile() if err != nil { - return fmt.Errorf("unable to retrieve config file\n%w", err) + return toolkit.FailedErrorf("unable to retrieve config file\n%w", err) } raw, ok := configFile.Config.Labels[MetadataLabel] if !ok { - return fmt.Errorf("unable to retrieve %s label", MetadataLabel) + return toolkit.FailedErrorf("unable to retrieve %s label", MetadataLabel) } var m metadata if err := json.Unmarshal([]byte(raw), &m); err != nil { - return fmt.Errorf("unable to unmarshal %s label", MetadataLabel) + return toolkit.FailedErrorf("unable to unmarshal %s label", MetadataLabel) } - if v.ID != m.ID { - return fmt.Errorf("invalid id in buildpackage: expected '%s', found '%s'", v.ID, m.ID) + if id != m.ID { + return toolkit.FailedErrorf("invalid id in buildpackage: expected '%s', found '%s'", id, m.ID) } - if v.Version != m.Version { - return fmt.Errorf("invalid version in buildpackage: expected '%s', found '%s'", v.Version, m.Version) + if version != m.Version { + return toolkit.FailedErrorf("invalid version in buildpackage: expected '%s', found '%s'", version, m.Version) } @@ -85,7 +98,7 @@ func (v Verifier) Verify() error { Version: %s Homepage: %s Stacks: %s -`, v.Address, m.ID, m.Version, m.Homepage, strings.Join(stacks, ", ")) +`, address, m.ID, m.Version, m.Homepage, strings.Join(stacks, ", ")) return nil } diff --git a/buildpackage/verify-metadata/verify_metadata_test.go b/buildpackage/verify-metadata/verify_metadata_test.go new file mode 100644 index 0000000..36a95d0 --- /dev/null +++ b/buildpackage/verify-metadata/verify_metadata_test.go @@ -0,0 +1,109 @@ +/* + * Copyright 2018-2020 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package metadata_test + +import ( + "testing" + + v1 "github.com/google/go-containerregistry/pkg/v1" + "github.com/google/go-containerregistry/pkg/v1/fake" + . "github.com/onsi/gomega" + "github.com/sclevine/spec" + "github.com/sclevine/spec/report" + "github.com/stretchr/testify/mock" + + metadata "github.com/buildpacks/github-actions/buildpackage/verify-metadata" + mocks2 "github.com/buildpacks/github-actions/buildpackage/verify-metadata/mocks" + "github.com/buildpacks/github-actions/toolkit/mocks" +) + +func TestVerifyMetadata(t *testing.T) { + spec.Run(t, "verify-metadata", func(t *testing.T, context spec.G, it spec.S) { + var ( + Expect = NewWithT(t).Expect + + f = &mocks2.ImageFunction{} + i = &fake.FakeImage{} + + tk = &mocks.Toolkit{} + ) + + it.Before(func() { + tk.On("GetInput", "id").Return("test-id", true) + tk.On("GetInput", "version").Return("test-version", true) + }) + + it("fails if address is not a digest image reference", func() { + tk.On("GetInput", "address").Return("test-host/test-repository:test-version", true) + + Expect(metadata.VerifyMetadata(tk, f.Execute)). + To(MatchError("::error ::address test-host/test-repository:test-version must be in digest form /@sh256:")) + }) + + context("valid address", func() { + it.Before(func() { + tk.On("GetInput", "address").Return("host/repository@sha256:04ba2d17480910bd340f0305d846b007148dafd64bc6fc2626870c174b7c7de7", true) + f.On("Execute", mock.Anything).Return(i, nil) + }) + + it("fails if io.buildpacks.buildpackage.metadata is not on image", func() { + i.ConfigFileReturns(&v1.ConfigFile{ + Config: v1.Config{ + Labels: map[string]string{}, + }, + }, nil) + + Expect(metadata.VerifyMetadata(tk, f.Execute)). + To(MatchError("::error ::unable to retrieve io.buildpacks.buildpackage.metadata label")) + }) + + it("fails if id does not match", func() { + i.ConfigFileReturns(&v1.ConfigFile{ + Config: v1.Config{ + Labels: map[string]string{metadata.MetadataLabel: `{ "id": "another-id", "version": "test-version" }`}, + }, + }, nil) + + Expect(metadata.VerifyMetadata(tk, f.Execute)). + To(MatchError("::error ::invalid id in buildpackage: expected 'test-id', found 'another-id'")) + }) + + it("fails if version does not match", func() { + i.ConfigFileReturns(&v1.ConfigFile{ + Config: v1.Config{ + Labels: map[string]string{metadata.MetadataLabel: `{ "id": "test-id", "version": "another-version" }`}, + }, + }, nil) + + Expect(metadata.VerifyMetadata(tk, f.Execute)). + To(MatchError("::error ::invalid version in buildpackage: expected 'test-version', found 'another-version'")) + }) + + it("passes if version and id match", func() { + i.ConfigFileReturns(&v1.ConfigFile{ + Config: v1.Config{ + Labels: map[string]string{metadata.MetadataLabel: `{ "id": "test-id", "version": "test-version" }`}, + }, + }, nil) + + Expect(metadata.VerifyMetadata(tk, f.Execute)).To(Succeed()) + }) + + }) + + }, spec.Report(report.Terminal{})) +} diff --git a/verify/verify_test.go b/verify/verify_test.go deleted file mode 100644 index c119056..0000000 --- a/verify/verify_test.go +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2018-2020 the original author or authors. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * https://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package verify_test - -import ( - "testing" - - "github.com/google/go-containerregistry/pkg/name" - v1 "github.com/google/go-containerregistry/pkg/v1" - "github.com/google/go-containerregistry/pkg/v1/fake" - "github.com/google/go-containerregistry/pkg/v1/remote" - . "github.com/onsi/gomega" - "github.com/sclevine/spec" - "github.com/sclevine/spec/report" - - "github.com/buildpacks/github-actions/verify" -) - -func TestVerify(t *testing.T) { - spec.Run(t, "verify", func(t *testing.T, when spec.G, it spec.S) { - var ( - Expect = NewWithT(t).Expect - - r = &TestRemote{Labels: make(map[string]string)} - - v = verify.Verifier{ - Image: r.Image, - - ID: "test-id", - Version: "test-version", - Address: "host/repository@sha256:04ba2d17480910bd340f0305d846b007148dafd64bc6fc2626870c174b7c7de7", - } - ) - - it("fails if address is not a digest image reference", func() { - v.Address = "test-host/test-repository:test-version" - - Expect(v.Verify()).To(MatchError("address test-host/test-repository:test-version must be in digest form /@sh256:")) - }) - - it("fails if io.buildpacks.buildpackage.metadata is not on image", func() { - Expect(v.Verify()).To(MatchError("unable to retrieve io.buildpacks.buildpackage.metadata label")) - }) - - it("fails if id does not match", func() { - r.Labels[verify.MetadataLabel] = `{ "id": "another-id", "version": "test-version" }` - - Expect(v.Verify()).To(MatchError("invalid id in buildpackage: expected 'test-id', found 'another-id'")) - }) - - it("fails if version does not match", func() { - r.Labels[verify.MetadataLabel] = `{ "id": "test-id", "version": "another-version" }` - - Expect(v.Verify()).To(MatchError("invalid version in buildpackage: expected 'test-version', found 'another-version'")) - }) - - it("passes if version and id match", func() { - r.Labels[verify.MetadataLabel] = `{ "id": "test-id", "version": "test-version" }` - - Expect(v.Verify()).To(Succeed()) - }) - - }, spec.Report(report.Terminal{})) -} - -type TestRemote struct { - Labels map[string]string -} - -func (t *TestRemote) Image(_ name.Reference, _ ...remote.Option) (v1.Image, error) { - i := &fake.FakeImage{} - i.ConfigFileReturns(&v1.ConfigFile{ - Config: v1.Config{ - Labels: t.Labels, - }, - }, nil) - - return i, nil -}