diff --git a/internal/ocipush/push.go b/internal/ocipush/push.go index 72c774d85..8f632a878 100644 --- a/internal/ocipush/push.go +++ b/internal/ocipush/push.go @@ -34,6 +34,28 @@ import ( v1 "github.com/opencontainers/image-spec/specs-go/v1" ) +const ( + // ComposeProjectArtifactType is the OCI 1.1-compliant artifact type value + // for the generated image manifest. + ComposeProjectArtifactType = "application/vnd.docker.compose.project" + // ComposeYAMLMediaType is the media type for each layer (Compose file) + // in the image manifest. + ComposeYAMLMediaType = "application/vnd.docker.compose.file+yaml" + // ComposeEmptyConfigMediaType is a media type used for the config descriptor + // when doing OCI 1.0-style pushes. + // + // The content is always `{}`, the same as a normal empty descriptor, but + // the specific media type allows clients to fall back to the config media + // type to recognize the manifest as a Compose project since the artifact + // type field is not available in OCI 1.0. + // + // This is based on guidance from the OCI 1.1 spec: + // > Implementers note: artifacts have historically been created without + // > an artifactType field, and tooling to work with artifacts should + // > fallback to the config.mediaType value. + ComposeEmptyConfigMediaType = "application/vnd.docker.compose.config.empty.v1+json" +) + // clientAuthStatusCodes are client (4xx) errors that are authentication // related. var clientAuthStatusCodes = []int{ @@ -49,7 +71,7 @@ type Pushable struct { func DescriptorForComposeFile(path string, content []byte) v1.Descriptor { return v1.Descriptor{ - MediaType: "application/vnd.docker.compose.file+yaml", + MediaType: ComposeYAMLMediaType, Digest: digest.FromString(string(content)), Size: int64(len(content)), Annotations: map[string]string{ @@ -133,12 +155,19 @@ func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion) ([]Pusha var artifactType string switch ociCompat { case api.OCIVersion1_0: - configData, err := json.Marshal(v1.ImageConfig{}) - if err != nil { - return nil, err - } + // "Content other than OCI container images MAY be packaged using the image manifest. + // When this is done, the config.mediaType value MUST be set to a value specific to + // the artifact type or the empty value." + // Source: https://github.com/opencontainers/image-spec/blob/main/manifest.md#guidelines-for-artifact-usage + // + // The `ComposeEmptyConfigMediaType` is used specifically for this purpose: + // there is no config, and an empty descriptor is used for OCI 1.1 in + // conjunction with the `ArtifactType`, but for OCI 1.0 compatibility, + // tooling falls back to the config media type, so this is used to + // indicate that it's not a container image but custom content. + configData := []byte("{}") config = v1.Descriptor{ - MediaType: v1.MediaTypeImageConfig, + MediaType: ComposeEmptyConfigMediaType, Digest: digest.FromBytes(configData), Size: int64(len(configData)), } @@ -148,7 +177,7 @@ func generateManifest(layers []v1.Descriptor, ociCompat api.OCIVersion) ([]Pusha toPush = append(toPush, Pushable{Descriptor: config, Data: configData}) case api.OCIVersion1_1: config = v1.DescriptorEmptyJSON - artifactType = "application/vnd.docker.compose.project" + artifactType = ComposeProjectArtifactType // N.B. the descriptor has the data embedded in it toPush = append(toPush, Pushable{Descriptor: config, Data: nil}) default: