├── .gitignore ├── e2e ├── run.sh ├── testdata │ ├── hello-world │ │ ├── invocation-image │ │ │ ├── Dockerfile │ │ │ └── run │ │ └── bundle.json.template │ └── bundle.json.golden.template ├── helper_test.go └── e2e_test.go ├── relocation ├── doc.go └── types.go ├── remotes ├── doc.go ├── log.go ├── typeless.go ├── mount_test.go ├── fixupevent.go ├── mocks_test.go ├── fixuphelpers.go ├── resolver.go ├── fixupoptions.go ├── push_test.go ├── pull.go ├── pull_test.go ├── mount.go ├── push.go └── fixup.go ├── converter ├── doc.go ├── types_test.go ├── types.go ├── convert_test.go └── convert.go ├── CODEOWNERS ├── internal ├── image_client.go └── version.go ├── cmd └── cnab-to-oci │ ├── version.go │ ├── main.go │ ├── pull.go │ ├── fixup.go │ └── push.go ├── .golangci.yml ├── doc.go ├── .github ├── workflows │ └── build.yaml └── dependabot.yaml ├── examples └── helloworld-cnab │ ├── bundle.json │ └── complete.bundle.json ├── Dockerfile ├── Makefile ├── CONTRIBUTING.md ├── go.mod ├── tests └── helpers.go ├── LICENSE ├── README.md └── go.sum /.gitignore: -------------------------------------------------------------------------------- 1 | bin/* 2 | .idea 3 | vendor/ 4 | -------------------------------------------------------------------------------- /e2e/run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | set -eu -o pipefail 3 | 4 | # Run the e2e tests 5 | cd ./e2e 6 | ./e2e.test 7 | -------------------------------------------------------------------------------- /relocation/doc.go: -------------------------------------------------------------------------------- 1 | // Package relocation provides types to handle relocation maps. 2 | package relocation // import "github.com/cnabio/cnab-to-oci/relocation" 3 | -------------------------------------------------------------------------------- /remotes/doc.go: -------------------------------------------------------------------------------- 1 | // Package remotes provides converters to transform CNAB to OCI format and vice versa. 2 | package remotes // import "github.com/cnabio/cnab-to-oci/remotes" 3 | -------------------------------------------------------------------------------- /converter/doc.go: -------------------------------------------------------------------------------- 1 | // Package cnabtooci provides converters to transform CNAB to OCI format and vice versa. 2 | package converter // import "github.com/cnabio/cnab-to-oci/converter" 3 | -------------------------------------------------------------------------------- /e2e/testdata/hello-world/invocation-image/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM busybox:1.31.0-uclibc 2 | 3 | COPY run /cnab/app/run 4 | RUN chmod +x /cnab/app/run 5 | 6 | ENTRYPOINT [ "/cnab/app/run" ] -------------------------------------------------------------------------------- /relocation/types.go: -------------------------------------------------------------------------------- 1 | package relocation 2 | 3 | // ImageRelocationMap stores the mapping between the original image reference as key, and the relocated reference as a value. 4 | type ImageRelocationMap map[string]string 5 | -------------------------------------------------------------------------------- /CODEOWNERS: -------------------------------------------------------------------------------- 1 | # This file is described here: https://help.github.com/en/articles/about-code-owners 2 | 3 | # Global Owners: These members are Core Maintainers of cnab-to-oci 4 | CODEOWNERS @silvin-lubecki @radu-matei @carolynvs 5 | LICENSE @silvin-lubecki @radu-matei @carolynvs 6 | CONTRIBUTING.md @silvin-lubecki @radu-matei @carolynvs 7 | GOVERNANCE.md @silvin-lubecki @radu-matei @carolynvs 8 | -------------------------------------------------------------------------------- /e2e/testdata/hello-world/invocation-image/run: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | action=$CNAB_ACTION 4 | name=$CNAB_INSTALLATION_NAME 5 | 6 | case $action in 7 | install) 8 | echo "Install action" 9 | ;; 10 | uninstall) 11 | echo "uninstall action" 12 | ;; 13 | upgrade) 14 | echo "Upgrade action" 15 | ;; 16 | *) 17 | echo "No action for $action" 18 | ;; 19 | esac 20 | echo "Action $action complete for $name" 21 | -------------------------------------------------------------------------------- /internal/image_client.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "context" 5 | "io" 6 | 7 | "github.com/docker/docker/api/types/image" 8 | ) 9 | 10 | // ImageClient is a subset of Docker's ImageAPIClient interface with only what we are using for cnab-to-oci. 11 | type ImageClient interface { 12 | ImagePush(ctx context.Context, ref string, options image.PushOptions) (io.ReadCloser, error) 13 | ImageTag(ctx context.Context, image, ref string) error 14 | } 15 | -------------------------------------------------------------------------------- /cmd/cnab-to-oci/version.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/cnabio/cnab-to-oci/internal" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func versionCmd() *cobra.Command { 11 | cmd := &cobra.Command{ 12 | Use: "version", 13 | Short: "Shows the version of cnab-to-oci", 14 | RunE: func(_ *cobra.Command, _ []string) error { 15 | fmt.Println(internal.FullVersion()) 16 | return nil 17 | }, 18 | } 19 | return cmd 20 | } 21 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | linters: 2 | enable-all: false 3 | disable-all: true 4 | enable: 5 | - errcheck 6 | - gocyclo 7 | - gofmt 8 | - goimports 9 | - gosimple 10 | - govet 11 | - ineffassign 12 | - lll 13 | - misspell 14 | - nakedret 15 | - revive 16 | - staticcheck 17 | - typecheck 18 | - unconvert 19 | - unparam 20 | - unused 21 | linters-settings: 22 | gocyclo: 23 | min-complexity: 16 24 | lll: 25 | line-length: 200 -------------------------------------------------------------------------------- /doc.go: -------------------------------------------------------------------------------- 1 | // Package cnabtooci provides converters to transform CNAB to OCI format and vice versa, 2 | // and high level functions to push and pull bundles to a registry. 3 | // In `cmd/cnab-to-oci` you can find the `main` package to create the `cnab-to-oci` binary 4 | // see https://github.com/cnabio/cnab-to-oci for more information about it. 5 | // 6 | // It can also be used as a library. For more, please refer to http://godoc.org/github.com/cnabio/cnab-to-oci/remotes 7 | package cnabtooci // import "github.com/cnabio/cnab-to-oci" 8 | -------------------------------------------------------------------------------- /.github/workflows/build.yaml: -------------------------------------------------------------------------------- 1 | # yaml-language-server: $schema=https://json.schemastore.org/github-workflow.json 2 | 3 | name: build 4 | on: [push, pull_request] 5 | 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout repository 11 | uses: actions/checkout@v6 12 | - uses: actions/setup-go@v6 13 | with: 14 | go-version-file: go.mod 15 | cache: true 16 | cache-dependency-path: go.sum 17 | - name: Build and Test 18 | run: make all-ci 19 | -------------------------------------------------------------------------------- /remotes/log.go: -------------------------------------------------------------------------------- 1 | package remotes 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "io" 7 | 8 | "github.com/containerd/log" 9 | "github.com/sirupsen/logrus" 10 | ) 11 | 12 | func logPayload(logger *logrus.Entry, payload interface{}) { 13 | buf, err := json.MarshalIndent(payload, "", " ") 14 | if err != nil { 15 | return 16 | } 17 | logger.Debug(string(buf)) 18 | } 19 | 20 | func withMutedContext(ctx context.Context) context.Context { 21 | logger := logrus.New() 22 | logger.SetLevel(logrus.FatalLevel) 23 | logger.SetOutput(io.Discard) 24 | return log.WithLogger(ctx, logrus.NewEntry(logger)) 25 | } 26 | -------------------------------------------------------------------------------- /examples/helloworld-cnab/bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "v1.0.0", 3 | "name": "helloworld", 4 | "version": "0.1.1", 5 | "description": "A short description of your bundle", 6 | "keywords": [ 7 | "helloworld", 8 | "cnab", 9 | "tutorial" 10 | ], 11 | "maintainers": [ 12 | { 13 | "name": "Jane Doe", 14 | "email": "jane.doe@example.com", 15 | "url": "https://example.com" 16 | } 17 | ], 18 | "invocationImages": [ 19 | { 20 | "imageType": "docker", 21 | "image": "cnab/helloworld:0.1.1", 22 | "size": 42 23 | } 24 | ], 25 | "images": null, 26 | "parameters": null, 27 | "credentials": null 28 | } 29 | -------------------------------------------------------------------------------- /cmd/cnab-to-oci/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/sirupsen/logrus" 7 | "github.com/spf13/cobra" 8 | ) 9 | 10 | func main() { 11 | var logLevel string 12 | cmd := &cobra.Command{ 13 | Use: "cnab-to-oci [options]", 14 | SilenceUsage: true, 15 | PersistentPreRunE: func(_ *cobra.Command, _ []string) error { 16 | level, err := logrus.ParseLevel(logLevel) 17 | if err != nil { 18 | return err 19 | } 20 | logrus.SetLevel(level) 21 | return nil 22 | }, 23 | } 24 | cmd.PersistentFlags().StringVar(&logLevel, "log-level", "info", `Set the logging level ("debug"|"info"|"warn"|"error"|"fatal")`) 25 | cmd.AddCommand(fixupCmd(), pushCmd(), pullCmd(), versionCmd()) 26 | if err := cmd.Execute(); err != nil { 27 | os.Exit(1) 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /.github/dependabot.yaml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: gomod 9 | directory: / 10 | labels: 11 | - "dependabot 🤖" 12 | schedule: 13 | interval: weekly 14 | day: sunday 15 | groups: 16 | go-dependencies: 17 | update-types: 18 | - "minor" 19 | - "patch" 20 | 21 | - package-ecosystem: "github-actions" 22 | directory: "/" 23 | labels: 24 | - "dependabot 🤖" 25 | schedule: 26 | interval: weekly 27 | day: sunday -------------------------------------------------------------------------------- /examples/helloworld-cnab/complete.bundle.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": "v1.0.0", 3 | "name": "helloworld", 4 | "version": "0.1.1", 5 | "description": "A short description of your bundle", 6 | "keywords": [ 7 | "helloworld", 8 | "cnab", 9 | "tutorial" 10 | ], 11 | "maintainers": [ 12 | { 13 | "name": "Jane Doe", 14 | "email": "jane.doe@example.com", 15 | "url": "https://example.com" 16 | } 17 | ], 18 | "invocationImages": [ 19 | { 20 | "imageType": "docker", 21 | "image": "cnab/helloworld:0.1.1", 22 | "size": 942, 23 | "contentDigest": "sha256:a59a4e74d9cc89e4e75dfb2cc7ea5c108e4236ba6231b53081a9e2506d1197b6", 24 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json" 25 | } 26 | ], 27 | "images": null, 28 | "parameters": null, 29 | "credentials": null 30 | } 31 | -------------------------------------------------------------------------------- /e2e/testdata/bundle.json.golden.template: -------------------------------------------------------------------------------- 1 | {"actions":{"io.cnab.status":{}},"description":"Hello, World!","images":{"hello":{"contentDigest":"sha256:2c213d6c05a0f68adfe9c7fe1a78a314e5c4fee783e2ee8592d49f10d0c4513f","description":"hello","image":"{{ .ServiceImage }}","imageType":"docker","mediaType":"application/vnd.docker.distribution.manifest.v2+json","size":2401},"whalesay":{"contentDigest":"sha256:b60a020c0f68047b353a4a747f27f5e5ddb17116b7b018762edfb6f7a6439a82","description":"whalesay","image":"{{ .WhalesayImage }}","imageType":"docker","mediaType":"application/vnd.docker.distribution.manifest.v2+json","size":1158}},"invocationImages":[{"contentDigest":"{{ .InvocationDigest }}","image":"{{ .InvocationImage }}","imageType":"docker","mediaType":"application/vnd.docker.distribution.manifest.v2+json","size":941}],"maintainers":[{"email":"user@email.com","name":"user"}],"name":"hello-world","parameters":{"fields":{"definition":"","destination":null}},"schemaVersion":"v1.0.0","version":"0.1.0"} -------------------------------------------------------------------------------- /internal/version.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "fmt" 5 | "runtime" 6 | "strings" 7 | "time" 8 | ) 9 | 10 | var ( 11 | // Version is the git tag that this was built from. 12 | Version = "unknown" 13 | // GitCommit is the commit that this was built from. 14 | GitCommit = "unknown" 15 | // BuildTime is the time at which the binary was built. 16 | BuildTime = "unknown" 17 | ) 18 | 19 | // FullVersion returns a string of version information. 20 | func FullVersion() string { 21 | res := []string{ 22 | fmt.Sprintf("Version: %s", Version), 23 | fmt.Sprintf("Git commit: %s", GitCommit), 24 | fmt.Sprintf("Built: %s", reformatDate(BuildTime)), 25 | fmt.Sprintf("OS/Arch: %s/%s", runtime.GOOS, runtime.GOARCH), 26 | } 27 | return strings.Join(res, "\n") 28 | } 29 | 30 | // FIXME(chris-crone): use function in docker/cli/cli/command/system/version.go. 31 | func reformatDate(buildTime string) string { 32 | t, errTime := time.Parse(time.RFC3339Nano, buildTime) 33 | if errTime == nil { 34 | return t.Format(time.ANSIC) 35 | } 36 | return buildTime 37 | } 38 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG ALPINE_VERSION=3.21 2 | ARG GO_VERSION=1.24.4 3 | 4 | # build image 5 | FROM golang:${GO_VERSION}-alpine${ALPINE_VERSION} AS build 6 | 7 | ARG DOCKERCLI_VERSION=20.10.23 8 | ARG DOCKERCLI_CHANNEL=stable 9 | 10 | ARG BUILDTIME 11 | ARG COMMIT 12 | ARG TAG 13 | ARG GOPROXY 14 | 15 | RUN apk add --no-cache \ 16 | bash \ 17 | make \ 18 | git \ 19 | curl \ 20 | util-linux \ 21 | coreutils \ 22 | build-base 23 | 24 | # Fetch docker cli to run a registry container for e2e tests 25 | RUN curl -Ls https://download.docker.com/linux/static/${DOCKERCLI_CHANNEL}/x86_64/docker-${DOCKERCLI_VERSION}.tgz | tar -xz 26 | 27 | WORKDIR /go/src/github.com/cnabio/cnab-to-oci 28 | COPY . . 29 | RUN make BUILDTIME=$BUILDTIME COMMIT=$COMMIT TAG=$TAG bin/cnab-to-oci &&\ 30 | make BUILDTIME=$BUILDTIME COMMIT=$COMMIT TAG=$TAG build-e2e-test 31 | 32 | # e2e image 33 | FROM alpine:${ALPINE_VERSION} AS e2e 34 | 35 | # copy all the elements needed for e2e tests from build image 36 | COPY --from=build /go/docker/docker /usr/bin/docker 37 | COPY --from=build /go/src/github.com/cnabio/cnab-to-oci/bin/cnab-to-oci /usr/bin/cnab-to-oci 38 | COPY --from=build /go/src/github.com/cnabio/cnab-to-oci/e2e /e2e 39 | COPY --from=build /go/src/github.com/cnabio/cnab-to-oci/e2e.test /e2e/e2e.test 40 | 41 | # Run end-to-end tests 42 | CMD ["e2e/run.sh"] 43 | -------------------------------------------------------------------------------- /e2e/helper_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | "strings" 7 | "testing" 8 | "time" 9 | 10 | "gotest.tools/v3/icmd" 11 | ) 12 | 13 | func startRegistry(t *testing.T) *Container { 14 | c := &Container{image: "registry:2", privatePort: 5000} 15 | c.Start(t) 16 | return c 17 | } 18 | 19 | // Container represents a docker container 20 | type Container struct { 21 | image string 22 | privatePort int 23 | container string 24 | } 25 | 26 | // Start starts a new docker container on a random port 27 | func (c *Container) Start(t *testing.T) { 28 | result := icmd.RunCommand("docker", "run", "--rm", "-d", "-P", c.image).Assert(t, icmd.Success) 29 | c.container = strings.Trim(result.Stdout(), " \r\n") 30 | time.Sleep(time.Second * 3) 31 | } 32 | 33 | // Stop terminates this container 34 | func (c *Container) Stop(t *testing.T) { 35 | icmd.RunCommand("docker", "stop", c.container).Assert(t, icmd.Success) 36 | } 37 | 38 | // GetAddress returns the host:port this container listens on 39 | func (c *Container) GetAddress(t *testing.T) string { 40 | result := icmd.RunCommand("docker", "port", c.container, strconv.Itoa(c.privatePort)).Assert(t, icmd.Success) 41 | port := strings.Split(strings.Split(result.Stdout(), ":")[1], "\n")[0] 42 | return fmt.Sprintf("127.0.0.1:%v", strings.Trim(port, " \r\n")) 43 | } 44 | -------------------------------------------------------------------------------- /converter/types_test.go: -------------------------------------------------------------------------------- 1 | package converter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cnabio/cnab-go/bundle" 7 | "gotest.tools/v3/assert" 8 | ) 9 | 10 | func TestPrepareForPush(t *testing.T) { 11 | b := &bundle.Bundle{} 12 | prepared, err := PrepareForPush(b) 13 | assert.NilError(t, err) 14 | 15 | // First try with OCI format and specific CNAB media type. Fallback should be set. 16 | assert.Equal(t, prepared.ManifestDescriptor.MediaType, "application/vnd.oci.image.manifest.v1+json") 17 | assert.Equal(t, prepared.ConfigBlobDescriptor.MediaType, "application/vnd.cnab.config.v1+json") 18 | assert.Check(t, prepared.Fallback != nil) 19 | // Try the first fallback, which set the media type to image config and still using OCI format 20 | fallback := prepared.Fallback 21 | assert.Equal(t, fallback.ManifestDescriptor.MediaType, "application/vnd.oci.image.manifest.v1+json") 22 | assert.Equal(t, fallback.ConfigBlobDescriptor.MediaType, "application/vnd.oci.image.config.v1+json") 23 | assert.Check(t, fallback.Fallback != nil) 24 | // Last fallback uses Docker format 25 | lastFallback := fallback.Fallback 26 | assert.Equal(t, lastFallback.ManifestDescriptor.MediaType, "application/vnd.docker.distribution.manifest.v2+json") 27 | assert.Equal(t, lastFallback.ConfigBlobDescriptor.MediaType, "application/vnd.docker.container.image.v1+json") 28 | } 29 | -------------------------------------------------------------------------------- /e2e/testdata/hello-world/bundle.json.template: -------------------------------------------------------------------------------- 1 | { 2 | "actions": { 3 | "io.cnab.status": {} 4 | }, 5 | "def ainitions": { 6 | "port": { 7 | "default": "8080", 8 | "type": "string" 9 | }, 10 | "text": { 11 | "default": "Hello, World!", 12 | "type": "string" 13 | } 14 | }, 15 | "description": "Hello, World!", 16 | "images": { 17 | "hello": { 18 | "description": "hello", 19 | "image": "{{ .ServiceImage }}", 20 | "imageType": "docker" 21 | }, 22 | "whalesay": { 23 | "description": "whalesay", 24 | "image": "{{ .WhalesayImage}}", 25 | "imageType": "docker" 26 | } 27 | }, 28 | "invocationImages": [ 29 | { 30 | "image": "{{ .InvocationImage }}", 31 | "imageType": "docker" 32 | } 33 | ], 34 | "maintainers": [ 35 | { 36 | "email": "user@email.com", 37 | "name": "user" 38 | } 39 | ], 40 | "name": "hello-world", 41 | "parameters": { 42 | "fields": { 43 | "port": { 44 | "definition": "port", 45 | "destination": { 46 | "env": "PORT" 47 | } 48 | }, 49 | "text": { 50 | "definition": "text", 51 | "destination": { 52 | "env": "HELLO_TEXT" 53 | } 54 | } 55 | } 56 | }, 57 | "schemaVersion": "v1.0.0", 58 | "version": "0.1.0" 59 | } 60 | -------------------------------------------------------------------------------- /cmd/cnab-to-oci/pull.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "os" 8 | 9 | "github.com/cnabio/cnab-to-oci/remotes" 10 | "github.com/cyberphone/json-canonicalization/go/src/webpki.org/jsoncanonicalizer" 11 | "github.com/distribution/reference" 12 | "github.com/spf13/cobra" 13 | ) 14 | 15 | type pullOptions struct { 16 | bundle string 17 | relocationMap string 18 | targetRef string 19 | insecureRegistries []string 20 | } 21 | 22 | func pullCmd() *cobra.Command { 23 | var opts pullOptions 24 | cmd := &cobra.Command{ 25 | Use: "pull [options]", 26 | Short: "Pulls an image reference", 27 | Args: cobra.ExactArgs(1), 28 | RunE: func(_ *cobra.Command, args []string) error { 29 | opts.targetRef = args[0] 30 | return runPull(opts) 31 | }, 32 | } 33 | 34 | cmd.Flags().StringVar(&opts.bundle, "bundle", "pulled.json", "bundle output file (- to print on standard output)") 35 | cmd.Flags().StringVar(&opts.relocationMap, "relocation-map", "relocation-map.json", "relocation map output file (- to print on standard output)") 36 | cmd.Flags().StringSliceVar(&opts.insecureRegistries, "insecure-registries", nil, "Use plain HTTP for those registries") 37 | return cmd 38 | } 39 | 40 | func runPull(opts pullOptions) error { 41 | ref, err := reference.ParseNormalizedNamed(opts.targetRef) 42 | if err != nil { 43 | return err 44 | } 45 | 46 | b, relocationMap, _, err := remotes.Pull(context.Background(), ref, createResolver(opts.insecureRegistries)) 47 | if err != nil { 48 | return err 49 | } 50 | if err := writeOutput(opts.bundle, b); err != nil { 51 | return err 52 | } 53 | return writeOutput(opts.relocationMap, relocationMap) 54 | } 55 | 56 | func writeOutput(file string, data interface{}) error { 57 | plainJSON, err := json.Marshal(data) 58 | if err != nil { 59 | return err 60 | } 61 | bytes, err := jsoncanonicalizer.Transform(plainJSON) 62 | if err != nil { 63 | return err 64 | } 65 | if file == "-" { 66 | fmt.Fprintln(os.Stdout, string(bytes)) 67 | return nil 68 | } 69 | return os.WriteFile(file, bytes, 0644) 70 | } 71 | -------------------------------------------------------------------------------- /remotes/typeless.go: -------------------------------------------------------------------------------- 1 | package remotes 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1" 7 | ) 8 | 9 | type typelessManifestList struct { 10 | Manifests []typelessDescriptor 11 | extras map[string]json.RawMessage 12 | } 13 | 14 | func (m *typelessManifestList) MarshalJSON() ([]byte, error) { 15 | data := map[string]json.RawMessage{} 16 | for k, v := range m.extras { 17 | data[k] = v 18 | } 19 | if len(m.Manifests) != 0 { 20 | manifestsJSON, err := json.Marshal(m.Manifests) 21 | if err != nil { 22 | return nil, err 23 | } 24 | data["manifests"] = json.RawMessage(manifestsJSON) 25 | } 26 | return json.Marshal(data) 27 | } 28 | 29 | func (m *typelessManifestList) UnmarshalJSON(source []byte) error { 30 | var data map[string]json.RawMessage 31 | if err := json.Unmarshal(source, &data); err != nil { 32 | return err 33 | } 34 | if manifestsJSON, ok := data["manifests"]; ok { 35 | if err := json.Unmarshal(manifestsJSON, &m.Manifests); err != nil { 36 | return err 37 | } 38 | delete(data, "manifests") 39 | } 40 | m.extras = data 41 | return nil 42 | } 43 | 44 | type typelessDescriptor struct { 45 | Platform *ocischemav1.Platform 46 | extras map[string]json.RawMessage 47 | } 48 | 49 | func (d *typelessDescriptor) MarshalJSON() ([]byte, error) { 50 | data := map[string]json.RawMessage{} 51 | for k, v := range d.extras { 52 | data[k] = v 53 | } 54 | if d.Platform != nil { 55 | platJSON, err := json.Marshal(d.Platform) 56 | if err != nil { 57 | return nil, err 58 | } 59 | data["platform"] = json.RawMessage(platJSON) 60 | } 61 | return json.Marshal(data) 62 | } 63 | 64 | func (d *typelessDescriptor) UnmarshalJSON(source []byte) error { 65 | var data map[string]json.RawMessage 66 | if err := json.Unmarshal(source, &data); err != nil { 67 | return err 68 | } 69 | if platJSON, ok := data["platform"]; ok { 70 | var plat ocischemav1.Platform 71 | if err := json.Unmarshal(platJSON, &plat); err != nil { 72 | return err 73 | } 74 | d.Platform = &plat 75 | delete(data, "platform") 76 | } 77 | d.extras = data 78 | return nil 79 | } 80 | -------------------------------------------------------------------------------- /remotes/mount_test.go: -------------------------------------------------------------------------------- 1 | package remotes 2 | 3 | import ( 4 | "context" 5 | "net/http" 6 | "net/http/httptest" 7 | "net/url" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/containerd/containerd/remotes/docker" 12 | "github.com/distribution/reference" 13 | ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1" 14 | "gotest.tools/v3/assert" 15 | ) 16 | 17 | type twoStepReader struct { 18 | first []byte 19 | second []byte 20 | hasReadFirst bool 21 | } 22 | 23 | func (r *twoStepReader) Read(p []byte) (n int, err error) { 24 | if r.hasReadFirst { 25 | return copy(p, r.second), nil 26 | } 27 | r.hasReadFirst = true 28 | return copy(p, r.first), nil 29 | } 30 | func (r *twoStepReader) Close() error { 31 | return nil 32 | } 33 | 34 | func TestRemoteReaderAtShortReads(t *testing.T) { 35 | helloWorld := []byte("Hello world!") 36 | r := &twoStepReader{ 37 | first: helloWorld[:5], 38 | second: helloWorld[5:], 39 | } 40 | tested := &remoteReaderAt{ 41 | ReadCloser: r, 42 | size: int64(len(helloWorld)), 43 | } 44 | 45 | actual := make([]byte, len(helloWorld)) 46 | n, err := tested.ReadAt(actual, 0) 47 | assert.NilError(t, err) 48 | assert.Equal(t, n, len(helloWorld)) 49 | assert.DeepEqual(t, helloWorld, actual) 50 | } 51 | 52 | func TestMountOnPush(t *testing.T) { 53 | hasMounted := false 54 | handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { 55 | // Testing if mount is called, mount API call is in the form of: 56 | // POST http://///blobs/uploads?from=/ 57 | if strings.Contains(r.URL.EscapedPath(), "library/test/blobs/uploads/") && strings.Contains(r.URL.Query().Get("from"), "library/busybox") { 58 | hasMounted = true 59 | } 60 | // We don't really care what we send here. 61 | w.WriteHeader(404) 62 | }) 63 | server := httptest.NewServer(handler) 64 | defer server.Close() 65 | 66 | resolver := docker.NewResolver(docker.ResolverOptions{ 67 | PlainHTTP: true, 68 | }) 69 | 70 | u, err := url.Parse(server.URL) 71 | assert.NilError(t, err) 72 | 73 | r, err := resolver.Pusher(context.TODO(), u.Hostname()+":"+u.Port()+"/library/test") 74 | assert.NilError(t, err) 75 | 76 | ref, err := reference.WithName(u.Hostname() + "/library/busybox") 77 | assert.NilError(t, err) 78 | 79 | desc := ocischemav1.Descriptor{} 80 | _, _ = pushWithAnnotation(context.TODO(), r, ref, desc) 81 | assert.Equal(t, hasMounted, true) 82 | } 83 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := all 2 | SHELL:=/bin/bash 3 | 4 | PKG_NAME := github.com/cnabio/cnab-to-oci 5 | 6 | EXEC_EXT := 7 | ifeq ($(OS),Windows_NT) 8 | EXEC_EXT := .exe 9 | endif 10 | 11 | ifeq ($(TAG),) 12 | TAG := $(shell git describe --always --dirty 2> /dev/null) 13 | endif 14 | ifeq ($(COMMIT),) 15 | COMMIT := $(shell git rev-parse --short HEAD 2> /dev/null) 16 | endif 17 | 18 | ifeq ($(BUILDTIME),) 19 | BUILDTIME := $(shell date -u +"%Y-%m-%dT%H:%M:%SZ" 2> /dev/null) 20 | endif 21 | ifeq ($(BUILDTIME),) 22 | BUILDTIME := unknown 23 | $(warning unable to set BUILDTIME. Set the value manually) 24 | endif 25 | 26 | LDFLAGS := "-s -w \ 27 | -X $(PKG_NAME)/internal.GitCommit=$(COMMIT) \ 28 | -X $(PKG_NAME)/internal.Version=$(TAG) \ 29 | -X $(PKG_NAME)/internal.BuildTime=$(BUILDTIME)" 30 | 31 | BUILD_ARGS := \ 32 | --build-arg BUILDTIME \ 33 | --build-arg COMMIT \ 34 | --build-arg TAG \ 35 | --build-arg=GOPROXY 36 | 37 | GO_BUILD := CGO_ENABLED=0 go build -ldflags=$(LDFLAGS) 38 | GO_TEST := CGO_ENABLED=0 go test -ldflags=$(LDFLAGS) -failfast 39 | GO_TEST_RACE := go test -ldflags=$(LDFLAGS) -failfast -race 40 | 41 | all: build test 42 | 43 | all-ci: lint all 44 | 45 | check_go_env: 46 | @test $$(go list) = "$(PKG_NAME)" || \ 47 | (echo "Invalid Go environment - The local directory structure must match: $(PKG_NAME)" && false) 48 | 49 | get-tools: 50 | go install golang.org/x/tools/cmd/goimports@latest 51 | go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.63.4 52 | 53 | # Default build 54 | build: bin/cnab-to-oci 55 | 56 | bin/%: cmd/% check_go_env 57 | $(GO_BUILD) -o $@$(EXEC_EXT) ./$< 58 | 59 | install: 60 | pushd cmd/cnab-to-oci && go install && popd 61 | 62 | clean: 63 | rm -rf bin 64 | 65 | test: test-unit test-e2e 66 | 67 | test-unit: 68 | $(GO_TEST_RACE) $(shell go list ./... | grep -vE '/e2e') 69 | 70 | test-e2e: e2e-image 71 | docker run --rm --network=host -v /var/run/docker.sock:/var/run/docker.sock cnab-to-oci-e2e 72 | 73 | build-e2e-test: 74 | $(GO_TEST) -c github.com/cnabio/cnab-to-oci/e2e 75 | 76 | e2e-image: 77 | docker build $(BUILD_ARGS) . -t cnab-to-oci-e2e 78 | 79 | format: get-tools 80 | go fmt ./... 81 | @for source in `find . -type f -name '*.go' -not -path "./vendor/*"`; do \ 82 | goimports -w $$source ; \ 83 | done 84 | 85 | lint: get-tools 86 | golangci-lint run ./... 87 | 88 | .PHONY: all get-tools build clean test test-unit test-e2e e2e-image lint 89 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | We have [good first issues][good-first-issue] for new contributors and [help wanted][help-wanted] issues for our other contributors. 2 | 3 | - `good first issue` has extra information to help you make your first contribution. 4 | - `help wanted` are issues suitable for someone who isn't a core maintainer. 5 | 6 | Maintainers will do their best regularly make new issues for you to solve and then help out as you work on them. 💖 7 | 8 | # Philosophy 9 | 10 | PRs are most welcome! 11 | 12 | - If there isn't an issue for your PR, please make an issue first and explain the problem or motivation for 13 | the change you are proposing. When the solution isn't straightforward, for example "Implement missing command X", 14 | then also outline your proposed solution. Your PR will go smoother if the solution is agreed upon before you've 15 | spent a lot of time implementing it. 16 | - It's OK to submit a PR directly for problems such as misspellings or other things where the motivation/problem is 17 | unambiguous. 18 | - If you aren't sure about your solution yet, put WIP in the title or open as a draft PR so that people know to be nice and 19 | wait for you to finish before commenting. 20 | - Try to keep your PRs to a single task. Please don't tackle multiple things in a single PR if possible. Otherwise, grouping related changes into commits will help us out a bunch when reviewing! 21 | - We encourage "follow-on PRs". If the core of your changes are good, and it won't hurt to do more of 22 | the changes later, we like to merge early, and keep working on it in another PR so that others can build 23 | on top of your work. 24 | 25 | When you're ready to get started, we recommend the following workflow: 26 | 27 | ``` 28 | $ go build ./... 29 | $ go test ./... 30 | $ golangci-lint run --config ./golangci.yml 31 | ``` 32 | 33 | We currently use [dep](https://github.com/golang/dep) for dependency management. 34 | 35 | [good-first-issue]: https://github.com/cnabio/cnab-to-oci/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22 36 | [help-wanted]: https://github.com/cnabio/cnab-to-oci/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22 37 | 38 | # Cutting a Release 39 | 40 | When you are asked to cut a new release, here is the process: 41 | 42 | 1. Figure out the correct version number, we follow [semver](semver.org) and 43 | have a funny [release naming scheme][release-name]: 44 | - Bump the major segment if there are any breaking changes. 45 | - Bump the minor segment if there are new features only. 46 | - Bump the patch segment if there are bug fixes only. 47 | - Bump the build segment (version-prerelease.BUILDTAG+releasename) if you only 48 | fixed something in the build, but the final binaries are the same. 49 | 50 | 1. Ensure that the CI build is passing, then make the tag and push it. 51 | 52 | ``` 53 | git checkout main 54 | git pull 55 | git tag VERSION -a -m "" 56 | git push --tags 57 | ``` 58 | -------------------------------------------------------------------------------- /cmd/cnab-to-oci/fixup.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "os" 7 | 8 | "github.com/cnabio/cnab-go/bundle" 9 | "github.com/cnabio/cnab-to-oci/remotes" 10 | containerdRemotes "github.com/containerd/containerd/remotes" 11 | "github.com/distribution/reference" 12 | "github.com/docker/cli/cli/config" 13 | "github.com/spf13/cobra" 14 | ) 15 | 16 | type fixupOptions struct { 17 | input string 18 | bundle string 19 | relocationMap string 20 | targetRef string 21 | insecureRegistries []string 22 | autoUpdateBundle bool 23 | } 24 | 25 | func fixupCmd() *cobra.Command { 26 | var opts fixupOptions 27 | cmd := &cobra.Command{ 28 | Use: "fixup [options]", 29 | Short: "Fixes the digest of an image", 30 | Long: "The fixup command resolves all the digest references from a registry and patches the bundle.json with them.", 31 | Args: cobra.ExactArgs(1), 32 | RunE: func(_ *cobra.Command, args []string) error { 33 | opts.input = args[0] 34 | return runFixup(opts) 35 | }, 36 | } 37 | cmd.Flags().StringVar(&opts.bundle, "bundle", "fixed-bundle.json", "fixed bundle output file (- to print on standard output)") 38 | cmd.Flags().StringVar(&opts.relocationMap, "relocation-map", "relocation-map.json", "relocation map output file (- to print on standard output)") 39 | cmd.Flags().StringVarP(&opts.targetRef, "target", "t", "", "reference where the bundle will be pushed") 40 | cmd.Flags().StringSliceVar(&opts.insecureRegistries, "insecure-registries", nil, "Use plain HTTP for those registries") 41 | cmd.Flags().BoolVar(&opts.autoUpdateBundle, "auto-update-bundle", false, "Updates the bundle image properties with the one resolved on the registry") 42 | return cmd 43 | } 44 | 45 | func runFixup(opts fixupOptions) error { 46 | bundleJSON, err := os.ReadFile(opts.input) 47 | if err != nil { 48 | return err 49 | } 50 | 51 | b, err := bundle.Unmarshal(bundleJSON) 52 | if err != nil { 53 | return err 54 | } 55 | 56 | ref, err := reference.ParseNormalizedNamed(opts.targetRef) 57 | if err != nil { 58 | return err 59 | } 60 | 61 | fixupOptions := []remotes.FixupOption{ 62 | remotes.WithEventCallback(displayEvent), 63 | } 64 | if opts.autoUpdateBundle { 65 | fixupOptions = append(fixupOptions, remotes.WithAutoBundleUpdate()) 66 | } 67 | relocationMap, err := remotes.FixupBundle(context.Background(), b, ref, createResolver(opts.insecureRegistries), fixupOptions...) 68 | if err != nil { 69 | return err 70 | } 71 | if err := writeOutput(opts.bundle, b); err != nil { 72 | return err 73 | } 74 | return writeOutput(opts.relocationMap, relocationMap) 75 | } 76 | 77 | func displayEvent(ev remotes.FixupEvent) { 78 | switch ev.EventType { 79 | case remotes.FixupEventTypeCopyImageStart: 80 | fmt.Fprintf(os.Stderr, "Starting to copy image %s...\n", ev.SourceImage) 81 | case remotes.FixupEventTypeCopyImageEnd: 82 | if ev.Error != nil { 83 | fmt.Fprintf(os.Stderr, "Failed to copy image %s: %s\n", ev.SourceImage, ev.Error) 84 | } else { 85 | fmt.Fprintf(os.Stderr, "Completed image %s copy\n", ev.SourceImage) 86 | } 87 | } 88 | } 89 | 90 | func createResolver(insecureRegistries []string) containerdRemotes.Resolver { 91 | return remotes.CreateResolver(config.LoadDefaultConfigFile(os.Stderr), insecureRegistries...) 92 | } 93 | -------------------------------------------------------------------------------- /cmd/cnab-to-oci/push.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "os" 9 | 10 | "github.com/cnabio/cnab-go/bundle" 11 | "github.com/cnabio/cnab-to-oci/remotes" 12 | "github.com/distribution/reference" 13 | "github.com/docker/docker/client" 14 | "github.com/spf13/cobra" 15 | ) 16 | 17 | type pushOptions struct { 18 | input string 19 | targetRef string 20 | insecureRegistries []string 21 | allowFallbacks bool 22 | invocationPlatforms []string 23 | componentPlatforms []string 24 | autoUpdateBundle bool 25 | pushImages bool 26 | } 27 | 28 | func pushCmd() *cobra.Command { 29 | var opts pushOptions 30 | cmd := &cobra.Command{ 31 | Use: "push [options]", 32 | Short: "Fixes and pushes the bundle to an registry", 33 | Args: cobra.ExactArgs(1), 34 | RunE: func(_ *cobra.Command, args []string) error { 35 | opts.input = args[0] 36 | if opts.targetRef == "" { 37 | return errors.New("--target flag must be set with a namespace ") 38 | } 39 | return runPush(opts) 40 | }, 41 | } 42 | 43 | cmd.Flags().StringVarP(&opts.targetRef, "target", "t", "", "reference where the bundle will be pushed") 44 | cmd.Flags().StringSliceVar(&opts.insecureRegistries, "insecure-registries", nil, "Use plain HTTP for those registries") 45 | cmd.Flags().BoolVar(&opts.allowFallbacks, "allow-fallbacks", true, "Enable automatic compatibility fallbacks for registries without support for custom media type, or OCI manifests") 46 | cmd.Flags().StringSliceVar(&opts.invocationPlatforms, "invocation-platforms", nil, "Platforms to push (for multi-arch invocation images)") 47 | cmd.Flags().StringSliceVar(&opts.componentPlatforms, "component-platforms", nil, "Platforms to push (for multi-arch component images)") 48 | cmd.Flags().BoolVar(&opts.autoUpdateBundle, "auto-update-bundle", false, "Updates the bundle image properties with the one resolved on the registry") 49 | cmd.Flags().BoolVar(&opts.pushImages, "push-images", true, "Allow to push missing images in the registry that are available in the local docker daemon image store") 50 | 51 | return cmd 52 | } 53 | 54 | func runPush(opts pushOptions) error { 55 | var b bundle.Bundle 56 | bundleJSON, err := os.ReadFile(opts.input) 57 | if err != nil { 58 | return err 59 | } 60 | if err := json.Unmarshal(bundleJSON, &b); err != nil { 61 | return err 62 | } 63 | resolver := createResolver(opts.insecureRegistries) 64 | ref, err := reference.ParseNormalizedNamed(opts.targetRef) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | fixupOptions := []remotes.FixupOption{ 70 | remotes.WithEventCallback(displayEvent), 71 | remotes.WithInvocationImagePlatforms(opts.invocationPlatforms), 72 | remotes.WithComponentImagePlatforms(opts.componentPlatforms), 73 | } 74 | if opts.autoUpdateBundle { 75 | fixupOptions = append(fixupOptions, remotes.WithAutoBundleUpdate()) 76 | } 77 | if opts.pushImages { 78 | cli, err := client.NewClientWithOpts(client.FromEnv) 79 | if err != nil { 80 | return err 81 | } 82 | fixupOptions = append(fixupOptions, remotes.WithPushImages(cli, os.Stdout)) 83 | } 84 | relocationMap, err := remotes.FixupBundle(context.Background(), &b, ref, resolver, fixupOptions...) 85 | if err != nil { 86 | return err 87 | } 88 | d, err := remotes.Push(context.Background(), &b, relocationMap, ref, resolver, opts.allowFallbacks) 89 | if err != nil { 90 | return err 91 | } 92 | fmt.Printf("Pushed successfully, with digest %q\n", d.Digest) 93 | return nil 94 | } 95 | -------------------------------------------------------------------------------- /remotes/fixupevent.go: -------------------------------------------------------------------------------- 1 | package remotes 2 | 3 | import ( 4 | "sync" 5 | 6 | "github.com/distribution/reference" 7 | ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1" 8 | ) 9 | 10 | // FixupEvent is an event that is raised by the Fixup Logic 11 | type FixupEvent struct { 12 | SourceImage string 13 | DestinationRef reference.Named 14 | EventType FixupEventType 15 | Message string 16 | Error error 17 | Progress ProgressSnapshot 18 | } 19 | 20 | // FixupEventType is the the type of event raised by the Fixup logic 21 | type FixupEventType string 22 | 23 | const ( 24 | // FixupEventTypeCopyImageStart is raised when the Fixup logic starts copying an 25 | // image 26 | FixupEventTypeCopyImageStart = FixupEventType("CopyImageStart") 27 | 28 | // FixupEventTypeCopyImageEnd is raised when the Fixup logic stops copying an 29 | // image. Error might be populated 30 | FixupEventTypeCopyImageEnd = FixupEventType("CopyImageEnd") 31 | 32 | // FixupEventTypeProgress is raised when Fixup logic reports progression 33 | FixupEventTypeProgress = FixupEventType("Progress") 34 | ) 35 | 36 | type descriptorProgress struct { 37 | ocischemav1.Descriptor 38 | done bool 39 | action string 40 | err error 41 | children []*descriptorProgress 42 | mut sync.RWMutex 43 | } 44 | 45 | func (p *descriptorProgress) markDone() { 46 | p.mut.Lock() 47 | defer p.mut.Unlock() 48 | p.done = true 49 | } 50 | 51 | func (p *descriptorProgress) setAction(a string) { 52 | p.mut.Lock() 53 | defer p.mut.Unlock() 54 | p.action = a 55 | } 56 | 57 | func (p *descriptorProgress) setError(err error) { 58 | p.mut.Lock() 59 | defer p.mut.Unlock() 60 | p.err = err 61 | } 62 | 63 | func (p *descriptorProgress) addChild(child *descriptorProgress) { 64 | p.mut.Lock() 65 | defer p.mut.Unlock() 66 | p.children = append(p.children, child) 67 | } 68 | 69 | func (p *descriptorProgress) snapshot() DescriptorProgressSnapshot { 70 | p.mut.RLock() 71 | defer p.mut.RUnlock() 72 | result := DescriptorProgressSnapshot{ 73 | Descriptor: p.Descriptor, 74 | Done: p.done, 75 | Action: p.action, 76 | Error: p.err, 77 | } 78 | if len(p.children) != 0 { 79 | result.Children = make([]DescriptorProgressSnapshot, len(p.children)) 80 | for ix, child := range p.children { 81 | result.Children[ix] = child.snapshot() 82 | } 83 | } 84 | return result 85 | } 86 | 87 | type progress struct { 88 | roots []*descriptorProgress 89 | mut sync.RWMutex 90 | } 91 | 92 | func (p *progress) addRoot(root *descriptorProgress) { 93 | p.mut.Lock() 94 | defer p.mut.Unlock() 95 | p.roots = append(p.roots, root) 96 | } 97 | 98 | func (p *progress) snapshot() ProgressSnapshot { 99 | p.mut.RLock() 100 | defer p.mut.RUnlock() 101 | result := ProgressSnapshot{} 102 | if len(p.roots) != 0 { 103 | result.Roots = make([]DescriptorProgressSnapshot, len(p.roots)) 104 | for ix, root := range p.roots { 105 | result.Roots[ix] = root.snapshot() 106 | } 107 | } 108 | return result 109 | } 110 | 111 | // DescriptorProgressSnapshot describes the current progress of a descriptor 112 | type DescriptorProgressSnapshot struct { 113 | ocischemav1.Descriptor 114 | Done bool 115 | Action string 116 | Error error 117 | Children []DescriptorProgressSnapshot 118 | } 119 | 120 | // ProgressSnapshot describes the current progress of a Fixup operation 121 | type ProgressSnapshot struct { 122 | Roots []DescriptorProgressSnapshot 123 | } 124 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/cnabio/cnab-to-oci 2 | 3 | go 1.24.4 4 | 5 | require ( 6 | github.com/cnabio/cnab-go v0.25.5 7 | github.com/containerd/containerd v1.7.30 8 | github.com/containerd/log v0.1.0 9 | github.com/containerd/platforms v0.2.1 10 | github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 11 | github.com/distribution/distribution v2.8.3+incompatible 12 | github.com/distribution/reference v0.6.1-0.20240718132515-8c942b0459df 13 | github.com/docker/cli v29.1.3+incompatible 14 | github.com/docker/distribution v2.8.3+incompatible 15 | github.com/docker/docker v28.5.2+incompatible 16 | github.com/hashicorp/go-multierror v1.1.1 17 | github.com/opencontainers/go-digest v1.0.0 18 | github.com/opencontainers/image-spec v1.1.1 19 | github.com/sirupsen/logrus v1.9.3 20 | github.com/spf13/cobra v1.10.2 21 | golang.org/x/sync v0.19.0 22 | gotest.tools/v3 v3.5.2 23 | ) 24 | 25 | require ( 26 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 // indirect 27 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect 28 | github.com/Masterminds/semver v1.5.0 // indirect 29 | github.com/Microsoft/go-winio v0.6.2 // indirect 30 | github.com/Microsoft/hcsshim v0.12.9 // indirect 31 | github.com/beorn7/perks v1.0.1 // indirect 32 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 33 | github.com/containerd/errdefs v1.0.0 // indirect 34 | github.com/containerd/errdefs/pkg v0.3.0 // indirect 35 | github.com/docker/docker-credential-helpers v0.9.3 // indirect 36 | github.com/docker/go-connections v0.6.0 // indirect 37 | github.com/docker/go-metrics v0.0.1 // indirect 38 | github.com/docker/go-units v0.5.0 // indirect 39 | github.com/felixge/httpsnoop v1.0.4 // indirect 40 | github.com/go-logr/logr v1.4.2 // indirect 41 | github.com/go-logr/stdr v1.2.2 // indirect 42 | github.com/google/go-cmp v0.7.0 // indirect 43 | github.com/gorilla/mux v1.8.1 // indirect 44 | github.com/hashicorp/errwrap v1.1.0 // indirect 45 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 46 | github.com/klauspost/compress v1.18.0 // indirect 47 | github.com/moby/docker-image-spec v1.3.1 // indirect 48 | github.com/moby/locker v1.0.1 // indirect 49 | github.com/moby/term v0.5.2 // indirect 50 | github.com/morikuni/aec v1.0.0 // indirect 51 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 52 | github.com/pkg/errors v0.9.1 // indirect 53 | github.com/prometheus/client_golang v1.20.5 // indirect 54 | github.com/prometheus/client_model v0.6.1 // indirect 55 | github.com/prometheus/common v0.61.0 // indirect 56 | github.com/prometheus/procfs v0.15.1 // indirect 57 | github.com/qri-io/jsonpointer v0.1.1 // indirect 58 | github.com/qri-io/jsonschema v0.2.2-0.20210831022256-780655b2ba0e // indirect 59 | github.com/sergi/go-diff v1.3.1 // indirect 60 | github.com/spf13/pflag v1.0.9 // indirect 61 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb // indirect 62 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect 63 | github.com/xeipuuv/gojsonschema v1.2.0 // indirect 64 | go.opentelemetry.io/auto/sdk v1.1.0 // indirect 65 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 // indirect 66 | go.opentelemetry.io/otel v1.33.0 // indirect 67 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 // indirect 68 | go.opentelemetry.io/otel/metric v1.33.0 // indirect 69 | go.opentelemetry.io/otel/trace v1.33.0 // indirect 70 | golang.org/x/sys v0.38.0 // indirect 71 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 // indirect 72 | google.golang.org/grpc v1.69.2 // indirect 73 | google.golang.org/protobuf v1.36.5 // indirect 74 | ) 75 | -------------------------------------------------------------------------------- /remotes/mocks_test.go: -------------------------------------------------------------------------------- 1 | package remotes 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/containerd/containerd/content" 10 | "github.com/containerd/containerd/remotes" 11 | "github.com/docker/docker/api/types/image" 12 | "github.com/opencontainers/go-digest" 13 | ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1" 14 | ) 15 | 16 | // Mock remote.Resolver interface 17 | type mockResolver struct { 18 | resolvedDescriptors []ocischemav1.Descriptor 19 | pushedReferences []string 20 | pusher *mockPusher 21 | fetcher *mockFetcher 22 | } 23 | 24 | func (r *mockResolver) Resolve(_ context.Context, ref string) (string, ocischemav1.Descriptor, error) { 25 | descriptor := r.resolvedDescriptors[0] 26 | r.resolvedDescriptors = r.resolvedDescriptors[1:] 27 | if descriptor.Size == -1 { 28 | return "", descriptor, fmt.Errorf("empty descriptor") 29 | } 30 | return ref, descriptor, nil 31 | } 32 | func (r *mockResolver) Fetcher(_ context.Context, _ string) (remotes.Fetcher, error) { 33 | return r.fetcher, nil 34 | } 35 | func (r *mockResolver) Pusher(_ context.Context, ref string) (remotes.Pusher, error) { 36 | r.pushedReferences = append(r.pushedReferences, ref) 37 | return r.pusher, nil 38 | } 39 | 40 | // Mock remotes.Pusher interface 41 | type mockPusher struct { 42 | pushedDescriptors []ocischemav1.Descriptor 43 | buffers []*bytes.Buffer 44 | returnErrorValues []error 45 | } 46 | 47 | func newMockPusher(ret []error) *mockPusher { 48 | return &mockPusher{ 49 | pushedDescriptors: []ocischemav1.Descriptor{}, 50 | buffers: []*bytes.Buffer{}, 51 | returnErrorValues: ret, 52 | } 53 | } 54 | 55 | func (p *mockPusher) Push(_ context.Context, d ocischemav1.Descriptor) (content.Writer, error) { 56 | p.pushedDescriptors = append(p.pushedDescriptors, d) 57 | buf := &bytes.Buffer{} 58 | p.buffers = append(p.buffers, buf) 59 | var err error 60 | if p.returnErrorValues != nil { 61 | err = p.returnErrorValues[0] 62 | p.returnErrorValues = p.returnErrorValues[1:] 63 | } 64 | return &mockWriter{ 65 | WriteCloser: nopWriteCloser{Buffer: buf}, 66 | }, err 67 | } 68 | 69 | // Mock content.Writer interface 70 | type mockWriter struct { 71 | io.WriteCloser 72 | } 73 | 74 | func (w mockWriter) Digest() digest.Digest { return "" } 75 | func (w mockWriter) Commit(_ context.Context, _ int64, _ digest.Digest, _ ...content.Opt) error { 76 | return nil 77 | } 78 | func (w mockWriter) Status() (content.Status, error) { return content.Status{}, nil } 79 | func (w mockWriter) Truncate(_ int64) error { return nil } 80 | 81 | type nopWriteCloser struct { 82 | *bytes.Buffer 83 | } 84 | 85 | func (n nopWriteCloser) Close() error { return nil } 86 | 87 | // Mock remotes.Fetcher interface 88 | type mockFetcher struct { 89 | indexBuffers []*bytes.Buffer 90 | } 91 | 92 | func (f *mockFetcher) Fetch(_ context.Context, _ ocischemav1.Descriptor) (io.ReadCloser, error) { 93 | rc := io.NopCloser(f.indexBuffers[0]) 94 | f.indexBuffers = f.indexBuffers[1:] 95 | return rc, nil 96 | } 97 | 98 | type mockReadCloser struct { 99 | } 100 | 101 | func (rc mockReadCloser) Read(_ []byte) (n int, err error) { 102 | return 0, io.EOF 103 | } 104 | 105 | func (rc mockReadCloser) Close() error { 106 | return nil 107 | } 108 | 109 | type mockImageClient struct { 110 | pushedImages int 111 | taggedImages map[string]string 112 | } 113 | 114 | func newMockImageClient() *mockImageClient { 115 | return &mockImageClient{taggedImages: map[string]string{}} 116 | } 117 | 118 | func (c *mockImageClient) ImagePush(_ context.Context, _ string, _ image.PushOptions) (io.ReadCloser, error) { 119 | c.pushedImages++ 120 | return mockReadCloser{}, nil 121 | } 122 | func (c *mockImageClient) ImageTag(_ context.Context, image, ref string) error { 123 | c.taggedImages[image] = ref 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /converter/types.go: -------------------------------------------------------------------------------- 1 | package converter 2 | 3 | import ( 4 | "encoding/json" 5 | 6 | "github.com/cnabio/cnab-go/bundle" 7 | "github.com/distribution/distribution/manifest/schema2" 8 | "github.com/docker/distribution" 9 | "github.com/opencontainers/go-digest" 10 | ocischema "github.com/opencontainers/image-spec/specs-go" 11 | ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1" 12 | ) 13 | 14 | const ( 15 | // CNABConfigMediaType is the config media type of the CNAB config image manifest 16 | CNABConfigMediaType = "application/vnd.cnab.config.v1+json" 17 | ) 18 | 19 | // PreparedBundleConfig contains the config blob, image manifest (and fallback), and descriptors for a CNAB config 20 | type PreparedBundleConfig struct { 21 | ConfigBlob []byte 22 | ConfigBlobDescriptor ocischemav1.Descriptor 23 | Manifest []byte 24 | ManifestDescriptor ocischemav1.Descriptor 25 | Fallback *PreparedBundleConfig 26 | } 27 | 28 | // PrepareForPush serializes a bundle config, generates its image manifest, and its manifest descriptor 29 | func PrepareForPush(b *bundle.Bundle) (*PreparedBundleConfig, error) { 30 | blob, err := b.Marshal() 31 | if err != nil { 32 | return nil, err 33 | } 34 | fallbackChain := []bundleConfigPreparer{ 35 | prepareOCIBundleConfig(CNABConfigMediaType), 36 | prepareOCIBundleConfig(ocischemav1.MediaTypeImageConfig), 37 | prepareNonOCIBundleConfig, 38 | } 39 | var first, current *PreparedBundleConfig 40 | for _, preparer := range fallbackChain { 41 | cfg, err := preparer(blob) 42 | if err != nil { 43 | return nil, err 44 | } 45 | if current == nil { 46 | first = cfg 47 | } else { 48 | current.Fallback = cfg 49 | } 50 | current = cfg 51 | } 52 | return first, nil 53 | } 54 | 55 | func descriptorOf(payload []byte, mediaType string) ocischemav1.Descriptor { 56 | return ocischemav1.Descriptor{ 57 | MediaType: mediaType, 58 | Digest: digest.FromBytes(payload), 59 | Size: int64(len(payload)), 60 | } 61 | } 62 | 63 | type bundleConfigPreparer func(blob []byte) (*PreparedBundleConfig, error) 64 | 65 | func prepareOCIBundleConfig(mediaType string) bundleConfigPreparer { 66 | return func(blob []byte) (*PreparedBundleConfig, error) { 67 | manifest := ocischemav1.Manifest{ 68 | Versioned: ocischema.Versioned{ 69 | SchemaVersion: OCIIndexSchemaVersion, 70 | }, 71 | Config: descriptorOf(blob, mediaType), 72 | } 73 | manifestBytes, err := json.Marshal(&manifest) 74 | if err != nil { 75 | return nil, err 76 | } 77 | return &PreparedBundleConfig{ 78 | ConfigBlob: blob, 79 | ConfigBlobDescriptor: manifest.Config, 80 | Manifest: manifestBytes, 81 | ManifestDescriptor: descriptorOf(manifestBytes, ocischemav1.MediaTypeImageManifest), 82 | }, nil 83 | } 84 | } 85 | 86 | func nonOCIDescriptorOf(blob []byte) distribution.Descriptor { 87 | return distribution.Descriptor{ 88 | MediaType: schema2.MediaTypeImageConfig, 89 | Size: int64(len(blob)), 90 | Digest: digest.FromBytes(blob), 91 | } 92 | } 93 | 94 | func prepareNonOCIBundleConfig(blob []byte) (*PreparedBundleConfig, error) { 95 | desc := nonOCIDescriptorOf(blob) 96 | man, err := schema2.FromStruct(schema2.Manifest{ 97 | Versioned: schema2.SchemaVersion, 98 | // Add a descriptor for the configuration because some registries 99 | // require the layers property to be defined and non-empty 100 | Layers: []distribution.Descriptor{ 101 | desc, 102 | }, 103 | Config: desc, 104 | }) 105 | if err != nil { 106 | return nil, err 107 | } 108 | manBytes, err := man.MarshalJSON() 109 | if err != nil { 110 | return nil, err 111 | } 112 | return &PreparedBundleConfig{ 113 | ConfigBlob: blob, 114 | ConfigBlobDescriptor: descriptorOf(blob, schema2.MediaTypeImageConfig), 115 | Manifest: manBytes, 116 | ManifestDescriptor: descriptorOf(manBytes, schema2.MediaTypeManifest), 117 | }, nil 118 | } 119 | -------------------------------------------------------------------------------- /remotes/fixuphelpers.go: -------------------------------------------------------------------------------- 1 | package remotes 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/cnabio/cnab-go/bundle" 10 | "github.com/containerd/containerd/images" 11 | "github.com/containerd/containerd/remotes" 12 | "github.com/distribution/reference" 13 | "github.com/opencontainers/go-digest" 14 | ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1" 15 | ) 16 | 17 | type sourceFetcherAdder interface { 18 | remotes.Fetcher 19 | Add(data []byte) digest.Digest 20 | } 21 | 22 | type sourceFetcherWithLocalData struct { 23 | inner remotes.Fetcher 24 | localData map[digest.Digest][]byte 25 | } 26 | 27 | func newSourceFetcherWithLocalData(inner remotes.Fetcher) *sourceFetcherWithLocalData { 28 | return &sourceFetcherWithLocalData{ 29 | inner: inner, 30 | localData: make(map[digest.Digest][]byte), 31 | } 32 | } 33 | 34 | func (s *sourceFetcherWithLocalData) Add(data []byte) digest.Digest { 35 | d := digest.FromBytes(data) 36 | s.localData[d] = data 37 | return d 38 | } 39 | 40 | func (s *sourceFetcherWithLocalData) Fetch(ctx context.Context, desc ocischemav1.Descriptor) (io.ReadCloser, error) { 41 | if v, ok := s.localData[desc.Digest]; ok { 42 | return io.NopCloser(bytes.NewReader(v)), nil 43 | } 44 | return s.inner.Fetch(ctx, desc) 45 | } 46 | 47 | type imageFixupInfo struct { 48 | targetRepo reference.Named 49 | sourceRef reference.Named 50 | resolvedDescriptor ocischemav1.Descriptor 51 | } 52 | 53 | func makeEventNotifier(events chan<- FixupEvent, baseImage string, targetRef reference.Named) (eventNotifier, *progress) { 54 | progress := &progress{} 55 | return func(eventType FixupEventType, message string, err error) { 56 | events <- FixupEvent{ 57 | DestinationRef: targetRef, 58 | SourceImage: baseImage, 59 | EventType: eventType, 60 | Message: message, 61 | Error: err, 62 | Progress: progress.snapshot(), 63 | } 64 | }, progress 65 | } 66 | 67 | func makeSourceFetcher(ctx context.Context, resolver remotes.Resolver, sourceRef string) (*sourceFetcherWithLocalData, error) { 68 | sourceRepoOnly, err := reference.ParseNormalizedNamed(sourceRef) 69 | if err != nil { 70 | return nil, err 71 | } 72 | f, err := resolver.Fetcher(ctx, sourceRepoOnly.Name()) 73 | if err != nil { 74 | return nil, err 75 | } 76 | return newSourceFetcherWithLocalData(f), nil 77 | } 78 | 79 | func makeManifestWalker(ctx context.Context, sourceFetcher remotes.Fetcher, 80 | notifyEvent eventNotifier, cfg fixupConfig, fixupInfo imageFixupInfo, progress *progress) (func(), error) { 81 | copier, err := newDescriptorCopier(ctx, cfg.resolver, sourceFetcher, fixupInfo.targetRepo.String(), notifyEvent, fixupInfo.sourceRef) 82 | if err != nil { 83 | return nil, err 84 | } 85 | descriptorContentHandler := &descriptorContentHandler{ 86 | descriptorCopier: copier, 87 | targetRepo: fixupInfo.targetRepo.String(), 88 | } 89 | ctx, cancel := context.WithCancel(ctx) 90 | cleaner := func() { 91 | cancel() 92 | } 93 | walker := newManifestWalker(notifyEvent, progress, descriptorContentHandler, cfg.maxConcurrentJobs) 94 | return cleaner, walker.walk(ctx, fixupInfo.resolvedDescriptor) 95 | } 96 | 97 | func notifyError(notifyEvent eventNotifier, err error) error { 98 | notifyEvent(FixupEventTypeCopyImageEnd, "", err) 99 | return err 100 | } 101 | 102 | func checkBaseImage(baseImage *bundle.BaseImage) error { 103 | switch baseImage.ImageType { 104 | case "docker": 105 | case "oci": 106 | case "": 107 | baseImage.ImageType = "oci" 108 | default: 109 | return fmt.Errorf("image type %q is not supported", baseImage.ImageType) 110 | } 111 | 112 | switch baseImage.MediaType { 113 | case ocischemav1.MediaTypeImageIndex: 114 | case ocischemav1.MediaTypeImageManifest: 115 | case images.MediaTypeDockerSchema2Manifest: 116 | case images.MediaTypeDockerSchema2ManifestList: 117 | case "": 118 | default: 119 | return fmt.Errorf("image media type %q is not supported", baseImage.MediaType) 120 | } 121 | 122 | return nil 123 | } 124 | -------------------------------------------------------------------------------- /remotes/resolver.go: -------------------------------------------------------------------------------- 1 | package remotes 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "fmt" 7 | "net/http" 8 | "strings" 9 | 10 | "github.com/containerd/containerd/remotes" 11 | "github.com/containerd/containerd/remotes/docker" 12 | "github.com/distribution/reference" 13 | "github.com/docker/cli/cli/config/configfile" 14 | "github.com/docker/docker/registry" 15 | ocispec "github.com/opencontainers/image-spec/specs-go/v1" 16 | ) 17 | 18 | // multiRegistryResolver is an OCI registry resolver that accepts a list of 19 | // insecure registries. It will skip TLS validation for registries that are secured with TLS 20 | // use plain http for unsecured registries and any registry that is exposed on a loopback ip address. 21 | type multiRegistryResolver struct { 22 | resolver remotes.Resolver 23 | plainHTTPRegistries map[string]struct{} 24 | skipTLSRegistries map[string]struct{} 25 | authorizer docker.Authorizer 26 | skipTLSClient *http.Client 27 | skipTLSAuthorizer docker.Authorizer 28 | } 29 | 30 | func (r *multiRegistryResolver) Resolve(ctx context.Context, ref string) (name string, desc ocispec.Descriptor, err error) { 31 | name, desc, err = r.resolver.Resolve(ctx, ref) 32 | 33 | // Add some extra context to the poor error message 34 | // which is returned when you forget to specify that the registry 35 | // uses an insecure TLS certificate 36 | // Example: pulling from host localhost:55027 failed with status code [manifests sha256:464c8a63f292a07fb0ea2bf2cf636dafe38bf74d0536879fb9ec4611f2168067]: 400 Bad Request 37 | if err != nil && strings.Contains(err.Error(), "400 Bad Request") { 38 | ref, otherErr := reference.ParseNormalizedNamed(ref) 39 | if otherErr != nil { 40 | return 41 | } 42 | repoInfo, otherErr := registry.ParseRepositoryInfo(ref) 43 | if otherErr != nil { 44 | return 45 | } 46 | 47 | // Check if the registry is not flagged with skipTLS, which is one common explanation for this error 48 | if _, skipTLS := r.skipTLSRegistries[repoInfo.Index.Name]; !skipTLS { 49 | err = fmt.Errorf("possible attempt to access an insecure registry without skipping TLS verification detected: %w", err) 50 | } 51 | } 52 | 53 | return 54 | } 55 | 56 | func (r *multiRegistryResolver) Fetcher(ctx context.Context, ref string) (remotes.Fetcher, error) { 57 | return r.resolver.Fetcher(ctx, ref) 58 | } 59 | 60 | func (r *multiRegistryResolver) Pusher(ctx context.Context, ref string) (remotes.Pusher, error) { 61 | return r.resolver.Pusher(ctx, ref) 62 | } 63 | 64 | // CreateResolver creates a docker registry resolver, using the local docker CLI credentials 65 | func CreateResolver(cfg *configfile.ConfigFile, insecureRegistries ...string) remotes.Resolver { 66 | authCreds := docker.WithAuthCreds(func(hostName string) (string, string, error) { 67 | if hostName == registry.DefaultV2Registry.Host { 68 | hostName = registry.IndexServer 69 | } 70 | a, err := cfg.GetAuthConfig(hostName) 71 | if err != nil { 72 | return "", "", err 73 | } 74 | if a.IdentityToken != "" { 75 | return "", a.IdentityToken, nil 76 | } 77 | return a.Username, a.Password, nil 78 | }) 79 | 80 | clientSkipTLS := &http.Client{ 81 | Transport: &http.Transport{ 82 | TLSClientConfig: &tls.Config{ 83 | InsecureSkipVerify: true, 84 | }, 85 | }, 86 | } 87 | 88 | result := &multiRegistryResolver{ 89 | authorizer: docker.NewDockerAuthorizer(authCreds), 90 | skipTLSClient: clientSkipTLS, 91 | skipTLSAuthorizer: docker.NewDockerAuthorizer(authCreds, docker.WithAuthClient(clientSkipTLS)), 92 | plainHTTPRegistries: make(map[string]struct{}), 93 | skipTLSRegistries: make(map[string]struct{}), 94 | } 95 | 96 | // Determine ahead of time how each registry is insecure 97 | // 1. It uses TLS but has a bad cert 98 | // 2. It doesn't use TLS 99 | for _, r := range insecureRegistries { 100 | pingURL := fmt.Sprintf("https://%s/v2/", r) 101 | resp, err := clientSkipTLS.Get(pingURL) 102 | if err == nil { 103 | resp.Body.Close() 104 | result.skipTLSRegistries[r] = struct{}{} 105 | } else { 106 | result.plainHTTPRegistries[r] = struct{}{} 107 | } 108 | } 109 | 110 | result.resolver = docker.NewResolver(docker.ResolverOptions{ 111 | Hosts: result.configureHosts(), 112 | }) 113 | 114 | return result 115 | } 116 | 117 | func (r *multiRegistryResolver) configureHosts() docker.RegistryHosts { 118 | return func(host string) ([]docker.RegistryHost, error) { 119 | config := docker.RegistryHost{ 120 | Client: http.DefaultClient, 121 | Authorizer: r.authorizer, 122 | Host: host, 123 | Scheme: "https", 124 | Path: "/v2", 125 | Capabilities: docker.HostCapabilityPull | docker.HostCapabilityResolve | docker.HostCapabilityPush, 126 | } 127 | 128 | if _, skipTLS := r.skipTLSRegistries[host]; skipTLS { 129 | config.Client = r.skipTLSClient 130 | config.Authorizer = r.skipTLSAuthorizer 131 | } else if _, plainHTTP := r.plainHTTPRegistries[host]; plainHTTP { 132 | config.Scheme = "http" 133 | } else { 134 | // Default to plain http for localhost 135 | match, err := docker.MatchLocalhost(host) 136 | if err != nil { 137 | return nil, err 138 | } 139 | if match { 140 | config.Scheme = "http" 141 | } 142 | } 143 | 144 | // If this is not set, then we aren't prompted to authenticate to Docker Hub, 145 | // which causes the returned content type to be text/html instead of the 146 | // specialized content types for images and manifests 147 | if host == "docker.io" { 148 | config.Host = "registry-1.docker.io" 149 | } 150 | 151 | return []docker.RegistryHost{config}, nil 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /remotes/fixupoptions.go: -------------------------------------------------------------------------------- 1 | package remotes 2 | 3 | import ( 4 | "fmt" 5 | "io" 6 | 7 | "github.com/cnabio/cnab-go/bundle" 8 | "github.com/cnabio/cnab-to-oci/internal" 9 | "github.com/cnabio/cnab-to-oci/relocation" 10 | "github.com/containerd/containerd/remotes" 11 | "github.com/containerd/platforms" 12 | "github.com/distribution/reference" 13 | ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1" 14 | ) 15 | 16 | const ( 17 | defaultMaxConcurrentJobs = 4 18 | defaultJobsBufferLength = 50 19 | ) 20 | 21 | func noopEventCallback(FixupEvent) {} 22 | 23 | // fixupConfig defines the input required for a Fixup operation 24 | type fixupConfig struct { 25 | bundle *bundle.Bundle 26 | relocationMap relocation.ImageRelocationMap 27 | targetRef reference.Named 28 | eventCallback func(FixupEvent) 29 | maxConcurrentJobs int 30 | jobsBufferLength int 31 | resolver remotes.Resolver 32 | invocationImagePlatformFilter platforms.Matcher 33 | componentImagePlatformFilter platforms.Matcher 34 | autoBundleUpdate bool 35 | pushImages bool 36 | imageClient internal.ImageClient 37 | pushOut io.Writer 38 | } 39 | 40 | // FixupOption is a helper for configuring a FixupBundle 41 | type FixupOption func(*fixupConfig) error 42 | 43 | func newFixupConfig(b *bundle.Bundle, ref reference.Named, resolver remotes.Resolver, options ...FixupOption) (fixupConfig, error) { 44 | cfg := fixupConfig{ 45 | bundle: b, 46 | relocationMap: relocation.ImageRelocationMap{}, 47 | targetRef: ref, 48 | resolver: resolver, 49 | eventCallback: noopEventCallback, 50 | jobsBufferLength: defaultJobsBufferLength, 51 | maxConcurrentJobs: defaultMaxConcurrentJobs, 52 | } 53 | for _, opt := range options { 54 | if err := opt(&cfg); err != nil { 55 | return fixupConfig{}, err 56 | } 57 | } 58 | return cfg, nil 59 | } 60 | 61 | // WithInvocationImagePlatforms use filters platforms for an invocation image 62 | func WithInvocationImagePlatforms(supportedPlatforms []string) FixupOption { 63 | return func(cfg *fixupConfig) error { 64 | if len(supportedPlatforms) == 0 { 65 | return nil 66 | } 67 | plats, err := toPlatforms(supportedPlatforms) 68 | if err != nil { 69 | return err 70 | } 71 | cfg.invocationImagePlatformFilter = platforms.Any(plats...) 72 | return nil 73 | } 74 | } 75 | 76 | // WithComponentImagePlatforms use filters platforms for an invocation image 77 | func WithComponentImagePlatforms(supportedPlatforms []string) FixupOption { 78 | return func(cfg *fixupConfig) error { 79 | if len(supportedPlatforms) == 0 { 80 | return nil 81 | } 82 | plats, err := toPlatforms(supportedPlatforms) 83 | if err != nil { 84 | return err 85 | } 86 | cfg.componentImagePlatformFilter = platforms.Any(plats...) 87 | return nil 88 | } 89 | } 90 | 91 | func toPlatforms(supportedPlatforms []string) ([]ocischemav1.Platform, error) { 92 | result := make([]ocischemav1.Platform, len(supportedPlatforms)) 93 | for ix, p := range supportedPlatforms { 94 | plat, err := platforms.Parse(p) 95 | if err != nil { 96 | return nil, err 97 | } 98 | result[ix] = plat 99 | } 100 | return result, nil 101 | } 102 | 103 | // WithEventCallback specifies a callback to execute for each Fixup event 104 | func WithEventCallback(callback func(FixupEvent)) FixupOption { 105 | return func(cfg *fixupConfig) error { 106 | cfg.eventCallback = callback 107 | return nil 108 | } 109 | } 110 | 111 | // WithParallelism provides a way to change the max concurrent jobs and the max number of jobs queued up 112 | func WithParallelism(maxConcurrentJobs int, jobsBufferLength int) FixupOption { 113 | return func(cfg *fixupConfig) error { 114 | cfg.maxConcurrentJobs = maxConcurrentJobs 115 | cfg.jobsBufferLength = jobsBufferLength 116 | return nil 117 | } 118 | } 119 | 120 | // WithAutoBundleUpdate updates the bundle with content digests and size provided by the registry 121 | func WithAutoBundleUpdate() FixupOption { 122 | return func(cfg *fixupConfig) error { 123 | cfg.autoBundleUpdate = true 124 | return nil 125 | } 126 | } 127 | 128 | // WithPushImages authorizes the fixup command to push missing images. 129 | // By default the fixup will look at images defined in the bundle. 130 | // Existing images in the target registry or accessible from an other registry will be copied or mounted under the 131 | // target tag. 132 | // But local only images (for example after a local build of components of the bundle) must be pushed. 133 | // This option will allow to push images that are only available in the docker daemon image store to the defined target. 134 | func WithPushImages(imageClient internal.ImageClient, out io.Writer) FixupOption { 135 | return func(cfg *fixupConfig) error { 136 | cfg.pushImages = true 137 | if imageClient == nil { 138 | return fmt.Errorf("could not configure fixup, 'imageClient' cannot be nil to push images") 139 | } 140 | cfg.imageClient = imageClient 141 | if out == nil { 142 | cfg.pushOut = io.Discard 143 | } else { 144 | cfg.pushOut = out 145 | } 146 | return nil 147 | } 148 | } 149 | 150 | // WithRelocationMap stores a previously generated relocation map. This map will be used to copy or mount images 151 | // based on local images but already pushed on a registry. 152 | // This way if a bundle is pulled on a machine that doesn't contain the images, when the bundle is pushed and images 153 | // are not found the fixup will try to resolve the corresponding images from the relocation map. 154 | func WithRelocationMap(relocationMap relocation.ImageRelocationMap) FixupOption { 155 | return func(cfg *fixupConfig) error { 156 | cfg.relocationMap = relocationMap 157 | return nil 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /e2e/e2e_test.go: -------------------------------------------------------------------------------- 1 | package e2e 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "os" 7 | "path/filepath" 8 | "regexp" 9 | "strings" 10 | "testing" 11 | "text/template" 12 | 13 | "gotest.tools/v3/assert" 14 | "gotest.tools/v3/fs" 15 | "gotest.tools/v3/golden" 16 | "gotest.tools/v3/icmd" 17 | ) 18 | 19 | func TestPushAndPullCNAB(t *testing.T) { 20 | dir := fs.NewDir(t, t.Name()) 21 | defer dir.Remove() 22 | r := startRegistry(t) 23 | defer r.Stop(t) 24 | registry := r.GetAddress(t) 25 | 26 | invocationImageName := registry + "/e2e/hello-world:0.1.0-invoc" 27 | serviceImageName := registry + "/e2e/http-echo" 28 | whalesayImageName := registry + "/e2e/whalesay" 29 | appImageName := registry + "/myuser" 30 | 31 | // Build invocation image 32 | cmd := icmd.Command("docker", "build", "-f", filepath.Join("testdata", "hello-world", "invocation-image", "Dockerfile"), 33 | "-t", invocationImageName, filepath.Join("testdata", "hello-world", "invocation-image")) 34 | cmd.Env = append(os.Environ(), "DOCKER_BUILDKIT=1") 35 | runCmd(t, cmd) 36 | 37 | // Fetch service images 38 | runCmd(t, icmd.Command("docker", "pull", "hashicorp/http-echo")) 39 | runCmd(t, icmd.Command("docker", "pull", "docker/compose")) 40 | runCmd(t, icmd.Command("docker", "tag", "hashicorp/http-echo", serviceImageName)) 41 | runCmd(t, icmd.Command("docker", "tag", "docker/compose", whalesayImageName)) 42 | 43 | // Tidy up my room 44 | defer func() { 45 | runCmd(t, icmd.Command("docker", "image", "rm", "-f", invocationImageName, "hashicorp/http-echo", serviceImageName, "docker/compose", whalesayImageName)) 46 | }() 47 | 48 | // Push the images to the registry 49 | output := runCmd(t, icmd.Command("docker", "push", invocationImageName)) 50 | invocDigest := getDigest(t, output) 51 | 52 | runCmd(t, icmd.Command("docker", "push", serviceImageName)) 53 | runCmd(t, icmd.Command("docker", "push", whalesayImageName)) 54 | 55 | // Templatize the bundle 56 | applyTemplate(t, serviceImageName, whalesayImageName, invocationImageName, invocDigest, filepath.Join("testdata", "hello-world", "bundle.json.template"), dir.Join("bundle.json")) 57 | 58 | // Save the fixed bundle 59 | runCmd(t, icmd.Command("cnab-to-oci", "fixup", dir.Join("bundle.json"), 60 | "--target", appImageName, 61 | "--insecure-registries", registry, 62 | "--bundle", dir.Join("fixed-bundle.json"), 63 | "--relocation-map", dir.Join("relocation.json"), 64 | "--auto-update-bundle")) 65 | 66 | // Check the fixed bundle 67 | applyTemplate(t, serviceImageName, whalesayImageName, invocationImageName, invocDigest, filepath.Join("testdata", "bundle.json.golden.template"), filepath.Join("testdata", "bundle.json.golden")) 68 | buf, err := os.ReadFile(dir.Join("fixed-bundle.json")) 69 | assert.NilError(t, err) 70 | golden.Assert(t, string(buf), "bundle.json.golden") 71 | 72 | // Check the relocation map 73 | checkRelocationMap(t, serviceImageName, invocationImageName, appImageName, dir.Join("relocation.json")) 74 | 75 | // Re fix-up, checking it works twice 76 | runCmd(t, icmd.Command("cnab-to-oci", "fixup", dir.Join("bundle.json"), 77 | "--target", appImageName, 78 | "--insecure-registries", registry, 79 | "--bundle", dir.Join("fixed-bundle.json"), 80 | "--auto-update-bundle")) 81 | 82 | // Push the CNAB to the registry and get the digest 83 | out := runCmd(t, icmd.Command("cnab-to-oci", "push", dir.Join("bundle.json"), 84 | "--target", appImageName, 85 | "--insecure-registries", registry, 86 | "--auto-update-bundle")) 87 | re := regexp.MustCompile(`"(.*)"`) 88 | digest := re.FindAllStringSubmatch(out, -1)[0][1] 89 | 90 | // Pull the CNAB from the registry 91 | runCmd(t, icmd.Command("cnab-to-oci", "pull", fmt.Sprintf("%s@%s", appImageName, digest), 92 | "--bundle", dir.Join("pulled-bundle.json"), 93 | "--relocation-map", dir.Join("pulled-relocation.json"), 94 | "--insecure-registries", registry)) 95 | pulledBundle, err := os.ReadFile(dir.Join("pulled-bundle.json")) 96 | assert.NilError(t, err) 97 | pulledRelocation, err := os.ReadFile(dir.Join("pulled-relocation.json")) 98 | assert.NilError(t, err) 99 | 100 | // Check the fixed bundle.json is equal to the pulled bundle.json 101 | golden.Assert(t, string(pulledBundle), dir.Join("fixed-bundle.json")) 102 | golden.Assert(t, string(pulledRelocation), dir.Join("relocation.json")) 103 | } 104 | 105 | func runCmd(t *testing.T, cmd icmd.Cmd) string { 106 | fmt.Println("#", strings.Join(cmd.Command, " ")) 107 | result := icmd.RunCmd(cmd) 108 | fmt.Println(result.Combined()) 109 | result.Assert(t, icmd.Success) 110 | return result.Stdout() 111 | } 112 | 113 | func applyTemplate(t *testing.T, serviceImageName, whalesayImageName, invocationImageName, invocationDigest, templateFile, resultFile string) { 114 | tmpl, err := template.ParseFiles(templateFile) 115 | assert.NilError(t, err) 116 | data := struct { 117 | InvocationImage string 118 | InvocationDigest string 119 | ServiceImage string 120 | WhalesayImage string 121 | }{ 122 | invocationImageName, 123 | invocationDigest, 124 | serviceImageName, 125 | whalesayImageName, 126 | } 127 | f, err := os.Create(resultFile) 128 | assert.NilError(t, err) 129 | defer f.Close() 130 | err = tmpl.Execute(f, data) 131 | assert.NilError(t, err) 132 | } 133 | 134 | func checkRelocationMap(t *testing.T, serviceImageName, invocationImageName, appImageName, relocationMapFile string) { 135 | data, err := os.ReadFile(relocationMapFile) 136 | assert.NilError(t, err) 137 | relocationMap := map[string]string{} 138 | err = json.Unmarshal(data, &relocationMap) 139 | assert.NilError(t, err) 140 | 141 | // Check the relocated images are in the app repository 142 | assert.Assert(t, strings.HasPrefix(relocationMap[serviceImageName], appImageName)) 143 | assert.Assert(t, strings.HasPrefix(relocationMap[invocationImageName], appImageName)) 144 | } 145 | 146 | func getDigest(t *testing.T, output string) string { 147 | re := regexp.MustCompile(`digest: (.+) size:`) 148 | result := re.FindStringSubmatch(output) 149 | assert.Equal(t, len(result), 2) 150 | digest := result[1] 151 | assert.Assert(t, digest != "") 152 | return digest 153 | } 154 | -------------------------------------------------------------------------------- /remotes/push_test.go: -------------------------------------------------------------------------------- 1 | package remotes 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "strings" 9 | "testing" 10 | 11 | "github.com/cnabio/cnab-go/bundle" 12 | "github.com/cnabio/cnab-to-oci/converter" 13 | "github.com/cnabio/cnab-to-oci/tests" 14 | "github.com/distribution/reference" 15 | ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1" 16 | "gotest.tools/v3/assert" 17 | ) 18 | 19 | const ( 20 | expectedBundleManifest = `{ 21 | "schemaVersion": 2, 22 | "manifests": [ 23 | { 24 | "mediaType":"application/vnd.oci.image.manifest.v1+json", 25 | "digest":"sha256:122a5dc186ec285488de9d25e99c96a11d3f7ff71c6e05a06c98e8627472a920", 26 | "size":189, 27 | "annotations":{ 28 | "io.cnab.manifest.type":"config" 29 | } 30 | }, 31 | { 32 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 33 | "digest": "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0343", 34 | "size": 506, 35 | "annotations": { 36 | "io.cnab.manifest.type": "invocation" 37 | } 38 | }, 39 | { 40 | "mediaType": "application/vnd.oci.image.manifest.v1+json", 41 | "digest": "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0342", 42 | "size": 507, 43 | "annotations": { 44 | "io.cnab.component.name": "another-image", 45 | "io.cnab.manifest.type": "component" 46 | } 47 | }, 48 | { 49 | "mediaType": "application/vnd.oci.image.manifest.v1+json", 50 | "digest": "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341", 51 | "size": 507, 52 | "annotations": { 53 | "io.cnab.component.name": "image-1", 54 | "io.cnab.manifest.type": "component" 55 | } 56 | } 57 | ], 58 | "annotations": { 59 | "io.cnab.keywords": "[\"keyword1\",\"keyword2\"]", 60 | "io.cnab.runtime_version": "v1.0.0", 61 | "org.opencontainers.artifactType": "application/vnd.cnab.manifest.v1", 62 | "org.opencontainers.image.authors": "[{\"name\":\"docker\",\"email\":\"docker@docker.com\",\"url\":\"docker.com\"}]", 63 | "org.opencontainers.image.description": "description", 64 | "org.opencontainers.image.title": "my-app", 65 | "org.opencontainers.image.version": "0.1.0" 66 | } 67 | }` 68 | expectedConfigManifest = `{ 69 | "schemaVersion": 2, 70 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 71 | "config": { 72 | "mediaType": "application/vnd.docker.container.image.v1+json", 73 | "size": 1596, 74 | "digest": "sha256:dbe3480b9cb300f389e8d02e4a682f9107772468feb6845f912dc8deed6d76fd" 75 | }, 76 | "layers": [ 77 | { 78 | "mediaType": "application/vnd.docker.container.image.v1+json", 79 | "size": 1596, 80 | "digest": "sha256:dbe3480b9cb300f389e8d02e4a682f9107772468feb6845f912dc8deed6d76fd" 81 | } 82 | ] 83 | }` 84 | ) 85 | 86 | func TestPush(t *testing.T) { 87 | pusher := &mockPusher{} 88 | resolver := &mockResolver{pusher: pusher} 89 | b := tests.MakeTestBundle() 90 | expectedBundleConfig, err := b.Marshal() 91 | assert.NilError(t, err, "marshaling to canonical json failed") 92 | ref, err := reference.ParseNamed("my.registry/namespace/my-app:my-tag") 93 | assert.NilError(t, err, "parsing the OCI reference failed") 94 | 95 | // push the bundle 96 | descriptor, err := Push(context.Background(), b, tests.MakeRelocationMap(), ref, resolver, true) 97 | assert.NilError(t, err, "push failed") 98 | assert.Equal(t, tests.BundleDigest, descriptor.Digest) 99 | assert.Equal(t, len(resolver.pushedReferences), 3) 100 | assert.Equal(t, len(pusher.pushedDescriptors), 3) 101 | assert.Equal(t, len(pusher.buffers), 3) 102 | 103 | // check pushed config 104 | assert.Equal(t, "my.registry/namespace/my-app", resolver.pushedReferences[0]) 105 | assert.Equal(t, converter.CNABConfigMediaType, pusher.pushedDescriptors[0].MediaType) 106 | assert.Equal(t, oneLiner(string(expectedBundleConfig)), pusher.buffers[0].String()) 107 | 108 | // check pushed config manifest 109 | assert.Equal(t, "my.registry/namespace/my-app", resolver.pushedReferences[1]) 110 | assert.Equal(t, ocischemav1.MediaTypeImageManifest, pusher.pushedDescriptors[1].MediaType) 111 | 112 | // check pushed bundle manifest index 113 | assert.Equal(t, "my.registry/namespace/my-app:my-tag", resolver.pushedReferences[2]) 114 | assert.Equal(t, ocischemav1.MediaTypeImageIndex, pusher.pushedDescriptors[2].MediaType) 115 | assert.Equal(t, oneLiner(expectedBundleManifest), pusher.buffers[2].String()) 116 | } 117 | 118 | func TestFallbackConfigManifest(t *testing.T) { 119 | // Make the pusher return an error for the first two calls 120 | // so that the fallbacks kick in and we get the non-oci 121 | // config manifest. 122 | pusher := newMockPusher([]error{ 123 | errors.New("1"), 124 | errors.New("2"), 125 | nil, 126 | nil, 127 | nil, 128 | nil, 129 | nil}) 130 | resolver := &mockResolver{pusher: pusher} 131 | b := tests.MakeTestBundle() 132 | ref, err := reference.ParseNamed("my.registry/namespace/my-app:my-tag") 133 | assert.NilError(t, err) 134 | 135 | // push the bundle 136 | relocationMap := tests.MakeRelocationMap() 137 | _, err = Push(context.Background(), b, relocationMap, ref, resolver, true) 138 | assert.NilError(t, err) 139 | assert.Equal(t, expectedConfigManifest, pusher.buffers[3].String()) 140 | } 141 | 142 | func oneLiner(s string) string { 143 | return strings.Replace(strings.Replace(s, " ", "", -1), "\n", "", -1) 144 | } 145 | 146 | func ExamplePush() { 147 | resolver := createExampleResolver() 148 | b := createExampleBundle() 149 | ref, err := reference.ParseNamed("my.registry/namespace/my-app:my-tag") 150 | if err != nil { 151 | panic(err) 152 | } 153 | 154 | // Push the bundle here 155 | descriptor, err := Push(context.Background(), b, tests.MakeRelocationMap(), ref, resolver, true) 156 | if err != nil { 157 | panic(err) 158 | } 159 | 160 | bytes, err := json.MarshalIndent(descriptor, "", " ") 161 | if err != nil { 162 | panic(err) 163 | } 164 | 165 | fmt.Printf("%s", string(bytes)) 166 | 167 | // Output: 168 | // { 169 | // "mediaType": "application/vnd.oci.image.index.v1+json", 170 | // "digest": "sha256:00043ca96c3c9470fc0f647c67613ddf500941556d1ecc14d75bc9b2834f66c3", 171 | // "size": 1360 172 | // } 173 | } 174 | 175 | func createExampleBundle() *bundle.Bundle { 176 | return tests.MakeTestBundle() 177 | } 178 | -------------------------------------------------------------------------------- /tests/helpers.go: -------------------------------------------------------------------------------- 1 | package tests 2 | 3 | import ( 4 | "github.com/cnabio/cnab-go/bundle" 5 | "github.com/cnabio/cnab-go/bundle/definition" 6 | "github.com/cnabio/cnab-to-oci/relocation" 7 | "github.com/distribution/distribution/manifest/schema2" 8 | "github.com/opencontainers/go-digest" 9 | ocischema "github.com/opencontainers/image-spec/specs-go" 10 | ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1" 11 | ) 12 | 13 | const BundleDigest digest.Digest = "sha256:00043ca96c3c9470fc0f647c67613ddf500941556d1ecc14d75bc9b2834f66c3" 14 | 15 | // MakeTestBundle creates a simple bundle for tests 16 | func MakeTestBundle() *bundle.Bundle { 17 | return &bundle.Bundle{ 18 | SchemaVersion: "v1.0.0", 19 | Actions: map[string]bundle.Action{ 20 | "action-1": { 21 | Modifies: true, 22 | }, 23 | }, 24 | Credentials: map[string]bundle.Credential{ 25 | "cred-1": { 26 | Location: bundle.Location{ 27 | EnvironmentVariable: "env-var", 28 | Path: "/some/path", 29 | }, 30 | }, 31 | }, 32 | Description: "description", 33 | Images: map[string]bundle.Image{ 34 | "image-1": { 35 | BaseImage: bundle.BaseImage{ 36 | Image: "my.registry/namespace/image-1", 37 | ImageType: "oci", 38 | MediaType: "application/vnd.oci.image.manifest.v1+json", 39 | Size: 507, 40 | Digest: "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341", 41 | }, 42 | }, 43 | "another-image": { 44 | BaseImage: bundle.BaseImage{ 45 | Image: "my.registry/namespace/another-image", 46 | ImageType: "oci", 47 | MediaType: "application/vnd.oci.image.manifest.v1+json", 48 | Size: 507, 49 | Digest: "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0342", 50 | }, 51 | }, 52 | }, 53 | InvocationImages: []bundle.InvocationImage{ 54 | { 55 | BaseImage: bundle.BaseImage{ 56 | Image: "my.registry/namespace/my-app-invoc", 57 | ImageType: "docker", 58 | MediaType: "application/vnd.docker.distribution.manifest.v2+json", 59 | Size: 506, 60 | Digest: "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0343", 61 | }, 62 | }, 63 | }, 64 | Keywords: []string{"keyword1", "keyword2"}, 65 | Maintainers: []bundle.Maintainer{ 66 | { 67 | Email: "docker@docker.com", 68 | Name: "docker", 69 | URL: "docker.com", 70 | }, 71 | }, 72 | Name: "my-app", 73 | Definitions: map[string]*definition.Schema{ 74 | "param1Type": { 75 | Enum: []interface{}{"value1", true, float64(1)}, 76 | Type: []interface{}{"string", "boolean", "number"}, 77 | Default: "hello", 78 | }, 79 | "output1Type": { 80 | Type: "string", 81 | }, 82 | // Include a number as a regression test for marshaling with the proper canonical json library 83 | // We should always be marshaling with a library that supports numeric types 84 | "numberType": { 85 | Type: "number", 86 | Default: 0.5, 87 | }, 88 | }, 89 | Parameters: map[string]bundle.Parameter{ 90 | "param1": { 91 | Definition: "param1Type", 92 | Destination: &bundle.Location{ 93 | EnvironmentVariable: "env_var", 94 | Path: "/some/path", 95 | }, 96 | }, 97 | "param2": { 98 | Definition: "numberType", 99 | Destination: &bundle.Location{ 100 | EnvironmentVariable: "PARM2", 101 | }, 102 | }, 103 | }, 104 | Outputs: map[string]bundle.Output{ 105 | "output1": { 106 | Definition: "output1Type", 107 | Description: "magic", 108 | ApplyTo: []string{"install"}, 109 | Path: "/cnab/app/outputs/magic", 110 | }, 111 | }, 112 | Custom: map[string]interface{}{ 113 | "my-key": "my-value", 114 | }, 115 | Version: "0.1.0", 116 | } 117 | } 118 | 119 | // MakeTestOCIIndex creates a dummy OCI index for tests 120 | func MakeTestOCIIndex() *ocischemav1.Index { 121 | return &ocischemav1.Index{ 122 | Versioned: ocischema.Versioned{ 123 | SchemaVersion: 2, 124 | }, 125 | Annotations: map[string]string{ 126 | "io.cnab.runtime_version": "v1.0.0", 127 | ocischemav1.AnnotationTitle: "my-app", 128 | ocischemav1.AnnotationVersion: "0.1.0", 129 | ocischemav1.AnnotationDescription: "description", 130 | ocischemav1.AnnotationAuthors: `[{"name":"docker","email":"docker@docker.com","url":"docker.com"}]`, 131 | "io.cnab.keywords": `["keyword1","keyword2"]`, 132 | "org.opencontainers.artifactType": "application/vnd.cnab.manifest.v1", 133 | }, 134 | Manifests: []ocischemav1.Descriptor{ 135 | { 136 | Digest: "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341", 137 | MediaType: schema2.MediaTypeManifest, 138 | Size: 315, 139 | Annotations: map[string]string{ 140 | "io.cnab.manifest.type": "config", 141 | }, 142 | }, 143 | { 144 | Digest: "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0343", 145 | MediaType: "application/vnd.docker.distribution.manifest.v2+json", 146 | Size: 506, 147 | Annotations: map[string]string{ 148 | "io.cnab.manifest.type": "invocation", 149 | }, 150 | }, 151 | { 152 | Digest: "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0342", 153 | MediaType: "application/vnd.oci.image.manifest.v1+json", 154 | Size: 507, 155 | Annotations: map[string]string{ 156 | "io.cnab.manifest.type": "component", 157 | "io.cnab.component.name": "another-image", 158 | }, 159 | }, 160 | { 161 | Digest: "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341", 162 | MediaType: "application/vnd.oci.image.manifest.v1+json", 163 | Size: 507, 164 | Annotations: map[string]string{ 165 | "io.cnab.manifest.type": "component", 166 | "io.cnab.component.name": "image-1", 167 | }, 168 | }, 169 | }, 170 | } 171 | } 172 | 173 | // MakeRelocationMap generates a fake relocation map 174 | func MakeRelocationMap() relocation.ImageRelocationMap { 175 | return relocation.ImageRelocationMap{ 176 | "my.registry/namespace/image-1": "my.registry/namespace/my-app@sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341", 177 | "my.registry/namespace/another-image": "my.registry/namespace/my-app@sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0342", 178 | "my.registry/namespace/my-app-invoc": "my.registry/namespace/my-app@sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0343", 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /remotes/pull.go: -------------------------------------------------------------------------------- 1 | package remotes 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | 10 | "github.com/cnabio/cnab-go/bundle" 11 | "github.com/cnabio/cnab-to-oci/converter" 12 | "github.com/cnabio/cnab-to-oci/relocation" 13 | "github.com/containerd/containerd/errdefs" 14 | "github.com/containerd/containerd/images" 15 | "github.com/containerd/containerd/remotes" 16 | "github.com/containerd/log" 17 | "github.com/distribution/distribution/registry/client/auth" 18 | "github.com/distribution/reference" 19 | "github.com/opencontainers/go-digest" 20 | ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1" 21 | ) 22 | 23 | // Pull pulls a bundle from an OCI Image Index manifest 24 | func Pull(ctx context.Context, ref reference.Named, resolver remotes.Resolver) (*bundle.Bundle, relocation.ImageRelocationMap, digest.Digest, error) { 25 | log.G(ctx).Debugf("Pulling CNAB Bundle %s", ref) 26 | index, descriptor, err := getIndex(ctx, ref, resolver) 27 | if err != nil { 28 | return nil, nil, "", err 29 | } 30 | b, err := getBundle(ctx, ref, resolver, index) 31 | if err != nil { 32 | return nil, nil, "", err 33 | } 34 | relocationMap, err := converter.GenerateRelocationMap(&index, b, ref) 35 | if err != nil { 36 | return nil, nil, "", err 37 | } 38 | 39 | log.G(ctx).Debugf("Digest: %s", descriptor.Digest) 40 | return b, relocationMap, descriptor.Digest, nil 41 | } 42 | 43 | func getIndex(ctx context.Context, ref auth.Scope, resolver remotes.Resolver) (ocischemav1.Index, ocischemav1.Descriptor, error) { 44 | logger := log.G(ctx) 45 | 46 | logger.Debug("Getting OCI Index Descriptor") 47 | resolvedRef, indexDescriptor, err := resolver.Resolve(withMutedContext(ctx), ref.String()) 48 | if err != nil { 49 | if errors.Is(err, errdefs.ErrNotFound) { 50 | return ocischemav1.Index{}, ocischemav1.Descriptor{}, err 51 | } 52 | return ocischemav1.Index{}, ocischemav1.Descriptor{}, fmt.Errorf("failed to resolve bundle manifest %q: %s", ref, err) 53 | } 54 | if indexDescriptor.MediaType != ocischemav1.MediaTypeImageIndex && indexDescriptor.MediaType != images.MediaTypeDockerSchema2ManifestList { 55 | return ocischemav1.Index{}, ocischemav1.Descriptor{}, fmt.Errorf("invalid media type %q for bundle manifest", indexDescriptor.MediaType) 56 | } 57 | logPayload(logger, indexDescriptor) 58 | 59 | logger.Debugf("Fetching OCI Index %s", indexDescriptor.Digest) 60 | indexPayload, err := pullPayload(ctx, resolver, resolvedRef, indexDescriptor) 61 | if err != nil { 62 | return ocischemav1.Index{}, ocischemav1.Descriptor{}, fmt.Errorf("failed to pull bundle manifest %q: %s", ref, err) 63 | } 64 | var index ocischemav1.Index 65 | if err := json.Unmarshal(indexPayload, &index); err != nil { 66 | return ocischemav1.Index{}, ocischemav1.Descriptor{}, fmt.Errorf("failed to pull bundle manifest %q: %s", ref, err) 67 | } 68 | logPayload(logger, index) 69 | 70 | return index, indexDescriptor, nil 71 | } 72 | 73 | func getBundle(ctx context.Context, ref reference.Named, resolver remotes.Resolver, index ocischemav1.Index) (*bundle.Bundle, error) { 74 | repoOnly, err := reference.ParseNormalizedNamed(ref.Name()) 75 | if err != nil { 76 | return nil, fmt.Errorf("invalid bundle manifest reference name %q: %s", ref, err) 77 | } 78 | 79 | // config is wrapped in an image manifest. So we first pull the manifest 80 | // and then the config blob within it 81 | configManifestDescriptor, err := getConfigManifestDescriptor(ctx, ref, index) 82 | if err != nil { 83 | return nil, err 84 | } 85 | 86 | manifest, err := getConfigManifest(ctx, ref, repoOnly, resolver, configManifestDescriptor) 87 | if err != nil { 88 | return nil, err 89 | } 90 | 91 | // Pull now the bundle itself 92 | return getBundleConfig(ctx, ref, repoOnly, resolver, manifest) 93 | } 94 | 95 | func getConfigManifestDescriptor(ctx context.Context, ref reference.Named, index ocischemav1.Index) (ocischemav1.Descriptor, error) { 96 | logger := log.G(ctx) 97 | 98 | logger.Debug("Getting Bundle Config Manifest Descriptor") 99 | configManifestDescriptor, err := converter.GetBundleConfigManifestDescriptor(&index) 100 | if err != nil { 101 | return ocischemav1.Descriptor{}, fmt.Errorf("failed to get bundle config manifest from %q: %s", ref, err) 102 | } 103 | logPayload(logger, configManifestDescriptor) 104 | 105 | return configManifestDescriptor, nil 106 | } 107 | 108 | func getConfigManifest(ctx context.Context, ref reference.Named, repoOnly reference.Named, resolver remotes.Resolver, configManifestDescriptor ocischemav1.Descriptor) (ocischemav1.Manifest, error) { 109 | logger := log.G(ctx) 110 | 111 | logger.Debugf("Getting Bundle Config Manifest %s", configManifestDescriptor.Digest) 112 | configManifestRef, err := reference.WithDigest(repoOnly, configManifestDescriptor.Digest) 113 | if err != nil { 114 | return ocischemav1.Manifest{}, fmt.Errorf("invalid bundle config manifest reference name %q: %s", ref, err) 115 | } 116 | configManifestPayload, err := pullPayload(ctx, resolver, configManifestRef.String(), configManifestDescriptor) 117 | if err != nil { 118 | return ocischemav1.Manifest{}, fmt.Errorf("failed to pull bundle config manifest %q: %s", ref, err) 119 | } 120 | var manifest ocischemav1.Manifest 121 | if err := json.Unmarshal(configManifestPayload, &manifest); err != nil { 122 | return ocischemav1.Manifest{}, err 123 | } 124 | logPayload(logger, manifest) 125 | 126 | return manifest, err 127 | } 128 | 129 | func getBundleConfig(ctx context.Context, ref reference.Named, repoOnly reference.Named, resolver remotes.Resolver, manifest ocischemav1.Manifest) (*bundle.Bundle, error) { 130 | logger := log.G(ctx) 131 | 132 | logger.Debugf("Fetching Bundle %s", manifest.Config.Digest) 133 | configRef, err := reference.WithDigest(repoOnly, manifest.Config.Digest) 134 | if err != nil { 135 | return nil, fmt.Errorf("invalid bundle reference name %q: %s", ref, err) 136 | } 137 | configPayload, err := pullPayload(ctx, resolver, configRef.String(), ocischemav1.Descriptor{ 138 | Digest: manifest.Config.Digest, 139 | MediaType: manifest.Config.MediaType, 140 | Size: manifest.Config.Size, 141 | }) 142 | if err != nil { 143 | return nil, fmt.Errorf("failed to pull bundle %q: %s", ref, err) 144 | } 145 | var b bundle.Bundle 146 | if err := json.Unmarshal(configPayload, &b); err != nil { 147 | return nil, fmt.Errorf("failed to pull bundle %q: %s", ref, err) 148 | } 149 | logPayload(logger, b) 150 | 151 | return &b, nil 152 | } 153 | 154 | func pullPayload(ctx context.Context, resolver remotes.Resolver, reference string, descriptor ocischemav1.Descriptor) ([]byte, error) { 155 | ctx = withMutedContext(ctx) 156 | fetcher, err := resolver.Fetcher(ctx, reference) 157 | if err != nil { 158 | return nil, err 159 | } 160 | reader, err := fetcher.Fetch(ctx, descriptor) 161 | if err != nil { 162 | return nil, err 163 | } 164 | defer reader.Close() 165 | 166 | result, err := io.ReadAll(reader) 167 | return result, err 168 | } 169 | -------------------------------------------------------------------------------- /converter/convert_test.go: -------------------------------------------------------------------------------- 1 | package converter 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/cnabio/cnab-to-oci/tests" 7 | "github.com/distribution/distribution/manifest/schema2" 8 | "github.com/distribution/reference" 9 | ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1" 10 | "gotest.tools/v3/assert" 11 | ) 12 | 13 | func TestConvertFromFixedUpBundleToOCI(t *testing.T) { 14 | bundleConfigDescriptor := ocischemav1.Descriptor{ 15 | Digest: "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341", 16 | MediaType: schema2.MediaTypeManifest, 17 | Size: 315, 18 | } 19 | targetRef := "my.registry/namespace/my-app:0.1.0" 20 | src := tests.MakeTestBundle() 21 | 22 | relocationMap := tests.MakeRelocationMap() 23 | 24 | expected := tests.MakeTestOCIIndex() 25 | 26 | // Convert from bundle to OCI index 27 | named, err := reference.ParseNormalizedNamed(targetRef) 28 | assert.NilError(t, err) 29 | actual, err := ConvertBundleToOCIIndex(src, named, bundleConfigDescriptor, relocationMap) 30 | assert.NilError(t, err) 31 | assert.DeepEqual(t, expected, actual) 32 | 33 | // Nil maintainers does not add annotation 34 | src = tests.MakeTestBundle() 35 | src.Maintainers = nil 36 | actual, err = ConvertBundleToOCIIndex(src, named, bundleConfigDescriptor, relocationMap) 37 | assert.NilError(t, err) 38 | _, hasMaintainers := actual.Annotations[ocischemav1.AnnotationAuthors] 39 | assert.Assert(t, !hasMaintainers) 40 | 41 | // Nil keywords does not add annotation 42 | src = tests.MakeTestBundle() 43 | src.Keywords = nil 44 | actual, err = ConvertBundleToOCIIndex(src, named, bundleConfigDescriptor, relocationMap) 45 | assert.NilError(t, err) 46 | _, hasKeywords := actual.Annotations[CNABKeywordsAnnotation] 47 | assert.Assert(t, !hasKeywords) 48 | 49 | // Multiple invocation images is not supported 50 | src = tests.MakeTestBundle() 51 | src.InvocationImages = append(src.InvocationImages, src.InvocationImages[0]) 52 | _, err = ConvertBundleToOCIIndex(src, named, bundleConfigDescriptor, relocationMap) 53 | assert.ErrorContains(t, err, "only one invocation image supported") 54 | 55 | // Invalid media type 56 | src = tests.MakeTestBundle() 57 | src.InvocationImages[0].MediaType = "some-invalid-mediatype" 58 | _, err = ConvertBundleToOCIIndex(src, named, bundleConfigDescriptor, relocationMap) 59 | assert.ErrorContains(t, err, `unsupported media type "some-invalid-mediatype" for image "my.registry/namespace/my-app@sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0343"`) 60 | 61 | // All images must be in the same repository 62 | src = tests.MakeTestBundle() 63 | badRelocationMap := tests.MakeRelocationMap() 64 | badRelocationMap["my.registry/namespace/my-app-invoc"] = "my.registry/namespace/other-repo@sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0343" 65 | _, err = ConvertBundleToOCIIndex(src, named, bundleConfigDescriptor, badRelocationMap) 66 | assert.ErrorContains(t, err, `invalid invocation image: image `+ 67 | `"my.registry/namespace/other-repo@sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0343" is not in the same repository as "my.registry/namespace/my-app:0.1.0"`) 68 | 69 | // Image reference must be digested 70 | src = tests.MakeTestBundle() 71 | badRelocationMap = tests.MakeRelocationMap() 72 | badRelocationMap["my.registry/namespace/my-app-invoc"] = "my.registry/namespace/my-app:not-digested" 73 | _, err = ConvertBundleToOCIIndex(src, named, bundleConfigDescriptor, badRelocationMap) 74 | assert.ErrorContains(t, err, "invalid invocation image: image \"my.registry/namespace/"+ 75 | "my-app:not-digested\" is not a digested reference") 76 | 77 | // Invalid reference 78 | src = tests.MakeTestBundle() 79 | badRelocationMap = tests.MakeRelocationMap() 80 | badRelocationMap["my.registry/namespace/my-app-invoc"] = "Some/iNvalid/Ref" 81 | _, err = ConvertBundleToOCIIndex(src, named, bundleConfigDescriptor, badRelocationMap) 82 | assert.ErrorContains(t, err, "invalid invocation image: "+ 83 | "image \"Some/iNvalid/Ref\" is not a valid image reference: invalid reference format: repository name (iNvalid/Ref) must be lowercase") 84 | 85 | // Invalid size 86 | src = tests.MakeTestBundle() 87 | src.InvocationImages[0].Size = 0 88 | _, err = ConvertBundleToOCIIndex(src, named, bundleConfigDescriptor, relocationMap) 89 | assert.ErrorContains(t, err, "size is not set") 90 | 91 | // mediatype ociindex 92 | src = tests.MakeTestBundle() 93 | src.InvocationImages[0].MediaType = ocischemav1.MediaTypeImageIndex 94 | _, err = ConvertBundleToOCIIndex(src, named, bundleConfigDescriptor, relocationMap) 95 | assert.NilError(t, err) 96 | 97 | // mediatype docker manifestlist 98 | src = tests.MakeTestBundle() 99 | src.InvocationImages[0].MediaType = "application/vnd.docker.distribution.manifest.list.v2+json" 100 | _, err = ConvertBundleToOCIIndex(src, named, bundleConfigDescriptor, relocationMap) 101 | assert.NilError(t, err) 102 | } 103 | 104 | func TestGetConfigDescriptor(t *testing.T) { 105 | ix := &ocischemav1.Index{ 106 | Manifests: []ocischemav1.Descriptor{ 107 | { 108 | Digest: "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341", 109 | MediaType: schema2.MediaTypeManifest, 110 | Size: 315, 111 | Annotations: map[string]string{ 112 | CNABDescriptorTypeAnnotation: CNABDescriptorTypeConfig, 113 | }, 114 | }, 115 | { 116 | Digest: "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341", 117 | MediaType: "application/vnd.docker.distribution.manifest.v2+json", 118 | Size: 315, 119 | Annotations: map[string]string{ 120 | "io.cnab.type": "invocation", 121 | }, 122 | }, 123 | { 124 | Digest: "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341", 125 | MediaType: "application/vnd.oci.image.manifest.v1+json", 126 | Size: 1385, 127 | Annotations: map[string]string{ 128 | "io.cnab.type": "component", 129 | "io.cnab.component_name": "image-1", 130 | "io.cnab.original_name": "nginx:2.12", 131 | }, 132 | }, 133 | }, 134 | } 135 | expected := ocischemav1.Descriptor{ 136 | Digest: "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341", 137 | MediaType: schema2.MediaTypeManifest, 138 | Size: 315, 139 | Annotations: map[string]string{ 140 | CNABDescriptorTypeAnnotation: CNABDescriptorTypeConfig, 141 | }, 142 | } 143 | d, err := GetBundleConfigManifestDescriptor(ix) 144 | assert.NilError(t, err) 145 | assert.DeepEqual(t, expected, d) 146 | ix.Manifests = ix.Manifests[1:] 147 | _, err = GetBundleConfigManifestDescriptor(ix) 148 | assert.ErrorContains(t, err, "bundle config not found") 149 | } 150 | 151 | func TestGenerateRelocationMap(t *testing.T) { 152 | targetRef := "my.registry/namespace/my-app:0.1.0" 153 | named, err := reference.ParseNormalizedNamed(targetRef) 154 | assert.NilError(t, err) 155 | 156 | ix := tests.MakeTestOCIIndex() 157 | b := tests.MakeTestBundle() 158 | 159 | expected := tests.MakeRelocationMap() 160 | 161 | relocationMap, err := GenerateRelocationMap(ix, b, named) 162 | assert.NilError(t, err) 163 | assert.DeepEqual(t, relocationMap, expected) 164 | } 165 | -------------------------------------------------------------------------------- /remotes/pull_test.go: -------------------------------------------------------------------------------- 1 | package remotes 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "encoding/json" 7 | "fmt" 8 | "os" 9 | "testing" 10 | 11 | "github.com/cnabio/cnab-to-oci/tests" 12 | "github.com/distribution/reference" 13 | ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1" 14 | "gotest.tools/v3/assert" 15 | ) 16 | 17 | func TestPull(t *testing.T) { 18 | index := tests.MakeTestOCIIndex() 19 | bufBundleManifest, err := json.Marshal(index) 20 | assert.NilError(t, err) 21 | 22 | bundleConfigManifestDescriptor := []byte(`{ 23 | "schemaVersion": 2, 24 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 25 | "config": { 26 | "mediaType": "application/vnd.docker.container.image.v1+json", 27 | "size": 315, 28 | "digest": "sha256:e2337974e94637d3fab7004f87501e605b08bca3adf9ecd356909a9329da128a" 29 | }, 30 | "layers": null 31 | }`) 32 | 33 | b := tests.MakeTestBundle() 34 | bufBundle, err := json.Marshal(b) 35 | assert.NilError(t, err) 36 | 37 | fetcher := &mockFetcher{indexBuffers: []*bytes.Buffer{ 38 | // Bundle index 39 | bytes.NewBuffer(bufBundleManifest), 40 | // Bundle config manifest 41 | bytes.NewBuffer(bundleConfigManifestDescriptor), 42 | // Bundle config 43 | bytes.NewBuffer(bufBundle), 44 | }} 45 | resolver := &mockResolver{ 46 | fetcher: fetcher, 47 | resolvedDescriptors: []ocischemav1.Descriptor{ 48 | // Bundle index descriptor 49 | { 50 | MediaType: ocischemav1.MediaTypeImageIndex, 51 | Digest: tests.BundleDigest, 52 | }, 53 | // Bundle config manifest descriptor 54 | { 55 | MediaType: ocischemav1.MediaTypeDescriptor, 56 | Digest: "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341", 57 | }, 58 | // Bundle config descriptor 59 | {MediaType: ocischemav1.MediaTypeImageIndex}, 60 | }, 61 | } 62 | ref, err := reference.ParseNamed("my.registry/namespace/my-app:my-tag") 63 | assert.NilError(t, err) 64 | 65 | // Pull the CNAB and get the bundle 66 | b, rm, digest, err := Pull(context.Background(), ref, resolver) 67 | assert.NilError(t, err) 68 | expectedBundle := tests.MakeTestBundle() 69 | assert.DeepEqual(t, expectedBundle, b) 70 | 71 | expectedRelocationMap := tests.MakeRelocationMap() 72 | assert.DeepEqual(t, expectedRelocationMap, rm) 73 | 74 | assert.Equal(t, tests.BundleDigest, digest, "incorrect digest pulled") 75 | } 76 | 77 | // nolint: lll 78 | func ExamplePull() { 79 | // Use remotes.CreateResolver for creating your remotes.Resolver 80 | resolver := createExampleResolver() 81 | ref, err := reference.ParseNamed("my.registry/namespace/my-app:my-tag") 82 | if err != nil { 83 | panic(err) 84 | } 85 | 86 | // Pull the CNAB, get the bundle and the associated relocation map 87 | resultBundle, relocationMap, _, err := Pull(context.Background(), ref, resolver) 88 | if err != nil { 89 | panic(err) 90 | } 91 | 92 | resultBundle.WriteTo(os.Stdout) //nolint:errcheck 93 | buf, err := json.Marshal(relocationMap) 94 | if err != nil { 95 | panic(err) 96 | } 97 | fmt.Printf("\n") 98 | fmt.Println(string(buf)) 99 | // Output: 100 | //{"actions":{"action-1":{"modifies":true}},"credentials":{"cred-1":{"env":"env-var","path":"/some/path"}},"custom":{"my-key":"my-value"},"definitions":{"numberType":{"default":0.5,"type":"number"},"output1Type":{"type":"string"},"param1Type":{"default":"hello","enum":["value1",true,1],"type":["string","boolean","number"]}},"description":"description","images":{"another-image":{"contentDigest":"sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0342","description":"","image":"my.registry/namespace/another-image","imageType":"oci","mediaType":"application/vnd.oci.image.manifest.v1+json","size":507},"image-1":{"contentDigest":"sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341","description":"","image":"my.registry/namespace/image-1","imageType":"oci","mediaType":"application/vnd.oci.image.manifest.v1+json","size":507}},"invocationImages":[{"contentDigest":"sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0343","image":"my.registry/namespace/my-app-invoc","imageType":"docker","mediaType":"application/vnd.docker.distribution.manifest.v2+json","size":506}],"keywords":["keyword1","keyword2"],"maintainers":[{"email":"docker@docker.com","name":"docker","url":"docker.com"}],"name":"my-app","outputs":{"output1":{"applyTo":["install"],"definition":"output1Type","description":"magic","path":"/cnab/app/outputs/magic"}},"parameters":{"param1":{"definition":"param1Type","destination":{"env":"env_var","path":"/some/path"}},"param2":{"definition":"numberType","destination":{"env":"PARM2"}}},"schemaVersion":"v1.0.0","version":"0.1.0"} 101 | //{"my.registry/namespace/image-1":"my.registry/namespace/my-app@sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341","my.registry/namespace/my-app-invoc":"my.registry/namespace/my-app@sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341"} 102 | } 103 | 104 | const ( 105 | bufBundleManifest = `{ 106 | "schemaVersion": 1, 107 | "manifests": [ 108 | { 109 | "mediaType": "application/vnd.oci.image.manifest.v1+json", 110 | "digest": "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341", 111 | "size": 315, 112 | "annotations": { 113 | "io.cnab.manifest.type": "config" 114 | } 115 | }, 116 | { 117 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 118 | "digest": "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341", 119 | "size": 506, 120 | "annotations": { 121 | "io.cnab.manifest.type": "invocation" 122 | } 123 | }, 124 | { 125 | "mediaType": "application/vnd.oci.image.manifest.v1+json", 126 | "digest": "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341", 127 | "size": 507, 128 | "annotations": { 129 | "io.cnab.component.name": "image-1", 130 | "io.cnab.manifest.type": "component" 131 | } 132 | } 133 | ], 134 | "annotations": { 135 | "io.cnab.keywords": "[\"keyword1\",\"keyword2\"]", 136 | "io.cnab.runtime_version": "v1.0.0", 137 | "io.docker.app.format": "cnab", 138 | "io.docker.type": "app", 139 | "org.opencontainers.image.authors": "[{\"name\":\"docker\",\"email\":\"docker@docker.com\",\"url\":\"docker.com\"}]", 140 | "org.opencontainers.image.description": "description", 141 | "org.opencontainers.image.title": "my-app", 142 | "org.opencontainers.image.version": "0.1.0" 143 | } 144 | }` 145 | 146 | bundleConfigManifestDescriptor = `{ 147 | "schemaVersion": 2, 148 | "config": { 149 | "mediaType": "application/vnd.cnab.config.v1+json", 150 | "size": 315, 151 | "digest": "sha256:e2337974e94637d3fab7004f87501e605b08bca3adf9ecd356909a9329da128a" 152 | }, 153 | "layers": null 154 | }` 155 | ) 156 | 157 | func createExampleResolver() *mockResolver { 158 | b := tests.MakeTestBundle() 159 | bufBundleConfig, err := json.Marshal(b) 160 | if err != nil { 161 | panic(err) 162 | } 163 | buf := []*bytes.Buffer{ 164 | // Bundle index 165 | bytes.NewBuffer([]byte(bufBundleManifest)), 166 | // Bundle config manifest 167 | bytes.NewBuffer([]byte(bundleConfigManifestDescriptor)), 168 | // Bundle config 169 | bytes.NewBuffer(bufBundleConfig), 170 | } 171 | fetcher := &mockFetcher{indexBuffers: buf} 172 | pusher := &mockPusher{} 173 | return &mockResolver{ 174 | pusher: pusher, 175 | fetcher: fetcher, 176 | resolvedDescriptors: []ocischemav1.Descriptor{ 177 | // Bundle index descriptor 178 | { 179 | MediaType: ocischemav1.MediaTypeImageIndex, 180 | Digest: "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341", 181 | Size: int64(len(bufBundleManifest)), 182 | }, 183 | // Bundle config manifest descriptor 184 | { 185 | MediaType: ocischemav1.MediaTypeDescriptor, 186 | Digest: "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341", 187 | Size: int64(len(bundleConfigManifestDescriptor)), 188 | }, 189 | // Bundle config descriptor 190 | { 191 | MediaType: ocischemav1.MediaTypeImageConfig, 192 | Digest: "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341", 193 | Size: int64(len(bufBundleConfig)), 194 | }, 195 | }, 196 | } 197 | } 198 | -------------------------------------------------------------------------------- /remotes/mount.go: -------------------------------------------------------------------------------- 1 | package remotes 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | "io" 8 | "sort" 9 | "strings" 10 | 11 | "github.com/containerd/containerd/content" 12 | "github.com/containerd/containerd/errdefs" 13 | "github.com/containerd/containerd/images" 14 | "github.com/containerd/containerd/remotes" 15 | "github.com/distribution/reference" 16 | "github.com/opencontainers/go-digest" 17 | ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1" 18 | "golang.org/x/sync/errgroup" 19 | ) 20 | 21 | const ( 22 | // labelDistributionSource describes the source blob comes from. 23 | // This label comes from containerd: https://github.com/containerd/containerd/blob/master/remotes/docker/handler.go#L35 24 | labelDistributionSource = "containerd.io/distribution.source" 25 | ) 26 | 27 | func newDescriptorCopier(ctx context.Context, resolver remotes.Resolver, 28 | sourceFetcher remotes.Fetcher, targetRepo string, 29 | eventNotifier eventNotifier, originalSource reference.Named) (*descriptorCopier, error) { 30 | destPusher, err := resolver.Pusher(ctx, targetRepo) 31 | if err != nil { 32 | return nil, err 33 | } 34 | return &descriptorCopier{ 35 | sourceFetcher: sourceFetcher, 36 | targetPusher: destPusher, 37 | eventNotifier: eventNotifier, 38 | resolver: resolver, 39 | originalSource: originalSource, 40 | }, nil 41 | } 42 | 43 | type descriptorCopier struct { 44 | sourceFetcher remotes.Fetcher 45 | targetPusher remotes.Pusher 46 | eventNotifier eventNotifier 47 | resolver remotes.Resolver 48 | originalSource reference.Named 49 | } 50 | 51 | func (h *descriptorCopier) Handle(ctx context.Context, desc *descriptorProgress) (retErr error) { 52 | ctx, cancel := context.WithCancel(ctx) 53 | defer cancel() 54 | if len(desc.URLs) > 0 { 55 | desc.markDone() 56 | desc.setAction("Skip (foreign layer)") 57 | return nil 58 | } 59 | desc.setAction("Copy") 60 | h.eventNotifier.reportProgress(nil) 61 | defer func() { 62 | if retErr != nil { 63 | desc.setError(retErr) 64 | } 65 | h.eventNotifier.reportProgress(retErr) 66 | }() 67 | writer, err := pushWithAnnotation(ctx, h.targetPusher, h.originalSource, desc.Descriptor) 68 | if errors.Is(err, errdefs.ErrAlreadyExists) { 69 | desc.markDone() 70 | if strings.Contains(err.Error(), "mounted") { 71 | desc.setAction("Mounted") 72 | } 73 | return nil 74 | } 75 | if err != nil { 76 | return err 77 | } 78 | defer writer.Close() 79 | reader, err := h.sourceFetcher.Fetch(ctx, desc.Descriptor) 80 | if err != nil { 81 | return err 82 | } 83 | defer reader.Close() 84 | err = content.Copy(ctx, writer, reader, desc.Size, desc.Digest) 85 | if errors.Is(err, errdefs.ErrAlreadyExists) { 86 | err = nil 87 | } 88 | if err == nil { 89 | desc.markDone() 90 | } 91 | return err 92 | } 93 | 94 | func pushWithAnnotation(ctx context.Context, pusher remotes.Pusher, ref reference.Named, desc ocischemav1.Descriptor) (content.Writer, error) { 95 | // Add the distribution source annotation to help containerd 96 | // mount instead of push when possible. 97 | repo := fmt.Sprintf("%s.%s", labelDistributionSource, reference.Domain(ref)) 98 | desc.Annotations = map[string]string{ 99 | repo: reference.FamiliarName(ref), 100 | } 101 | return pusher.Push(ctx, desc) 102 | } 103 | 104 | func isManifest(mediaType string) bool { 105 | return mediaType == images.MediaTypeDockerSchema1Manifest || 106 | mediaType == images.MediaTypeDockerSchema2Manifest || 107 | mediaType == images.MediaTypeDockerSchema2ManifestList || 108 | mediaType == ocischemav1.MediaTypeImageIndex || 109 | mediaType == ocischemav1.MediaTypeImageManifest 110 | } 111 | 112 | type imageContentProvider struct { 113 | fetcher remotes.Fetcher 114 | } 115 | 116 | func (p *imageContentProvider) ReaderAt(ctx context.Context, desc ocischemav1.Descriptor) (content.ReaderAt, error) { 117 | rc, err := p.fetcher.Fetch(ctx, desc) 118 | if err != nil { 119 | return nil, err 120 | } 121 | return &remoteReaderAt{ReadCloser: rc, currentOffset: 0, size: desc.Size}, nil 122 | } 123 | 124 | type remoteReaderAt struct { 125 | io.ReadCloser 126 | currentOffset int64 127 | size int64 128 | } 129 | 130 | func (r *remoteReaderAt) Size() int64 { 131 | return r.size 132 | } 133 | 134 | func (r *remoteReaderAt) ReadAt(p []byte, off int64) (int, error) { 135 | if off != r.currentOffset { 136 | return 0, fmt.Errorf("at the moment this reader only supports offset at %d, requested offset was %d", r.currentOffset, off) 137 | } 138 | n, err := r.Read(p) 139 | r.currentOffset += int64(n) 140 | if err == io.EOF && n == len(p) { 141 | return n, nil 142 | } 143 | if err != nil || n == len(p) { 144 | return n, err 145 | } 146 | n2, err := r.ReadAt(p[n:], r.currentOffset) 147 | n += n2 148 | return n, err 149 | } 150 | 151 | type descriptorContentHandler struct { 152 | descriptorCopier *descriptorCopier 153 | targetRepo string 154 | 155 | // Keep track of which layers we have copied for this image 156 | // so that we can avoid copying the same layer more than once. 157 | layersScheduled map[digest.Digest]struct{} 158 | } 159 | 160 | func (h *descriptorContentHandler) createCopyTask(ctx context.Context, descProgress *descriptorProgress) (func(ctx context.Context) error, error) { 161 | if _, scheduled := h.layersScheduled[descProgress.Digest]; scheduled { 162 | return func(_ context.Context) error { 163 | // Skip. We have already scheduled a copy of this layer 164 | return nil 165 | }, nil 166 | } 167 | 168 | // Mark that we have scheduled this layer. Some images can have a layer duplicated 169 | // within the image and attempts to copy the same layer multiple times results in 170 | // unexpected size errors when the later copy tasks try to copy an existing layer. 171 | if h.layersScheduled == nil { 172 | h.layersScheduled = make(map[digest.Digest]struct{}, 1) 173 | } 174 | h.layersScheduled[descProgress.Digest] = struct{}{} 175 | 176 | copyOrMountWorkItem := func(ctx context.Context) error { 177 | return h.descriptorCopier.Handle(ctx, descProgress) 178 | } 179 | if !isManifest(descProgress.MediaType) { 180 | return copyOrMountWorkItem, nil 181 | } 182 | _, _, err := h.descriptorCopier.resolver.Resolve(ctx, fmt.Sprintf("%s@%s", h.targetRepo, descProgress.Digest)) 183 | if err == nil { 184 | descProgress.setAction("Skip (already present)") 185 | descProgress.markDone() 186 | return nil, errdefs.ErrAlreadyExists 187 | } 188 | return copyOrMountWorkItem, nil 189 | } 190 | 191 | type manifestWalker struct { 192 | getChildren images.HandlerFunc 193 | eventNotifier eventNotifier 194 | progress *progress 195 | contentHandler *descriptorContentHandler 196 | maxConcurrentJobs int 197 | } 198 | 199 | func newManifestWalker( 200 | eventNotifier eventNotifier, 201 | progress *progress, 202 | descriptorContentHandler *descriptorContentHandler, 203 | maxConcurrentJobs int) *manifestWalker { 204 | sourceFetcher := descriptorContentHandler.descriptorCopier.sourceFetcher 205 | return &manifestWalker{ 206 | eventNotifier: eventNotifier, 207 | getChildren: images.ChildrenHandler(&imageContentProvider{sourceFetcher}), 208 | progress: progress, 209 | contentHandler: descriptorContentHandler, 210 | maxConcurrentJobs: maxConcurrentJobs, 211 | } 212 | } 213 | 214 | type copyTask struct { 215 | digest digest.Digest 216 | copyTask func(ctx context.Context) error 217 | depth int 218 | } 219 | 220 | func (w *manifestWalker) collectCopyTasks(ctx context.Context, desc ocischemav1.Descriptor, parent *descriptorProgress, depth int) ([]copyTask, error) { 221 | descProgress := &descriptorProgress{ 222 | Descriptor: desc, 223 | } 224 | if parent != nil { 225 | parent.addChild(descProgress) 226 | } else { 227 | w.progress.addRoot(descProgress) 228 | } 229 | 230 | var allItems []copyTask 231 | copyOrMountWorkItem, err := w.contentHandler.createCopyTask(ctx, descProgress) 232 | if errors.Is(err, errdefs.ErrAlreadyExists) { 233 | w.eventNotifier.reportProgress(nil) 234 | return nil, nil 235 | } 236 | if err != nil { 237 | w.eventNotifier.reportProgress(err) 238 | return nil, err 239 | } 240 | allItems = append(allItems, copyTask{ 241 | desc.Digest, 242 | copyOrMountWorkItem, 243 | depth, 244 | }) 245 | children, err := w.getChildren.Handle(ctx, desc) 246 | if err != nil { 247 | return nil, err 248 | } 249 | 250 | for _, c := range children { 251 | childCopyTasks, err := w.collectCopyTasks(ctx, c, descProgress, depth+1) 252 | if err != nil { 253 | return nil, err 254 | } 255 | allItems = append(allItems, childCopyTasks...) 256 | } 257 | 258 | return allItems, nil 259 | } 260 | 261 | func (w *manifestWalker) walk(ctx context.Context, desc ocischemav1.Descriptor) error { 262 | select { 263 | case <-ctx.Done(): 264 | return ctx.Err() 265 | default: 266 | } 267 | tasks, err := w.collectCopyTasks(ctx, desc, nil, 0) 268 | if err != nil { 269 | return err 270 | } 271 | if len(tasks) == 0 { 272 | return nil 273 | } 274 | sort.Slice(tasks, func(i, j int) bool { 275 | return tasks[i].depth > tasks[j].depth 276 | }) 277 | 278 | workGroup, c := errgroup.WithContext(ctx) 279 | workGroup.SetLimit(w.maxConcurrentJobs) 280 | lastDepth := tasks[0].depth 281 | for _, task := range tasks { 282 | if task.depth != lastDepth { 283 | err = workGroup.Wait() 284 | if err != nil { 285 | return err 286 | } 287 | workGroup, c = errgroup.WithContext(ctx) 288 | workGroup.SetLimit(w.maxConcurrentJobs) 289 | } 290 | workGroup.Go(func() error { 291 | select { 292 | case <-c.Done(): 293 | return c.Err() 294 | default: 295 | } 296 | err = task.copyTask(c) 297 | if err != nil { 298 | return err 299 | } 300 | return nil 301 | }) 302 | lastDepth = task.depth 303 | } 304 | 305 | return workGroup.Wait() 306 | } 307 | 308 | type eventNotifier func(eventType FixupEventType, message string, err error) 309 | 310 | func (n eventNotifier) reportProgress(err error) { 311 | n(FixupEventTypeProgress, "", err) 312 | } 313 | -------------------------------------------------------------------------------- /converter/convert.go: -------------------------------------------------------------------------------- 1 | package converter 2 | 3 | import ( 4 | _ "crypto/sha256" // this ensures we can parse sha256 digests 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "sort" 9 | 10 | "github.com/cnabio/cnab-go/bundle" 11 | "github.com/cnabio/cnab-to-oci/relocation" 12 | "github.com/containerd/containerd/images" 13 | "github.com/distribution/reference" 14 | ocischema "github.com/opencontainers/image-spec/specs-go" 15 | ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1" 16 | ) 17 | 18 | const ( // General values 19 | // CNABVersion is the currently supported CNAB runtime version 20 | CNABVersion = "v1.0.0" 21 | 22 | // OCIIndexSchemaVersion is the currently supported OCI index schema's version 23 | OCIIndexSchemaVersion = 2 24 | ) 25 | 26 | type cnabDescriptorTypeValue = string 27 | 28 | const ( // Top Level annotations and values 29 | // CNABRuntimeVersionAnnotation is the top level annotation specifying the CNAB runtime version 30 | CNABRuntimeVersionAnnotation = "io.cnab.runtime_version" 31 | // CNABKeywordsAnnotation is the top level annotation specifying a list of keywords 32 | CNABKeywordsAnnotation = "io.cnab.keywords" 33 | // ArtifactTypeAnnotation is the top level annotation specifying the type of the artifact in the registry 34 | ArtifactTypeAnnotation = "org.opencontainers.artifactType" 35 | // ArtifactTypeValue is the value of ArtifactTypeAnnotion for CNAB bundles 36 | ArtifactTypeValue = "application/vnd.cnab.manifest.v1" 37 | ) 38 | 39 | const ( // Descriptor level annotations and values 40 | // CNABDescriptorTypeAnnotation is a descriptor-level annotation specifying the type of reference image (currently invocation or component) 41 | CNABDescriptorTypeAnnotation = "io.cnab.manifest.type" 42 | // CNABDescriptorTypeInvocation is the CNABDescriptorTypeAnnotation value for invocation images 43 | CNABDescriptorTypeInvocation cnabDescriptorTypeValue = "invocation" 44 | // CNABDescriptorTypeComponent is the CNABDescriptorTypeAnnotation value for component images 45 | CNABDescriptorTypeComponent cnabDescriptorTypeValue = "component" 46 | // CNABDescriptorTypeConfig is the CNABDescriptorTypeAnnotation value for bundle configuration 47 | CNABDescriptorTypeConfig cnabDescriptorTypeValue = "config" 48 | 49 | // CNABDescriptorComponentNameAnnotation is a decriptor-level annotation specifying the component name 50 | CNABDescriptorComponentNameAnnotation = "io.cnab.component.name" 51 | ) 52 | 53 | // GetBundleConfigManifestDescriptor returns the CNAB runtime config manifest descriptor from a OCI index 54 | func GetBundleConfigManifestDescriptor(ix *ocischemav1.Index) (ocischemav1.Descriptor, error) { 55 | for _, d := range ix.Manifests { 56 | if d.Annotations[CNABDescriptorTypeAnnotation] == CNABDescriptorTypeConfig { 57 | return d, nil 58 | } 59 | } 60 | return ocischemav1.Descriptor{}, errors.New("bundle config not found") 61 | } 62 | 63 | // ConvertBundleToOCIIndex converts a CNAB bundle into an OCI Index representation 64 | func ConvertBundleToOCIIndex(b *bundle.Bundle, targetRef reference.Named, 65 | bundleConfigManifestRef ocischemav1.Descriptor, relocationMap relocation.ImageRelocationMap) (*ocischemav1.Index, error) { 66 | annotations, err := makeAnnotations(b) 67 | if err != nil { 68 | return nil, err 69 | } 70 | manifests, err := makeManifests(b, targetRef, bundleConfigManifestRef, relocationMap) 71 | if err != nil { 72 | return nil, err 73 | } 74 | result := ocischemav1.Index{ 75 | Versioned: ocischema.Versioned{ 76 | SchemaVersion: OCIIndexSchemaVersion, 77 | }, 78 | Annotations: annotations, 79 | Manifests: manifests, 80 | } 81 | return &result, nil 82 | } 83 | 84 | // GenerateRelocationMap generates the bundle relocation map 85 | func GenerateRelocationMap(ix *ocischemav1.Index, b *bundle.Bundle, originRepo reference.Named) (relocation.ImageRelocationMap, error) { 86 | relocationMap := relocation.ImageRelocationMap{} 87 | 88 | for _, d := range ix.Manifests { 89 | switch d.MediaType { 90 | case ocischemav1.MediaTypeImageManifest, ocischemav1.MediaTypeImageIndex: 91 | case images.MediaTypeDockerSchema2Manifest, images.MediaTypeDockerSchema2ManifestList: 92 | default: 93 | return nil, fmt.Errorf("unsupported manifest descriptor %q with mediatype %q", d.Digest, d.MediaType) 94 | } 95 | descriptorType, ok := d.Annotations[CNABDescriptorTypeAnnotation] 96 | if !ok { 97 | return nil, fmt.Errorf("manifest descriptor %q has no CNAB descriptor type annotation %q", d.Digest, CNABDescriptorTypeAnnotation) 98 | } 99 | if descriptorType == CNABDescriptorTypeConfig { 100 | continue 101 | } 102 | // strip tag/digest from originRepo 103 | originRepo, err := reference.ParseNormalizedNamed(originRepo.Name()) 104 | if err != nil { 105 | return nil, fmt.Errorf("failed to create a digested reference for manifest descriptor %q: %s", d.Digest, err) 106 | } 107 | ref, err := reference.WithDigest(originRepo, d.Digest) 108 | if err != nil { 109 | return nil, fmt.Errorf("failed to create a digested reference for manifest descriptor %q: %s", d.Digest, err) 110 | } 111 | refFamiliar := reference.FamiliarString(ref) 112 | switch descriptorType { 113 | // The current descriptor is an invocation image 114 | case CNABDescriptorTypeInvocation: 115 | if len(b.InvocationImages) == 0 { 116 | return nil, fmt.Errorf("unknown invocation image: %q", d.Digest) 117 | } 118 | relocationMap[b.InvocationImages[0].Image] = refFamiliar 119 | 120 | // The current descriptor is a component image 121 | case CNABDescriptorTypeComponent: 122 | componentName, ok := d.Annotations[CNABDescriptorComponentNameAnnotation] 123 | if !ok { 124 | return nil, fmt.Errorf("component name missing in descriptor %q", d.Digest) 125 | } 126 | c, ok := b.Images[componentName] 127 | if !ok { 128 | return nil, fmt.Errorf("component %q not found in bundle", componentName) 129 | } 130 | relocationMap[c.Image] = refFamiliar 131 | default: 132 | return nil, fmt.Errorf("invalid CNAB descriptor type %q in descriptor %q", descriptorType, d.Digest) 133 | } 134 | } 135 | 136 | return relocationMap, nil 137 | } 138 | 139 | func makeAnnotations(b *bundle.Bundle) (map[string]string, error) { 140 | result := map[string]string{ 141 | CNABRuntimeVersionAnnotation: string(b.SchemaVersion), 142 | ocischemav1.AnnotationTitle: b.Name, 143 | ocischemav1.AnnotationVersion: b.Version, 144 | ocischemav1.AnnotationDescription: b.Description, 145 | ArtifactTypeAnnotation: ArtifactTypeValue, 146 | } 147 | if b.Maintainers != nil { 148 | maintainers, err := json.Marshal(b.Maintainers) 149 | if err != nil { 150 | return nil, err 151 | } 152 | result[ocischemav1.AnnotationAuthors] = string(maintainers) 153 | } 154 | if b.Keywords != nil { 155 | keywords, err := json.Marshal(b.Keywords) 156 | if err != nil { 157 | return nil, err 158 | } 159 | result[CNABKeywordsAnnotation] = string(keywords) 160 | } 161 | return result, nil 162 | } 163 | 164 | func makeManifests(b *bundle.Bundle, targetReference reference.Named, 165 | bundleConfigManifestReference ocischemav1.Descriptor, relocationMap relocation.ImageRelocationMap) ([]ocischemav1.Descriptor, error) { 166 | if len(b.InvocationImages) != 1 { 167 | return nil, errors.New("only one invocation image supported") 168 | } 169 | if bundleConfigManifestReference.Annotations == nil { 170 | bundleConfigManifestReference.Annotations = map[string]string{} 171 | } 172 | bundleConfigManifestReference.Annotations[CNABDescriptorTypeAnnotation] = CNABDescriptorTypeConfig 173 | manifests := []ocischemav1.Descriptor{bundleConfigManifestReference} 174 | invocationImage, err := makeDescriptor(b.InvocationImages[0].BaseImage, targetReference, relocationMap) 175 | if err != nil { 176 | return nil, fmt.Errorf("invalid invocation image: %s", err) 177 | } 178 | invocationImage.Annotations = map[string]string{ 179 | CNABDescriptorTypeAnnotation: CNABDescriptorTypeInvocation, 180 | } 181 | manifests = append(manifests, invocationImage) 182 | images := makeSortedImages(b.Images) 183 | for _, name := range images { 184 | img := b.Images[name] 185 | image, err := makeDescriptor(img.BaseImage, targetReference, relocationMap) 186 | if err != nil { 187 | return nil, fmt.Errorf("invalid image: %s", err) 188 | } 189 | image.Annotations = map[string]string{ 190 | CNABDescriptorTypeAnnotation: CNABDescriptorTypeComponent, 191 | CNABDescriptorComponentNameAnnotation: name, 192 | } 193 | manifests = append(manifests, image) 194 | } 195 | return manifests, nil 196 | } 197 | 198 | func makeSortedImages(images map[string]bundle.Image) []string { 199 | var result []string 200 | for k := range images { 201 | result = append(result, k) 202 | } 203 | sort.Strings(result) 204 | return result 205 | } 206 | 207 | func makeDescriptor(baseImage bundle.BaseImage, targetReference reference.Named, relocationMap relocation.ImageRelocationMap) (ocischemav1.Descriptor, error) { 208 | relocatedImage, ok := relocationMap[baseImage.Image] 209 | if !ok { 210 | return ocischemav1.Descriptor{}, fmt.Errorf("image %q not present in the relocation map", baseImage.Image) 211 | } 212 | 213 | named, err := reference.ParseNormalizedNamed(relocatedImage) 214 | if err != nil { 215 | return ocischemav1.Descriptor{}, fmt.Errorf("image %q is not a valid image reference: %s", relocatedImage, err) 216 | } 217 | if named.Name() != targetReference.Name() { 218 | return ocischemav1.Descriptor{}, fmt.Errorf("image %q is not in the same repository as %q", relocatedImage, targetReference.String()) 219 | } 220 | digested, ok := named.(reference.Digested) 221 | if !ok { 222 | return ocischemav1.Descriptor{}, fmt.Errorf("image %q is not a digested reference", relocatedImage) 223 | } 224 | mediaType, err := getMediaType(baseImage, relocatedImage) 225 | if err != nil { 226 | return ocischemav1.Descriptor{}, err 227 | } 228 | if baseImage.Size == 0 { 229 | return ocischemav1.Descriptor{}, fmt.Errorf("image %q size is not set", relocatedImage) 230 | } 231 | 232 | return ocischemav1.Descriptor{ 233 | Digest: digested.Digest(), 234 | MediaType: mediaType, 235 | Size: int64(baseImage.Size), 236 | }, nil 237 | } 238 | 239 | func getMediaType(baseImage bundle.BaseImage, relocatedImage string) (string, error) { 240 | mediaType := baseImage.MediaType 241 | if mediaType == "" { 242 | switch baseImage.ImageType { 243 | case "docker": 244 | mediaType = images.MediaTypeDockerSchema2Manifest 245 | case "oci": 246 | mediaType = ocischemav1.MediaTypeImageManifest 247 | default: 248 | return "", fmt.Errorf("unsupported image type %q for image %q", baseImage.ImageType, relocatedImage) 249 | } 250 | } 251 | switch mediaType { 252 | case ocischemav1.MediaTypeImageManifest: 253 | case images.MediaTypeDockerSchema2Manifest: 254 | case ocischemav1.MediaTypeImageIndex: 255 | case images.MediaTypeDockerSchema2ManifestList: 256 | default: 257 | return "", fmt.Errorf("unsupported media type %q for image %q", baseImage.MediaType, relocatedImage) 258 | } 259 | return mediaType, nil 260 | } 261 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | https://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | Copyright 2013-2018 Docker, Inc. 180 | 181 | Licensed under the Apache License, Version 2.0 (the "License"); 182 | you may not use this file except in compliance with the License. 183 | You may obtain a copy of the License at 184 | 185 | https://www.apache.org/licenses/LICENSE-2.0 186 | 187 | Unless required by applicable law or agreed to in writing, software 188 | distributed under the License is distributed on an "AS IS" BASIS, 189 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 190 | See the License for the specific language governing permissions and 191 | limitations under the License. 192 | -------------------------------------------------------------------------------- /remotes/push.go: -------------------------------------------------------------------------------- 1 | package remotes 2 | 3 | import ( 4 | "context" 5 | "encoding/base64" 6 | "encoding/json" 7 | "errors" 8 | "fmt" 9 | "io" 10 | "os" 11 | 12 | "github.com/cnabio/cnab-go/bundle" 13 | "github.com/cnabio/cnab-to-oci/converter" 14 | "github.com/cnabio/cnab-to-oci/internal" 15 | "github.com/cnabio/cnab-to-oci/relocation" 16 | "github.com/containerd/containerd/errdefs" 17 | "github.com/containerd/containerd/images" 18 | "github.com/containerd/containerd/remotes" 19 | "github.com/containerd/log" 20 | "github.com/distribution/reference" 21 | "github.com/docker/cli/cli/config" 22 | "github.com/docker/cli/cli/config/credentials" 23 | configtypes "github.com/docker/cli/cli/config/types" 24 | "github.com/docker/docker/api/types/image" 25 | registrytypes "github.com/docker/docker/api/types/registry" 26 | "github.com/docker/docker/pkg/jsonmessage" 27 | "github.com/docker/docker/registry" 28 | "github.com/opencontainers/go-digest" 29 | ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1" 30 | ) 31 | 32 | // ManifestOption is a callback used to customize a manifest before pushing it 33 | type ManifestOption func(*ocischemav1.Index) error 34 | 35 | // Push pushes a bundle as an OCI Image Index manifest 36 | func Push(ctx context.Context, 37 | b *bundle.Bundle, 38 | relocationMap relocation.ImageRelocationMap, 39 | ref reference.Named, 40 | resolver remotes.Resolver, 41 | allowFallbacks bool, 42 | options ...ManifestOption) (ocischemav1.Descriptor, error) { 43 | log.G(ctx).Debugf("Pushing CNAB Bundle %s", ref) 44 | 45 | confManifestDescriptor, err := pushConfig(ctx, b, ref, resolver, allowFallbacks) 46 | if err != nil { 47 | return ocischemav1.Descriptor{}, err 48 | } 49 | 50 | indexDescriptor, err := pushIndex(ctx, b, relocationMap, ref, resolver, allowFallbacks, confManifestDescriptor, options...) 51 | if err != nil { 52 | return ocischemav1.Descriptor{}, err 53 | } 54 | 55 | log.G(ctx).Debug("CNAB Bundle pushed") 56 | return indexDescriptor, nil 57 | } 58 | 59 | func pushConfig(ctx context.Context, 60 | b *bundle.Bundle, 61 | ref reference.Named, //nolint:interfacer 62 | resolver remotes.Resolver, 63 | allowFallbacks bool) (ocischemav1.Descriptor, error) { 64 | logger := log.G(ctx) 65 | logger.Debugf("Pushing CNAB Bundle Config") 66 | 67 | bundleConfig, err := converter.PrepareForPush(b) 68 | if err != nil { 69 | return ocischemav1.Descriptor{}, err 70 | } 71 | confManifestDescriptor, err := pushBundleConfig(ctx, resolver, ref.Name(), bundleConfig, allowFallbacks) 72 | if err != nil { 73 | return ocischemav1.Descriptor{}, fmt.Errorf("error while pushing bundle config manifest: %s", err) 74 | } 75 | 76 | logger.Debug("CNAB Bundle Config pushed") 77 | return confManifestDescriptor, nil 78 | } 79 | 80 | func pushIndex(ctx context.Context, b *bundle.Bundle, relocationMap relocation.ImageRelocationMap, ref reference.Named, resolver remotes.Resolver, allowFallbacks bool, 81 | confManifestDescriptor ocischemav1.Descriptor, options ...ManifestOption) (ocischemav1.Descriptor, error) { 82 | logger := log.G(ctx) 83 | logger.Debug("Pushing CNAB Index") 84 | 85 | indexDescriptor, indexPayload, err := prepareIndex(b, relocationMap, ref, confManifestDescriptor, options...) 86 | if err != nil { 87 | return ocischemav1.Descriptor{}, err 88 | } 89 | // Push the bundle index 90 | logger.Debug("Trying to push OCI Index") 91 | logger.Debug(string(indexPayload)) 92 | logger.Debug("OCI Index Descriptor") 93 | logPayload(logger, indexDescriptor) 94 | 95 | if err := pushPayload(ctx, resolver, ref.String(), indexDescriptor, indexPayload); err != nil { 96 | if !allowFallbacks { 97 | logger.Debug("Not using fallbacks, giving up") 98 | return ocischemav1.Descriptor{}, err 99 | } 100 | logger.Debugf("Unable to push OCI Index: %v", err) 101 | // retry with a docker manifestlist 102 | return pushDockerManifestList(ctx, b, relocationMap, ref, resolver, confManifestDescriptor, options...) 103 | } 104 | 105 | logger.Debugf("CNAB Index pushed") 106 | return indexDescriptor, nil 107 | } 108 | 109 | func pushDockerManifestList(ctx context.Context, b *bundle.Bundle, relocationMap relocation.ImageRelocationMap, ref reference.Named, resolver remotes.Resolver, 110 | confManifestDescriptor ocischemav1.Descriptor, options ...ManifestOption) (ocischemav1.Descriptor, error) { 111 | logger := log.G(ctx) 112 | 113 | indexDescriptor, indexPayload, err := prepareIndexNonOCI(b, relocationMap, ref, confManifestDescriptor, options...) 114 | if err != nil { 115 | return ocischemav1.Descriptor{}, err 116 | } 117 | logger.Debug("Trying to push Index with Manifest list as fallback") 118 | logger.Debug(string(indexPayload)) 119 | logger.Debug("Manifest list Descriptor") 120 | logPayload(logger, indexDescriptor) 121 | 122 | if err := pushPayload(ctx, 123 | resolver, ref.String(), 124 | indexDescriptor, 125 | indexPayload); err != nil { 126 | return ocischemav1.Descriptor{}, err 127 | } 128 | return indexDescriptor, nil 129 | } 130 | 131 | func prepareIndex(b *bundle.Bundle, 132 | relocationMap relocation.ImageRelocationMap, 133 | ref reference.Named, 134 | confDescriptor ocischemav1.Descriptor, 135 | options ...ManifestOption) (ocischemav1.Descriptor, []byte, error) { 136 | ix, err := convertIndexAndApplyOptions(b, relocationMap, ref, confDescriptor, options...) 137 | if err != nil { 138 | return ocischemav1.Descriptor{}, nil, err 139 | } 140 | indexPayload, err := json.Marshal(ix) 141 | if err != nil { 142 | return ocischemav1.Descriptor{}, nil, fmt.Errorf("invalid bundle manifest %q: %s", ref, err) 143 | } 144 | indexDescriptor := ocischemav1.Descriptor{ 145 | Digest: digest.FromBytes(indexPayload), 146 | MediaType: ocischemav1.MediaTypeImageIndex, 147 | Size: int64(len(indexPayload)), 148 | } 149 | return indexDescriptor, indexPayload, nil 150 | } 151 | 152 | type ociIndexWrapper struct { 153 | ocischemav1.Index 154 | MediaType string `json:"mediaType,omitempty"` 155 | } 156 | 157 | func convertIndexAndApplyOptions(b *bundle.Bundle, 158 | relocationMap relocation.ImageRelocationMap, 159 | ref reference.Named, 160 | confDescriptor ocischemav1.Descriptor, 161 | options ...ManifestOption) (*ocischemav1.Index, error) { 162 | ix, err := converter.ConvertBundleToOCIIndex(b, ref, confDescriptor, relocationMap) 163 | if err != nil { 164 | return nil, err 165 | } 166 | for _, opts := range options { 167 | if err := opts(ix); err != nil { 168 | return nil, fmt.Errorf("failed to prepare bundle manifest %q: %s", ref, err) 169 | } 170 | } 171 | return ix, nil 172 | } 173 | 174 | func prepareIndexNonOCI(b *bundle.Bundle, 175 | relocationMap relocation.ImageRelocationMap, 176 | ref reference.Named, 177 | confDescriptor ocischemav1.Descriptor, 178 | options ...ManifestOption) (ocischemav1.Descriptor, []byte, error) { 179 | ix, err := convertIndexAndApplyOptions(b, relocationMap, ref, confDescriptor, options...) 180 | if err != nil { 181 | return ocischemav1.Descriptor{}, nil, err 182 | } 183 | w := &ociIndexWrapper{Index: *ix, MediaType: images.MediaTypeDockerSchema2ManifestList} 184 | w.SchemaVersion = 2 185 | indexPayload, err := json.Marshal(w) 186 | if err != nil { 187 | return ocischemav1.Descriptor{}, nil, fmt.Errorf("invalid bundle manifest %q: %s", ref, err) 188 | } 189 | indexDescriptor := ocischemav1.Descriptor{ 190 | Digest: digest.FromBytes(indexPayload), 191 | MediaType: images.MediaTypeDockerSchema2ManifestList, 192 | Size: int64(len(indexPayload)), 193 | } 194 | return indexDescriptor, indexPayload, nil 195 | } 196 | 197 | func pushPayload(ctx context.Context, resolver remotes.Resolver, reference string, descriptor ocischemav1.Descriptor, payload []byte) error { 198 | ctx = withMutedContext(ctx) 199 | pusher, err := resolver.Pusher(ctx, reference) 200 | if err != nil { 201 | return err 202 | } 203 | writer, err := pusher.Push(ctx, descriptor) 204 | if err != nil { 205 | if errors.Is(err, errdefs.ErrAlreadyExists) { 206 | return nil 207 | } 208 | return err 209 | } 210 | defer writer.Close() 211 | if _, err := writer.Write(payload); err != nil { 212 | if errors.Is(err, errdefs.ErrAlreadyExists) { 213 | return nil 214 | } 215 | return err 216 | } 217 | err = writer.Commit(ctx, descriptor.Size, descriptor.Digest) 218 | if errors.Is(err, errdefs.ErrAlreadyExists) { 219 | return nil 220 | } 221 | return err 222 | } 223 | 224 | func pushBundleConfig(ctx context.Context, resolver remotes.Resolver, reference string, bundleConfig *converter.PreparedBundleConfig, allowFallbacks bool) (ocischemav1.Descriptor, error) { 225 | if d, err := pushBundleConfigDescriptor(ctx, "Config", resolver, reference, 226 | bundleConfig.ConfigBlobDescriptor, bundleConfig.ConfigBlob, bundleConfig.Fallback, allowFallbacks); err != nil { 227 | return d, err 228 | } 229 | return pushBundleConfigDescriptor(ctx, "Config Manifest", resolver, reference, 230 | bundleConfig.ManifestDescriptor, bundleConfig.Manifest, bundleConfig.Fallback, allowFallbacks) 231 | } 232 | 233 | func pushBundleConfigDescriptor(ctx context.Context, name string, resolver remotes.Resolver, reference string, 234 | descriptor ocischemav1.Descriptor, payload []byte, fallback *converter.PreparedBundleConfig, allowFallbacks bool) (ocischemav1.Descriptor, error) { 235 | logger := log.G(ctx) 236 | logger.Debugf("Trying to push CNAB Bundle %s", name) 237 | logger.Debugf("CNAB Bundle %s Descriptor", name) 238 | logPayload(logger, descriptor) 239 | 240 | if err := pushPayload(ctx, resolver, reference, descriptor, payload); err != nil { 241 | if allowFallbacks && fallback != nil { 242 | logger.Debugf("Failed to push CNAB Bundle %s, trying with a fallback method", name) 243 | return pushBundleConfig(ctx, resolver, reference, fallback, allowFallbacks) 244 | } 245 | return ocischemav1.Descriptor{}, err 246 | } 247 | return descriptor, nil 248 | } 249 | 250 | func pushTaggedImage(ctx context.Context, imageClient internal.ImageClient, targetRef reference.Named, out io.Writer) error { 251 | repoInfo, err := registry.ParseRepositoryInfo(targetRef) 252 | if err != nil { 253 | return err 254 | } 255 | 256 | authConfig := resolveAuthConfig(repoInfo.Index) 257 | encodedAuth, err := encodeAuthToBase64(authConfig) 258 | if err != nil { 259 | return err 260 | } 261 | 262 | reader, err := imageClient.ImagePush(ctx, targetRef.String(), image.PushOptions{ 263 | RegistryAuth: encodedAuth, 264 | }) 265 | if err != nil { 266 | return err 267 | } 268 | defer reader.Close() 269 | return jsonmessage.DisplayJSONMessagesStream(reader, out, 0, false, nil) 270 | } 271 | 272 | func encodeAuthToBase64(authConfig configtypes.AuthConfig) (string, error) { 273 | buf, err := json.Marshal(authConfig) 274 | if err != nil { 275 | return "", err 276 | } 277 | return base64.URLEncoding.EncodeToString(buf), nil 278 | } 279 | 280 | func resolveAuthConfig(index *registrytypes.IndexInfo) configtypes.AuthConfig { 281 | cfg := config.LoadDefaultConfigFile(os.Stderr) 282 | 283 | hostName := index.Name 284 | if index.Official { 285 | hostName = registry.IndexServer 286 | } 287 | 288 | configs, err := cfg.GetAllCredentials() 289 | if err != nil { 290 | return configtypes.AuthConfig{} 291 | } 292 | 293 | // See https://github.com/docker/cli/blob/23446275646041f9b598d64c51be24d5d0e49376/cli/config/credentials/file_store.go#L32-L47 294 | // We are looking for the hostname in the configuration, and if not we are trying with a pure hostname (so without 295 | // http/https). 296 | authConfig, ok := configs[hostName] 297 | if !ok { 298 | for reg, config := range configs { 299 | if hostName == credentials.ConvertToHostname(reg) { 300 | return config 301 | } 302 | } 303 | return configtypes.AuthConfig{} 304 | } 305 | return authConfig 306 | } 307 | -------------------------------------------------------------------------------- /remotes/fixup.go: -------------------------------------------------------------------------------- 1 | package remotes 2 | 3 | import ( 4 | "context" 5 | "encoding/json" 6 | "fmt" 7 | "io" 8 | 9 | "github.com/cnabio/cnab-go/bundle" 10 | "github.com/cnabio/cnab-to-oci/relocation" 11 | "github.com/containerd/containerd/images" 12 | "github.com/containerd/containerd/remotes" 13 | "github.com/containerd/log" 14 | "github.com/containerd/platforms" 15 | "github.com/distribution/reference" 16 | "github.com/hashicorp/go-multierror" 17 | ocischemav1 "github.com/opencontainers/image-spec/specs-go/v1" 18 | ) 19 | 20 | // FixupBundle checks that all the references are present in the referenced repository, otherwise it will mount all 21 | // the manifests to that repository. The bundle is then patched with the new digested references. 22 | func FixupBundle(ctx context.Context, b *bundle.Bundle, ref reference.Named, resolver remotes.Resolver, opts ...FixupOption) (relocation.ImageRelocationMap, error) { 23 | logger := log.G(ctx) 24 | logger.Debugf("Fixing up bundle %s", ref) 25 | 26 | // Configure the fixup and the event loop 27 | cfg, err := newFixupConfig(b, ref, resolver, opts...) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | events := make(chan FixupEvent) 33 | eventLoopDone := make(chan struct{}) 34 | defer func() { 35 | close(events) 36 | // wait for all queued events to be treated 37 | <-eventLoopDone 38 | }() 39 | go func() { 40 | defer close(eventLoopDone) 41 | for ev := range events { 42 | cfg.eventCallback(ev) 43 | } 44 | }() 45 | 46 | // Fixup invocation images 47 | if len(b.InvocationImages) != 1 { 48 | return nil, fmt.Errorf("only one invocation image supported for bundle %q", ref) 49 | } 50 | 51 | relocationMap := cfg.relocationMap 52 | if err := fixupImage(ctx, "InvocationImage", &b.InvocationImages[0].BaseImage, relocationMap, cfg, events, cfg.invocationImagePlatformFilter); err != nil { 53 | return nil, err 54 | } 55 | // Fixup images 56 | for name, original := range b.Images { 57 | if err := fixupImage(ctx, name, &original.BaseImage, relocationMap, cfg, events, cfg.componentImagePlatformFilter); err != nil { 58 | return nil, err 59 | } 60 | b.Images[name] = original 61 | } 62 | 63 | logger.Debug("Bundle fixed") 64 | return relocationMap, nil 65 | } 66 | 67 | func fixupImage( 68 | ctx context.Context, 69 | name string, 70 | baseImage *bundle.BaseImage, 71 | relocationMap relocation.ImageRelocationMap, 72 | cfg fixupConfig, 73 | events chan<- FixupEvent, 74 | platformFilter platforms.Matcher) error { 75 | 76 | // Fixup the base image, using the relocated base image if available 77 | sourceImage := *baseImage 78 | if relocatedBaseImage, ok := relocationMap[baseImage.Image]; ok { 79 | sourceImage.Image = relocatedBaseImage 80 | } 81 | 82 | log.G(ctx).Debugf("Updating entry in relocation map for %q", baseImage.Image) 83 | ctx = withMutedContext(ctx) 84 | notifyEvent, progress := makeEventNotifier(events, sourceImage.Image, cfg.targetRef) 85 | 86 | notifyEvent(FixupEventTypeCopyImageStart, "", nil) 87 | fixupInfo, pushed, err := fixupBaseImage(ctx, name, &sourceImage, cfg) 88 | if err != nil { 89 | return notifyError(notifyEvent, err) 90 | } 91 | // Update the relocation map with the original image name and the digested reference of the image pushed inside the bundle repository 92 | newRef, err := reference.WithDigest(fixupInfo.targetRepo, fixupInfo.resolvedDescriptor.Digest) 93 | if err != nil { 94 | return err 95 | } 96 | 97 | relocationMap[baseImage.Image] = newRef.String() 98 | 99 | // if the autoUpdateBundle flag is passed, mutate the bundle with the resolved digest, mediaType, and size 100 | if cfg.autoBundleUpdate { 101 | baseImage.Digest = fixupInfo.resolvedDescriptor.Digest.String() 102 | baseImage.Size = uint64(fixupInfo.resolvedDescriptor.Size) 103 | baseImage.MediaType = fixupInfo.resolvedDescriptor.MediaType 104 | } else { 105 | if baseImage.Digest != fixupInfo.resolvedDescriptor.Digest.String() { 106 | return fmt.Errorf("image %q digest differs %q after fixup: %q", baseImage.Image, baseImage.Digest, fixupInfo.resolvedDescriptor.Digest.String()) 107 | } 108 | if baseImage.Size != uint64(fixupInfo.resolvedDescriptor.Size) { 109 | return fmt.Errorf("image %q size differs %d after fixup: %d", baseImage.Image, baseImage.Size, fixupInfo.resolvedDescriptor.Size) 110 | } 111 | if baseImage.MediaType != fixupInfo.resolvedDescriptor.MediaType { 112 | return fmt.Errorf("image %q media type differs %q after fixup: %q", baseImage.Image, baseImage.MediaType, fixupInfo.resolvedDescriptor.MediaType) 113 | } 114 | } 115 | 116 | if pushed { 117 | notifyEvent(FixupEventTypeCopyImageEnd, "Image has been pushed for service "+name, nil) 118 | return nil 119 | } 120 | 121 | if fixupInfo.sourceRef.Name() == fixupInfo.targetRepo.Name() { 122 | notifyEvent(FixupEventTypeCopyImageEnd, "Nothing to do: image reference is already present in repository"+fixupInfo.targetRepo.String(), nil) 123 | return nil 124 | } 125 | 126 | sourceFetcher, err := makeSourceFetcher(ctx, cfg.resolver, fixupInfo.sourceRef.Name()) 127 | if err != nil { 128 | return notifyError(notifyEvent, err) 129 | } 130 | 131 | // Fixup platforms 132 | if err := fixupPlatforms(ctx, baseImage, relocationMap, &fixupInfo, sourceFetcher, platformFilter); err != nil { 133 | return notifyError(notifyEvent, err) 134 | } 135 | 136 | // Prepare and run the copier 137 | cleaner, err := makeManifestWalker(ctx, sourceFetcher, notifyEvent, cfg, fixupInfo, progress) 138 | if err != nil { 139 | return notifyError(notifyEvent, err) 140 | } 141 | defer cleaner() 142 | 143 | notifyEvent(FixupEventTypeCopyImageEnd, "", nil) 144 | return nil 145 | } 146 | 147 | func fixupPlatforms(ctx context.Context, 148 | baseImage *bundle.BaseImage, 149 | relocationMap relocation.ImageRelocationMap, 150 | fixupInfo *imageFixupInfo, 151 | sourceFetcher sourceFetcherAdder, 152 | filter platforms.Matcher) error { 153 | 154 | logger := log.G(ctx) 155 | logger.Debugf("Fixup platforms for image %v, with relocation map %v", baseImage, relocationMap) 156 | if filter == nil || 157 | (fixupInfo.resolvedDescriptor.MediaType != ocischemav1.MediaTypeImageIndex && 158 | fixupInfo.resolvedDescriptor.MediaType != images.MediaTypeDockerSchema2ManifestList) { 159 | // no platform filter if platform is empty, or if the descriptor is not an OCI Index / Docker Manifest list 160 | return nil 161 | } 162 | 163 | reader, err := sourceFetcher.Fetch(ctx, fixupInfo.resolvedDescriptor) 164 | if err != nil { 165 | return err 166 | } 167 | defer reader.Close() 168 | 169 | manifestBytes, err := io.ReadAll(reader) 170 | if err != nil { 171 | return err 172 | } 173 | var manifestList typelessManifestList 174 | if err := json.Unmarshal(manifestBytes, &manifestList); err != nil { 175 | return err 176 | } 177 | var validManifests []typelessDescriptor 178 | for _, d := range manifestList.Manifests { 179 | if d.Platform != nil && filter.Match(*d.Platform) { 180 | validManifests = append(validManifests, d) 181 | } 182 | } 183 | if len(validManifests) == 0 { 184 | return fmt.Errorf("no descriptor matching the platform filter found in %q", fixupInfo.sourceRef) 185 | } 186 | manifestList.Manifests = validManifests 187 | manifestBytes, err = json.Marshal(&manifestList) 188 | if err != nil { 189 | return err 190 | } 191 | d := sourceFetcher.Add(manifestBytes) 192 | descriptor := fixupInfo.resolvedDescriptor 193 | descriptor.Digest = d 194 | descriptor.Size = int64(len(manifestBytes)) 195 | fixupInfo.resolvedDescriptor = descriptor 196 | 197 | return nil 198 | } 199 | 200 | func fixupBaseImage(ctx context.Context, name string, baseImage *bundle.BaseImage, cfg fixupConfig) (imageFixupInfo, bool, error) { 201 | // Check image references 202 | if err := checkBaseImage(baseImage); err != nil { 203 | return imageFixupInfo{}, false, fmt.Errorf("invalid image %q for service %q: %s", baseImage.Image, name, err) 204 | } 205 | targetRepoOnly, err := reference.ParseNormalizedNamed(cfg.targetRef.Name()) 206 | if err != nil { 207 | return imageFixupInfo{}, false, err 208 | } 209 | 210 | fixups := []func(context.Context, reference.Named, *bundle.BaseImage, fixupConfig) (imageFixupInfo, bool, bool, error){ 211 | pushByDigest, 212 | resolveImageInRelocationMap, 213 | resolveImage, 214 | pushLocalImage, 215 | } 216 | 217 | var bigErr *multierror.Error 218 | for _, f := range fixups { 219 | info, pushed, ok, err := f(ctx, targetRepoOnly, baseImage, cfg) 220 | if err != nil { 221 | log.G(ctx).Debug(err) 222 | // do not stop trying fixups after the first error. Only report the errors if all fixups were unable to push the image. 223 | bigErr = multierror.Append(bigErr, fmt.Errorf("failed to fixup the image %s for service %q: %v", baseImage.Image, name, err)) 224 | } 225 | if ok { 226 | return info, pushed, nil 227 | } 228 | } 229 | 230 | return imageFixupInfo{}, false, bigErr.ErrorOrNil() 231 | } 232 | 233 | func pushByDigest(ctx context.Context, target reference.Named, baseImage *bundle.BaseImage, cfg fixupConfig) (imageFixupInfo, bool, bool, error) { 234 | if baseImage.Image != "" || !cfg.pushImages { 235 | return imageFixupInfo{}, false, false, nil 236 | } 237 | descriptor, err := pushImageToTarget(ctx, baseImage.Digest, cfg) 238 | if err != nil { 239 | return imageFixupInfo{}, false, false, fmt.Errorf("failed to push digested image %s@%s to target %s: %v", baseImage.Image, baseImage.Digest, target, err) 240 | } 241 | return imageFixupInfo{ 242 | targetRepo: target, 243 | sourceRef: nil, 244 | resolvedDescriptor: descriptor, 245 | }, true, true, nil 246 | } 247 | 248 | func resolveImage(ctx context.Context, target reference.Named, baseImage *bundle.BaseImage, cfg fixupConfig) (imageFixupInfo, bool, bool, error) { 249 | sourceImageRef, err := ref(baseImage.Image) 250 | if err != nil { 251 | return imageFixupInfo{}, false, false, fmt.Errorf("failed to resolve image: invalid source ref %s: %w", baseImage.Image, err) 252 | } 253 | _, descriptor, err := cfg.resolver.Resolve(ctx, sourceImageRef.String()) 254 | if err != nil { 255 | return imageFixupInfo{}, false, false, fmt.Errorf("failed to resolve image %s: %w", sourceImageRef.String(), err) 256 | } 257 | return imageFixupInfo{ 258 | targetRepo: target, 259 | sourceRef: sourceImageRef, 260 | resolvedDescriptor: descriptor, 261 | }, false, true, nil 262 | } 263 | 264 | func resolveImageInRelocationMap(ctx context.Context, target reference.Named, baseImage *bundle.BaseImage, cfg fixupConfig) (imageFixupInfo, bool, bool, error) { 265 | sourceImageRef, err := ref(baseImage.Image) 266 | if err != nil { 267 | return imageFixupInfo{}, false, false, fmt.Errorf("failed to resolve image in relocation map: invalid source ref %s: %v", baseImage.Image, err) 268 | } 269 | relocatedRef, ok := cfg.relocationMap[baseImage.Image] 270 | if !ok { 271 | return imageFixupInfo{}, false, false, nil 272 | } 273 | relocatedImageRef, err := ref(relocatedRef) 274 | if err != nil { 275 | return imageFixupInfo{}, false, false, fmt.Errorf("failed to resolve image in relocation map: invalid target ref %s: %v", relocatedRef, err) 276 | } 277 | _, descriptor, err := cfg.resolver.Resolve(ctx, relocatedImageRef.String()) 278 | if err != nil { 279 | return imageFixupInfo{}, false, false, err 280 | } 281 | return imageFixupInfo{ 282 | targetRepo: target, 283 | sourceRef: sourceImageRef, 284 | resolvedDescriptor: descriptor, 285 | }, false, true, nil 286 | } 287 | 288 | func pushLocalImage(ctx context.Context, target reference.Named, baseImage *bundle.BaseImage, cfg fixupConfig) (imageFixupInfo, bool, bool, error) { 289 | if !cfg.pushImages { 290 | return imageFixupInfo{}, false, false, nil 291 | } 292 | sourceImageRef, err := ref(baseImage.Image) 293 | if err != nil { 294 | return imageFixupInfo{}, false, false, fmt.Errorf("failed to push local image: invalid source ref %s: %v", baseImage.Image, err) 295 | } 296 | descriptor, err := pushImageToTarget(ctx, baseImage.Image, cfg) 297 | if err != nil { 298 | return imageFixupInfo{}, false, false, fmt.Errorf("failed to push local image %s: %v", baseImage.Image, err) 299 | } 300 | return imageFixupInfo{ 301 | targetRepo: target, 302 | sourceRef: sourceImageRef, 303 | resolvedDescriptor: descriptor, 304 | }, true, true, nil 305 | } 306 | 307 | func ref(str string) (reference.Named, error) { 308 | r, err := reference.ParseNormalizedNamed(str) 309 | if err != nil { 310 | return nil, fmt.Errorf("%q is not a valid reference: %v", str, err) 311 | } 312 | return reference.TagNameOnly(r), nil 313 | } 314 | 315 | // pushImageToTarget pushes the image from the local docker daemon store to the target defined in the configuration. 316 | // Docker image cannot be pushed by digest to a registry. So to be able to push the image inside the targeted repository 317 | // the same behaviour than for multi architecture images is used: all the images are tagged for the targeted repository 318 | // and then pushed. 319 | // Every time a new image is pushed under a tag, the previous tagged image will be untagged. But this untagged image 320 | // remains accessible using its digest. So right after pushing it, the image is resolved to grab its digest from the 321 | // registry and can be added to the index. 322 | // The final workflow is then: 323 | // - tag the image to push with targeted reference 324 | // - push the image using a docker `ImageAPIClient` 325 | // - resolve the pushed image to grab its digest 326 | func pushImageToTarget(ctx context.Context, src string, cfg fixupConfig) (ocischemav1.Descriptor, error) { 327 | taggedRef := reference.TagNameOnly(cfg.targetRef) 328 | 329 | if err := cfg.imageClient.ImageTag(ctx, src, cfg.targetRef.String()); err != nil { 330 | return ocischemav1.Descriptor{}, fmt.Errorf("failed to push image %q, make sure the image exists locally: %s", src, err) 331 | } 332 | 333 | if err := pushTaggedImage(ctx, cfg.imageClient, cfg.targetRef, cfg.pushOut); err != nil { 334 | return ocischemav1.Descriptor{}, fmt.Errorf("failed to push image %q: %s", src, err) 335 | } 336 | 337 | _, descriptor, err := cfg.resolver.Resolve(ctx, taggedRef.String()) 338 | if err != nil { 339 | return ocischemav1.Descriptor{}, fmt.Errorf("failed to resolve %q after pushing it: %s", taggedRef, err) 340 | } 341 | 342 | return descriptor, nil 343 | } 344 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![CircleCI](https://circleci.com/gh/cnabio/cnab-to-oci/tree/main.svg?style=svg)](https://circleci.com/gh/cnabio/cnab-to-oci/tree/main) [![Documentation](https://godoc.org/github.com/cnabio/cnab-to-oci/remotes?status.svg)](http://godoc.org/github.com/cnabio/cnab-to-oci/remotes) 2 | 3 | # CNAB to OCI 4 | 5 | The intent of CNAB to OCI is to propose a reference implementation for sharing a 6 | CNAB using an OCI or Docker registry. 7 | 8 | [Jump to the example](#example). 9 | 10 | ## Rationale for this approach 11 | 12 | Goals: 13 | - Package the information from a CNAB [`bundle.json`](https://github.com/deislabs/cnab-spec/blob/main/101-bundle-json.md) into a format that can be stored in container registries. 14 | - Require no or only minor changes to the [OCI specification](https://github.com/opencontainers/image-spec). 15 | - Major changes would take long to get approved. 16 | - Anything that diverges from the current specification will require coordination with registries to ensure compatibility. 17 | - Store all container images required for the CNAB in the same repository and reference them from the same manifest. 18 | - If a user can access the CNAB, they can access all the parts needed to install it. 19 | - Moving a CNAB from one repository to another is atomic. 20 | - Ensure that registries can reason over these CNABs. 21 | - Provide enough information for registries to understand how to present these artifacts. 22 | 23 | Non-goals: 24 | - A perfectly clean solution. 25 | - The authors acknowledge that there is a tension between getting something working today and the ideal solution. 26 | 27 | ### Selection of OCI index 28 | 29 | The CNAB specification references a 30 | [list of invocation images](https://github.com/deislabs/cnab-spec/blob/main/101-bundle-json.md#invocation-images) 31 | and a 32 | [map of other images](https://github.com/deislabs/cnab-spec/blob/main/101-bundle-json.md#the-image-map). 33 | An [OCI index](#what-is-an-oci-index) is already used for handling multiple 34 | images so this was seen as a natural fit. 35 | 36 | The only disadvantage of the OCI index is its lack of a top-level 37 | mechanism for communicating type which may make the artifacts more difficult for 38 | registries to understand. The authors propose overcoming this using 39 | [annotations](#annotations). 40 | 41 | ### Annotations 42 | 43 | [Annotations](https://github.com/opencontainers/image-spec/blob/master/annotations.md) 44 | are an optional part of the OCI specification. They can be included in the 45 | top-level of an OCI index, at the top-level of a manifest, or as part of a 46 | descriptor. 47 | 48 | While they are generally unrestricted key-value pairs, [some guidance is given 49 | for keys by the OCI](https://github.com/opencontainers/image-spec/blob/master/annotations.md#pre-defined-annotation-keys). 50 | 51 | Formalising some common annotations across various artifact types will make them 52 | useful for registries to use to better understand the artifacts. 53 | 54 | ### The future 55 | 56 | There is a clear trend towards more types of artifacts being stored in container 57 | registries. While it's too early to predict exactly what will be stored in 58 | registries, a couple of observations can be made. 59 | 60 | Just as how OCI indices evolved from a need, it's likely that the OCI will 61 | specify new schemas and media types once there are several new artifacts being 62 | stored in registries. This needs to be done, in part, with hindsight so that the 63 | specification captures the requirements for storing all artifacts. 64 | 65 | It's likely that other artifacts will also want to reference multiple images 66 | and/or artifacts. This means that the approach of using an OCI index for a 67 | single object is likely to continue to be valid and useful. 68 | 69 | Agreeing upon and using several common annotations will provide a solid 70 | foundation for future specification work. If Helm, CNAB, and whatever comes next 71 | find annotations that work well, these can be promoted to fields of the next OCI 72 | specification. 73 | 74 | ## FAQ 75 | 76 | ### What is CNAB? 77 | 78 | CNAB stands for Cloud Native Application Bundle. It aims to be the equivalent of 79 | a deb (or MSI) package but for all things Cloud Native. See 80 | [this site](https://cnab.io) for more. 81 | 82 | ### What is an OCI index? 83 | 84 | A container image is presented by a registry as a 85 | [manifest](https://github.com/opencontainers/image-spec/blob/master/manifest.md). 86 | Each manifest is platform specific which means that in order to use an image on 87 | multiple platforms, one needs to fetch the correct manifest for that platform. 88 | 89 | Initially this was solved by indicating the platform as part of the tag, e.g.: 90 | `myimage:tag-`. This is undesirable for base images used on multiple 91 | platforms as it requires platform specific code. As such a manifest list was 92 | added where multiple manifests could be presented behind the same code. 93 | 94 | The client can fetch the manifest list (or 95 | [OCI index](https://github.com/opencontainers/image-spec/blob/master/image-index.md)) 96 | and match its platform to those presented so that it gets the correct image 97 | manifest. Registries are content addressable so the manifest can be found using 98 | the digest. 99 | 100 | An example of this is the `golang:alpine` image, note that a Docker manifest 101 | list is the older version of an OCI index and they serve the same purpose: 102 | 103 | ```console 104 | $ DOCKER_CLI_EXPERIMENTAL=enabled docker manifest inspect golang:alpine 105 | { 106 | "schemaVersion": 2, 107 | "mediaType": "application/vnd.docker.distribution.manifest.list.v2+json", 108 | "manifests": [ 109 | { 110 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 111 | "size": 1365, 112 | "digest": "sha256:9ba4afd1011b9151c3967651538b600f19e48eff2ddde987feb2b72ab2c0bb69", 113 | "platform": { 114 | "architecture": "amd64", 115 | "os": "linux" 116 | } 117 | }, 118 | { 119 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 120 | "size": 1572, 121 | "digest": "sha256:89cc8193f7abc4237b0df2417e0b9fa61687017cd507456b21241d9ea4d94dd3", 122 | "platform": { 123 | "architecture": "arm", 124 | "os": "linux", 125 | "variant": "v6" 126 | } 127 | }, 128 | { 129 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 130 | "size": 1572, 131 | "digest": "sha256:6803d818bb3dd6edfccbe35b70477483fc75ed11d925e80e4af443f737146328", 132 | "platform": { 133 | "architecture": "arm64", 134 | "os": "linux", 135 | "variant": "v8" 136 | } 137 | }, 138 | { 139 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 140 | "size": 1572, 141 | "digest": "sha256:923201b72b9dcf9e96290f9f171c34a9c743047d707afe69d9c167d430607db7", 142 | "platform": { 143 | "architecture": "386", 144 | "os": "linux" 145 | } 146 | }, 147 | { 148 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 149 | "size": 1572, 150 | "digest": "sha256:1782df1f6fa4ede250547f7aa491d3d11fa974bc62a1b9b0e493f07c3ba4430f", 151 | "platform": { 152 | "architecture": "ppc64le", 153 | "os": "linux" 154 | } 155 | }, 156 | { 157 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json", 158 | "size": 1572, 159 | "digest": "sha256:d50b32798b5e99eb046ee557c567c83d25e39cbaf42f1d4f24af708644a69123", 160 | "platform": { 161 | "architecture": "s390x", 162 | "os": "linux" 163 | } 164 | } 165 | ] 166 | } 167 | ``` 168 | 169 | ## Getting started 170 | 171 | Clone the project into your GOPATH. You can then build it using: 172 | 173 | ```console 174 | $ make 175 | ``` 176 | 177 | You should now have a `cnab-to-oci` binary the `bin/` folder. To run it, 178 | execute: 179 | 180 | ```console 181 | $ bin/cnab-to-oci --help 182 | ``` 183 | 184 | ### Prerequisites 185 | 186 | - Make 187 | - Golang 1.9+ 188 | - Git 189 | 190 | ### Installing 191 | 192 | For installing, make sure your `$GOPATH/bin` is part of your `$PATH`. 193 | 194 | ```console 195 | $ make install 196 | ``` 197 | 198 | This will build and install `cnab-to-oci` into `$GOPATH/bin`. 199 | 200 | ### Usage 201 | 202 | The `cnab-to-oci` binary is a demonstration tool to `push` and `pull` a CNAB 203 | to a registry. It has three commands: `push`, `pull` and `fixup` which are 204 | described in the following sections. 205 | 206 | #### Push 207 | 208 | The `push` command packages a `bundle.json` file into an OCI image index 209 | (falling back to a Docker manifest if the registry does not support this) and 210 | pushes this to the registry. As part of this process, [`fixup`](#fixup) process 211 | is implicitly run. 212 | 213 | ```console 214 | $ bin/cnab-to-oci push examples/helloworld-cnab/bundle.json --target myhubusername/repo 215 | Ensuring image cnab/helloworld:0.1.1 is present in repository docker.io/myhubusername/repo 216 | Image is not present in repository 217 | Mounting descriptor sha256:bbffe37bb3899b1384bf1483cdcff44bd148d52078b4655e69cd23d534ea043d with media type application/vnd.docker.image.rootfs.diff.tar.gzip (size: 203) 218 | Mounting descriptor sha256:e280b57a032b8bb2ab45f26ea67f42b5d47fd5aca2dd63c5bcdbbd1f753f20b7 with media type application/vnd.docker.image.rootfs.diff.tar.gzip (size: 370) 219 | Mounting descriptor sha256:8e3ba11ec2a2b39ab372c60c16b421536e50e5ce64a0bc81765c2e38381bcff6 with media type application/vnd.docker.image.rootfs.diff.tar.gzip (size: 2206542) 220 | Mounting descriptor sha256:58e6f39290459b6563b348052b2a1a8cf2a44fac19a80ae0da36c82a32f151f8 with media type application/vnd.docker.container.image.v1+json (size: 2135) 221 | Copying descriptor sha256:a59a4e74d9cc89e4e75dfb2cc7ea5c108e4236ba6231b53081a9e2506d1197b6 with media type application/vnd.docker.distribution.manifest.v2+json (size: 942) 222 | {"errors":[{"code":"MANIFEST_INVALID","message":"manifest invalid","detail":{}}]} 223 | 224 | Pushed successfully, with digest "sha256:6cabd752cb01d2efb9485225baf7fc26f4322c1f45f537f76c5eeb67ba8d83e0" 225 | ``` 226 | 227 | **Note:** if your images -invocation images as well as service images- are not already 228 | pushed on a registry, `cnab-to-oci` will try to resolve them locally and push them 229 | from your docker daemon image store. 230 | 231 | **Note:** The `MANIFEST_INVALID` error in the above case is because the Docker Hub 232 | does not currently support the OCI image index type. 233 | 234 | **Note**: When using the Docker Hub, no tag will show up in the Hub interface. 235 | The artifact must be referenced by its SHA - see [`pull`](#pull). 236 | 237 | #### Pull 238 | 239 | The `pull` command is used to fetch a CNAB packaged as an OCI image index or 240 | Docker manifest from a registry. This must be done using the digest returned by 241 | the [`push`](#push) command. By default the output is saved to `pulled.json`. 242 | 243 | ```console 244 | $ bin/cnab-to-oci pull myhubusername/repo@sha256:6cabd752cb01d2efb9485225baf7fc26f4322c1f45f537f76c5eeb67ba8d83e0 245 | 246 | $ cat pulled.json 247 | { 248 | "name": "helloworld", 249 | "version": "0.1.1", 250 | "description": "A short description of your bundle", 251 | "keywords": [ 252 | "helloworld", 253 | "cnab", 254 | "tutorial" 255 | ], 256 | "maintainers": [ 257 | { 258 | "name": "Jane Doe", 259 | "email": "jane.doe@example.com", 260 | "url": "https://example.com" 261 | } 262 | ], 263 | "invocationImages": [ 264 | { 265 | "imageType": "docker", 266 | "image": "myhubusername/repo@sha256:a59a4e74d9cc89e4e75dfb2cc7ea5c108e4236ba6231b53081a9e2506d1197b6", 267 | "size": 942, 268 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json" 269 | } 270 | ], 271 | "images": null, 272 | "parameters": null, 273 | "credentials": null 274 | } 275 | ``` 276 | 277 | #### Fixup 278 | 279 | The `fixup` command resolves all the image digest references (for the 280 | _invocationImages_ as well as the _images_ in the `bundle.json`) from the 281 | relevant registries and pushes them to the _target_ repository to ensure they're 282 | available to anyone who has access to the CNAB in the target repository. A 283 | patched `bundle.json` is saved by default to `fixed-bundle.json` 284 | 285 | ```console 286 | $ bin/cnab-to-oci fixup examples/helloworld-cnab/bundle.json --target myhubusername/repo 287 | Ensuring image cnab/helloworld:0.1.1 is present in repository docker.io/myhubusername/repo 288 | Image is not present in repository 289 | Mounting descriptor sha256:bbffe37bb3899b1384bf1483cdcff44bd148d52078b4655e69cd23d534ea043d with media type application/vnd.docker.image.rootfs.diff.tar.gzip (size: 203) 290 | Mounting descriptor sha256:e280b57a032b8bb2ab45f26ea67f42b5d47fd5aca2dd63c5bcdbbd1f753f20b7 with media type application/vnd.docker.image.rootfs.diff.tar.gzip (size: 370) 291 | Mounting descriptor sha256:8e3ba11ec2a2b39ab372c60c16b421536e50e5ce64a0bc81765c2e38381bcff6 with media type application/vnd.docker.image.rootfs.diff.tar.gzip (size: 2206542) 292 | Mounting descriptor sha256:58e6f39290459b6563b348052b2a1a8cf2a44fac19a80ae0da36c82a32f151f8 with media type application/vnd.docker.container.image.v1+json (size: 2135) 293 | Copying descriptor sha256:a59a4e74d9cc89e4e75dfb2cc7ea5c108e4236ba6231b53081a9e2506d1197b6 with media type application/vnd.docker.distribution.manifest.v2+json (size: 942) 294 | 295 | $ cat fixed-bundle.json 296 | { 297 | "name": "helloworld", 298 | "version": "0.1.1", 299 | "description": "A short description of your bundle", 300 | "keywords": [ 301 | "helloworld", 302 | "cnab", 303 | "tutorial" 304 | ], 305 | "maintainers": [ 306 | { 307 | "name": "Jane Doe", 308 | "email": "jane.doe@example.com", 309 | "url": "https://example.com" 310 | } 311 | ], 312 | "invocationImages": [ 313 | { 314 | "imageType": "docker", 315 | "image": "myhubusername/repo@sha256:a59a4e74d9cc89e4e75dfb2cc7ea5c108e4236ba6231b53081a9e2506d1197b6", 316 | "size": 942, 317 | "mediaType": "application/vnd.docker.distribution.manifest.v2+json" 318 | } 319 | ], 320 | "images": null, 321 | "parameters": null, 322 | "credentials": null 323 | } 324 | ``` 325 | 326 | **Note:** In the above example, the invocation image reference now matches the 327 | target repository. 328 | 329 | ### Example 330 | 331 | The following is an example of an OCI image index sent to the registry. 332 | 333 | ```json 334 | { 335 | "schemaVersion": 2, 336 | "manifests": [ 337 | { 338 | "mediaType": "application/vnd.oci.image.manifest.v1+json", 339 | "digest": "sha256:d59a1aa7866258751a261bae525a1842c7ff0662d4f34a355d5f36826abc0341", 340 | "size": 285, 341 | "annotations": { 342 | "io.cnab.manifest.type": "config" 343 | } 344 | }, 345 | { 346 | "mediaType": "application/vnd.oci.image.manifest.v1+json", 347 | "digest": "sha256:196d12cf6ab19273823e700516e98eb1910b03b17840f9d5509f03858484d321", 348 | "size": 506, 349 | "annotations": { 350 | "io.cnab.manifest.type": "invocation" 351 | } 352 | }, 353 | { 354 | "mediaType": "application/vnd.oci.image.manifest.v1+json", 355 | "digest": "sha256:6bb891430fb6e2d3b4db41fd1f7ece08c5fc769d8f4823ec33c7c7ba99679213", 356 | "size": 507, 357 | "annotations": { 358 | "io.cnab.component.name": "image-1", 359 | "io.cnab.manifest.type": "component" 360 | } 361 | } 362 | ], 363 | "annotations": { 364 | "io.cnab.keywords": "[\"keyword1\",\"keyword2\"]", 365 | "io.cnab.runtime_version": "v1.0.0", 366 | "org.opencontainers.artifactType": "application/vnd.cnab.manifest.v1", 367 | "org.opencontainers.image.authors": "[{\"name\":\"docker\",\"email\":\"docker@docker.com\",\"url\":\"docker.com\"}]", 368 | "org.opencontainers.image.description": "description", 369 | "org.opencontainers.image.title": "my-app", 370 | "org.opencontainers.image.version": "0.1.0" 371 | } 372 | } 373 | ``` 374 | 375 | The first manifest in the manifest list references the CNAB configuration. An 376 | example of this follows: 377 | 378 | ```json 379 | { 380 | "schemaVersion": 2, 381 | "config": { 382 | "mediaType": "application/vnd.cnab.config.v1+json", 383 | "digest": "sha256:4bc453b53cb3d914b45f4b250294236adba2c0e09ff6f03793949e7e39fd4cc1", 384 | "size": 578 385 | }, 386 | "layers": [] 387 | } 388 | ``` 389 | 390 | Subsequent manifests in the manifest list are standard OCI images. 391 | 392 | This example proposes two OCI specification and registry changes: 393 | 1. It proposes the addition of an `org.opencontainers.artifactType` annotation to be included in the OCI specification. 394 | 1. It requires that registries support the `application/vnd.cnab.config.v1+json` media type for a config type. 395 | 396 | ## Development 397 | 398 | ### Running the tests 399 | 400 | ```console 401 | $ make test 402 | ``` 403 | 404 | ### Running the e2e tests 405 | 406 | ```console 407 | $ make e2e 408 | ``` 409 | 410 | ## Contributing 411 | 412 | Please read [CONTRIBUTING.md](CONTRIBUTING.md) for details on our code of 413 | conduct, and the process for submitting pull requests to us. 414 | 415 | ## Maintainers 416 | 417 | See also the list of [maintainers](MAINTAINERS) who participated in this 418 | project. 419 | 420 | ## Contributors 421 | 422 | See also the list of 423 | [contributors](https://github.com/cnabio/cnab-to-oci/graphs/contributors) who 424 | participated in this project. 425 | 426 | ## License 427 | 428 | This project is licensed under the Apache 2 License - see the [LICENSE](LICENSE) 429 | file for details. 430 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6 h1:He8afgbRMd7mFxO99hRNu+6tazq8nFF9lIwo9JFroBk= 2 | github.com/AdaLogics/go-fuzz-headers v0.0.0-20240806141605-e8a1dd7889d6/go.mod h1:8o94RPi1/7XTJvwPpRSzSUedZrtlirdB3r9Z20bi2f8= 3 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= 4 | github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= 5 | github.com/Masterminds/semver v1.5.0 h1:H65muMkzWKEuNDnfl9d70GUjFniHKHRbFPGBuZ3QEww= 6 | github.com/Masterminds/semver v1.5.0/go.mod h1:MB6lktGJrhw8PrUyiEoblNEGEQ+RzHPF078ddwwvV3Y= 7 | github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= 8 | github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= 9 | github.com/Microsoft/hcsshim v0.12.9 h1:2zJy5KA+l0loz1HzEGqyNnjd3fyZA31ZBCGKacp6lLg= 10 | github.com/Microsoft/hcsshim v0.12.9/go.mod h1:fJ0gkFAna6ukt0bLdKB8djt4XIJhF/vEPuoIWYVvZ8Y= 11 | github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= 12 | github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= 13 | github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= 14 | github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= 15 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 16 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 17 | github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= 18 | github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= 19 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 20 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 21 | github.com/cnabio/cnab-go v0.25.5 h1:vk61+Xd4mPn3M/WDq3ptoBlH4JnDUj0HyiQcvAEVWRE= 22 | github.com/cnabio/cnab-go v0.25.5/go.mod h1:Ebr7Jdx8aVaDjkzXlx+LhG0sqD+jV1gnWAKfBuxn57k= 23 | github.com/containerd/cgroups v1.1.0 h1:v8rEWFl6EoqHB+swVNjVoCJE8o3jX7e8nqBGPLaDFBM= 24 | github.com/containerd/cgroups/v3 v3.0.3 h1:S5ByHZ/h9PMe5IOQoN7E+nMc2UcLEM/V48DGDJ9kip0= 25 | github.com/containerd/cgroups/v3 v3.0.3/go.mod h1:8HBe7V3aWGLFPd/k03swSIsGjZhHI2WzJmticMgVuz0= 26 | github.com/containerd/containerd v1.7.30 h1:/2vezDpLDVGGmkUXmlNPLCCNKHJ5BbC5tJB5JNzQhqE= 27 | github.com/containerd/containerd v1.7.30/go.mod h1:fek494vwJClULlTpExsmOyKCMUAbuVjlFsJQc4/j44M= 28 | github.com/containerd/continuity v0.4.4 h1:/fNVfTJ7wIl/YPMHjf+5H32uFhl63JucB34PlCpMKII= 29 | github.com/containerd/continuity v0.4.4/go.mod h1:/lNJvtJKUQStBzpVQ1+rasXO1LAWtUQssk28EZvJ3nE= 30 | github.com/containerd/errdefs v1.0.0 h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI= 31 | github.com/containerd/errdefs v1.0.0/go.mod h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M= 32 | github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE= 33 | github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= 34 | github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= 35 | github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= 36 | github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= 37 | github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= 38 | github.com/containerd/typeurl v1.0.2 h1:Chlt8zIieDbzQFzXzAeBEF92KhExuE4p9p92/QmY7aY= 39 | github.com/containerd/typeurl/v2 v2.2.0 h1:6NBDbQzr7I5LHgp34xAXYF5DOTQDn05X58lsPEmzLso= 40 | github.com/containerd/typeurl/v2 v2.2.0/go.mod h1:8XOOxnyatxSWuG8OfsZXVnAF4iZfedjS/8UHSPJnX4g= 41 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 42 | github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= 43 | github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= 44 | github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467 h1:uX1JmpONuD549D73r6cgnxyUu18Zb7yHAy5AYU0Pm4Q= 45 | github.com/cyberphone/json-canonicalization v0.0.0-20241213102144-19d51d7fe467/go.mod h1:uzvlm1mxhHkdfqitSA92i7Se+S9ksOn3a3qmv/kyOCw= 46 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 47 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 48 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 49 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 50 | github.com/distribution/distribution v2.8.3+incompatible h1:RlpEXBLq/WPXYvBYMDAmBX/SnhD67qwtvW/DzKc8pAo= 51 | github.com/distribution/distribution v2.8.3+incompatible/go.mod h1:EgLm2NgWtdKgzF9NpMzUKgzmR7AMmb0VQi2B+ZzDRjc= 52 | github.com/distribution/reference v0.6.1-0.20240718132515-8c942b0459df h1:5dsN0RDoKdzqhxBIJ/fJxZGAtcAbkCd6FUPtkao6ROM= 53 | github.com/distribution/reference v0.6.1-0.20240718132515-8c942b0459df/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= 54 | github.com/docker/cli v29.1.3+incompatible h1:+kz9uDWgs+mAaIZojWfFt4d53/jv0ZUOOoSh5ZnH36c= 55 | github.com/docker/cli v29.1.3+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= 56 | github.com/docker/distribution v2.8.3+incompatible h1:AtKxIZ36LoNK51+Z6RpzLpddBirtxJnzDrHLEKxTAYk= 57 | github.com/docker/distribution v2.8.3+incompatible/go.mod h1:J2gT2udsDAN96Uj4KfcMRqY0/ypR+oyYUYmja8H+y+w= 58 | github.com/docker/docker v28.5.2+incompatible h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM= 59 | github.com/docker/docker v28.5.2+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= 60 | github.com/docker/docker-credential-helpers v0.9.3 h1:gAm/VtF9wgqJMoxzT3Gj5p4AqIjCBS4wrsOh9yRqcz8= 61 | github.com/docker/docker-credential-helpers v0.9.3/go.mod h1:x+4Gbw9aGmChi3qTLZj8Dfn0TD20M/fuWy0E5+WDeCo= 62 | github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= 63 | github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= 64 | github.com/docker/go-metrics v0.0.1 h1:AgB/0SvBxihN0X8OR4SjsblXkbMvalQ8cjmtKQ2rQV8= 65 | github.com/docker/go-metrics v0.0.1/go.mod h1:cG1hvH2utMXtqgqqYE9plW6lDxS3/5ayHzueweSI3Vw= 66 | github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= 67 | github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= 68 | github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7 h1:UhxFibDNY/bfvqU5CAUmr9zpesgbU6SWc8/B4mflAE4= 69 | github.com/docker/libtrust v0.0.0-20160708172513-aabc10ec26b7/go.mod h1:cyGadeNEkKy96OOhEzfZl+yxihPEzKnqJwvfuSUqbZE= 70 | github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= 71 | github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= 72 | github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= 73 | github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= 74 | github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= 75 | github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= 76 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 77 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 78 | github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= 79 | github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= 80 | github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= 81 | github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= 82 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 83 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 84 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 85 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 86 | github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 87 | github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 88 | github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 89 | github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= 90 | github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= 91 | github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 92 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 93 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 94 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 95 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 96 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 97 | github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= 98 | github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= 99 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0 h1:TmHmbvxPmaegwhDubVz0lICL0J5Ka2vwTzhoePEXsGE= 100 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.24.0/go.mod h1:qztMSjm835F2bXf+5HKAPIS5qsmQDqZna/PgVt4rWtI= 101 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 102 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 103 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 104 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 105 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 106 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 107 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 108 | github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= 109 | github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= 110 | github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= 111 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 112 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 113 | github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= 114 | github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= 115 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 116 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 117 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 118 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 119 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 120 | github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= 121 | github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= 122 | github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= 123 | github.com/moby/locker v1.0.1 h1:fOXqR41zeveg4fFODix+1Ch4mj/gT0NE1XJbp/epuBg= 124 | github.com/moby/locker v1.0.1/go.mod h1:S7SDdo5zpBK84bzzVlKr2V0hz+7x9hWbYC/kq7oQppc= 125 | github.com/moby/sys/atomicwriter v0.1.0 h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw= 126 | github.com/moby/sys/atomicwriter v0.1.0/go.mod h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs= 127 | github.com/moby/sys/mountinfo v0.6.2 h1:BzJjoreD5BMFNmD9Rus6gdd1pLuecOFPt8wC+Vygl78= 128 | github.com/moby/sys/mountinfo v0.6.2/go.mod h1:IJb6JQeOklcdMU9F5xQ8ZALD+CUr5VlGpwtX+VE0rpI= 129 | github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= 130 | github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= 131 | github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g= 132 | github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= 133 | github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= 134 | github.com/moby/term v0.5.2/go.mod h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc= 135 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 136 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 137 | github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 138 | github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= 139 | github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= 140 | github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= 141 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 142 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 143 | github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= 144 | github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= 145 | github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= 146 | github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= 147 | github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= 148 | github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 149 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 150 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 151 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 152 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 153 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 154 | github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= 155 | github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= 156 | github.com/prometheus/client_golang v1.1.0/go.mod h1:I1FGZT9+L76gKKOs5djB6ezCbFQP1xR9D75/vuwEF3g= 157 | github.com/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y= 158 | github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE= 159 | github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= 160 | github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= 161 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 162 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 163 | github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= 164 | github.com/prometheus/common v0.6.0/go.mod h1:eBmuwkDJBwy6iBfxCBob6t6dR6ENT/y+J+Zk0j9GMYc= 165 | github.com/prometheus/common v0.61.0 h1:3gv/GThfX0cV2lpO7gkTUwZru38mxevy90Bj8YFSRQQ= 166 | github.com/prometheus/common v0.61.0/go.mod h1:zr29OCN/2BsJRaFwG8QOBr41D6kkchKbpeNH7pAjb/s= 167 | github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= 168 | github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= 169 | github.com/prometheus/procfs v0.0.3/go.mod h1:4A/X28fw3Fc593LaREMrKMqOKvUAntwMDaekg4FpcdQ= 170 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 171 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 172 | github.com/qri-io/jsonpointer v0.1.1 h1:prVZBZLL6TW5vsSB9fFHFAMBLI4b0ri5vribQlTJiBA= 173 | github.com/qri-io/jsonpointer v0.1.1/go.mod h1:DnJPaYgiKu56EuDp8TU5wFLdZIcAnb/uH9v37ZaMV64= 174 | github.com/qri-io/jsonschema v0.2.2-0.20210831022256-780655b2ba0e h1:gqHzseevuZPr3oOLES1nrPO3exQfeTKUiPcJub5axVs= 175 | github.com/qri-io/jsonschema v0.2.2-0.20210831022256-780655b2ba0e/go.mod h1:g7DPkiOsK1xv6T/Ao5scXRkd+yTFygcANPBaaqW+VrI= 176 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 177 | github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= 178 | github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= 179 | github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= 180 | github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= 181 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 182 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 183 | github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= 184 | github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= 185 | github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= 186 | github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 187 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 188 | github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 189 | github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 190 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 191 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 192 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 193 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 194 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 195 | github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 196 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb h1:zGWFAtiMcyryUHoUjUJX0/lt1H2+i2Ka2n+D3DImSNo= 197 | github.com/xeipuuv/gojsonpointer v0.0.0-20190905194746-02993c407bfb/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= 198 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 h1:EzJWgHovont7NscjpAxXsDA8S8BMYve8Y5+7cuRE7R0= 199 | github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= 200 | github.com/xeipuuv/gojsonschema v1.2.0 h1:LhYJRs+L4fBtjZUfuSZIKGeVu0QRy8e5Xi7D17UxZ74= 201 | github.com/xeipuuv/gojsonschema v1.2.0/go.mod h1:anYRn/JVcOK2ZgGU+IjEV4nwlhoK5sQluxsYJ78Id3Y= 202 | go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= 203 | go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= 204 | go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= 205 | go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= 206 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0 h1:yd02MEjBdJkG3uabWP9apV+OuWRIXGDuJEUJbOHmCFU= 207 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.58.0/go.mod h1:umTcuxiv1n/s/S6/c2AT/g2CQ7u5C59sHDNmfSwgz7Q= 208 | go.opentelemetry.io/otel v1.33.0 h1:/FerN9bax5LoK51X/sI0SVYrjSE0/yUL7DpxW4K3FWw= 209 | go.opentelemetry.io/otel v1.33.0/go.mod h1:SUUkR6csvUQl+yjReHu5uM3EtVV7MBm5FHKRlNx4I8I= 210 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0 h1:Vh5HayB/0HHfOQA7Ctx69E/Y/DcQSMPpKANYVMQ7fBA= 211 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.33.0/go.mod h1:cpgtDBaqD/6ok/UG0jT15/uKjAY8mRA53diogHBg3UI= 212 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0 h1:cMyu9O88joYEaI47CnQkxO1XZdpoTF9fEnW2duIddhw= 213 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.32.0/go.mod h1:6Am3rn7P9TVVeXYG+wtcGE7IE1tsQ+bP3AuWcKt/gOI= 214 | go.opentelemetry.io/otel/metric v1.33.0 h1:r+JOocAyeRVXD8lZpjdQjzMadVZp2M4WmQ+5WtEnklQ= 215 | go.opentelemetry.io/otel/metric v1.33.0/go.mod h1:L9+Fyctbp6HFTddIxClbQkjtubW6O9QS3Ann/M82u6M= 216 | go.opentelemetry.io/otel/sdk v1.33.0 h1:iax7M131HuAm9QkZotNHEfstof92xM+N8sr3uHXc2IM= 217 | go.opentelemetry.io/otel/sdk v1.33.0/go.mod h1:A1Q5oi7/9XaMlIWzPSxLRWOI8nG3FnzHJNbiENQuihM= 218 | go.opentelemetry.io/otel/trace v1.33.0 h1:cCJuF7LRjUFso9LPnEAHJDB2pqzp+hbO8eu1qqW2d/s= 219 | go.opentelemetry.io/otel/trace v1.33.0/go.mod h1:uIcdVUZMpTAmz0tI1z04GoVSezK37CbGV4fr1f2nBck= 220 | go.opentelemetry.io/proto/otlp v1.4.0 h1:TA9WRvW6zMwP+Ssb6fLoUIuirti1gGbP28GcKG1jgeg= 221 | go.opentelemetry.io/proto/otlp v1.4.0/go.mod h1:PPBWZIP98o2ElSqI35IHfu7hIhSwvc5N38Jw8pXuGFY= 222 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 223 | golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= 224 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 225 | golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= 226 | golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 227 | golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= 228 | golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= 229 | golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 230 | golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 231 | golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= 232 | golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 233 | golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 234 | golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 235 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 236 | golang.org/x/sys v0.0.0-20190801041406-cbf593c0f2f3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 237 | golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 238 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 239 | golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc= 240 | golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 241 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 242 | golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM= 243 | golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM= 244 | golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE= 245 | golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= 246 | google.golang.org/genproto v0.0.0-20240123012728-ef4313101c80 h1:KAeGQVN3M9nD0/bQXnr/ClcEMJ968gUXJQ9pwfSynuQ= 247 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576 h1:CkkIfIt50+lT6NHAVoRYEyAvQGFM7xEwXUUywFvEb3Q= 248 | google.golang.org/genproto/googleapis/api v0.0.0-20241209162323-e6fa225c2576/go.mod h1:1R3kvZ1dtP3+4p4d3G8uJ8rFk/fWlScl38vanWACI08= 249 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422 h1:3UsHvIr4Wc2aW4brOaSCmcxh9ksica6fHEr8P1XhkYw= 250 | google.golang.org/genproto/googleapis/rpc v0.0.0-20250106144421-5f5ef82da422/go.mod h1:3ENsm/5D1mzDyhpzeRi1NR784I0BcofWBoSc5QqqMK4= 251 | google.golang.org/grpc v1.69.2 h1:U3S9QEtbXC0bYNvRtcoklF3xGtLViumSYxWykJS+7AU= 252 | google.golang.org/grpc v1.69.2/go.mod h1:vyjdE6jLBI76dgpDojsFGNaHlxdjXN9ghpnd2o7JGZ4= 253 | google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM= 254 | google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE= 255 | gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= 256 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 257 | gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 258 | gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 259 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 260 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 261 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 262 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 263 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 264 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 265 | gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= 266 | gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= 267 | --------------------------------------------------------------------------------