Updates oci image config to support upstream ArgsEscaped

ArgsEscaped has now been merged into upstream OCI image spec.
This change removes the workaround we were doing in containerd
to deserialize the extra json outside of the spec and instead
just uses the formal spec types.

Signed-off-by: Justin Terry <jlterry@amazon.com>
This commit is contained in:
Justin Terry 2022-10-05 15:11:35 -07:00
parent 31f9d13f0c
commit d4b9dade13
14 changed files with 95 additions and 73 deletions

2
go.mod
View File

@ -44,7 +44,7 @@ require (
github.com/moby/sys/signal v0.7.0 github.com/moby/sys/signal v0.7.0
github.com/moby/sys/symlink v0.2.0 github.com/moby/sys/symlink v0.2.0
github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.0.3-0.20220303224323-02efb9a75ee1 github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b
github.com/opencontainers/runc v1.1.4 github.com/opencontainers/runc v1.1.4
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417
github.com/opencontainers/selinux v1.10.1 github.com/opencontainers/selinux v1.10.1

5
go.sum
View File

@ -759,8 +759,8 @@ github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zM
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.2-0.20211117181255-693428a734f5/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.0.3-0.20220303224323-02efb9a75ee1 h1:9iFHD5Kt9hkOfeawBNiEeEaV7bmC4/Z5wJp8E9BptMs= github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8=
github.com/opencontainers/image-spec v1.0.3-0.20220303224323-02efb9a75ee1/go.mod h1:K/JAU0m27RFhDRX4PcFdIKntROP6y5Ed6O91aZYDQfs= github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U= github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
@ -846,7 +846,6 @@ github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6So
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k=
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=

View File

@ -12,7 +12,7 @@ require (
github.com/containerd/typeurl v1.0.3-0.20220422153119-7f6e6d160d67 github.com/containerd/typeurl v1.0.3-0.20220422153119-7f6e6d160d67
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/opencontainers/go-digest v1.0.0 github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.0.3-0.20220303224323-02efb9a75ee1 github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b
github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417
github.com/sirupsen/logrus v1.8.1 github.com/sirupsen/logrus v1.8.1
github.com/stretchr/testify v1.8.0 github.com/stretchr/testify v1.8.0

View File

@ -530,8 +530,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0= github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/opencontainers/image-spec v1.0.3-0.20220303224323-02efb9a75ee1 h1:9iFHD5Kt9hkOfeawBNiEeEaV7bmC4/Z5wJp8E9BptMs= github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b h1:YWuSjZCQAPM8UUBLkYUk1e+rZcvWHJmFb6i6rM44Xs8=
github.com/opencontainers/image-spec v1.0.3-0.20220303224323-02efb9a75ee1/go.mod h1:K/JAU0m27RFhDRX4PcFdIKntROP6y5Ed6O91aZYDQfs= github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b/go.mod h1:3OVijpioIKYWTqjiG0zfF6wvoJ4fAXGbjdZuI2NgsRQ=
github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= github.com/opencontainers/runc v1.0.2/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=
github.com/opencontainers/runc v1.0.3/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0= github.com/opencontainers/runc v1.0.3/go.mod h1:aTaHFFwQXuA71CiyxOdFFIorAoemI04suvGRQFzWTD0=
github.com/opencontainers/runc v1.1.2/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc= github.com/opencontainers/runc v1.1.2/go.mod h1:Tj1hFw6eFWp/o33uxGf5yF2BX5yz2Z6iptFpuvbbKqc=

View File

@ -406,22 +406,6 @@ func WithImageConfigArgs(image Image, args []string) SpecOpts {
// even if there is no specified user in the image config // even if there is no specified user in the image config
return WithAdditionalGIDs("root")(ctx, client, c, s) return WithAdditionalGIDs("root")(ctx, client, c, s)
} else if s.Windows != nil { } else if s.Windows != nil {
// imageExtended is a superset of the oci Image struct that changes
// the Config type to be imageConfigExtended in order to add the
// ability to deserialize `ArgsEscaped` which is not an OCI field,
// but is supported by Docker built images.
type imageExtended struct {
Config struct {
ArgsEscaped bool `json:"ArgsEscaped,omitempty"`
}
}
// Deserialize the extended image format for Windows.
var ociImageExtended imageExtended
if err := json.Unmarshal(imageConfigBytes, &ociImageExtended); err != nil {
return err
}
argsEscaped := ociImageExtended.Config.ArgsEscaped
s.Process.Env = replaceOrAppendEnvValues(config.Env, s.Process.Env) s.Process.Env = replaceOrAppendEnvValues(config.Env, s.Process.Env)
// To support Docker ArgsEscaped on Windows we need to combine the // To support Docker ArgsEscaped on Windows we need to combine the
@ -462,7 +446,7 @@ func WithImageConfigArgs(image Image, args []string) SpecOpts {
return errors.New("no arguments specified") return errors.New("no arguments specified")
} }
if argsEscaped && (len(config.Entrypoint) > 0 || cmdFromImage) { if config.ArgsEscaped && (len(config.Entrypoint) > 0 || cmdFromImage) {
s.Process.Args = nil s.Process.Args = nil
s.Process.CommandLine = cmd[0] s.Process.CommandLine = cmd[0]
if len(cmd) > 1 { if len(cmd) > 1 {

View File

@ -18,13 +18,11 @@ package oci
import ( import (
"context" "context"
"encoding/json"
"testing" "testing"
"github.com/containerd/containerd/containers" "github.com/containerd/containerd/containers"
"github.com/containerd/containerd/namespaces" "github.com/containerd/containerd/namespaces"
"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1" ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"github.com/opencontainers/runtime-spec/specs-go" "github.com/opencontainers/runtime-spec/specs-go"
) )
@ -114,36 +112,6 @@ func TestWithWindowNetworksAllowUnqualifiedDNSQuery(t *testing.T) {
} }
} }
func newFakeArgsEscapedImage(config ocispec.ImageConfig) (Image, error) {
type imageExtended struct {
Config struct {
ocispec.ImageConfig
ArgsEscaped bool `json:"ArgsEscaped,omitempty"`
}
}
// Copy to extended format.
configExtended := imageExtended{}
configExtended.Config.ImageConfig = config
configExtended.Config.ArgsEscaped = true
configBlob, err := json.Marshal(configExtended)
if err != nil {
return nil, err
}
configDescriptor := ocispec.Descriptor{
MediaType: ocispec.MediaTypeImageConfig,
Digest: digest.NewDigestFromBytes(digest.SHA256, configBlob),
}
return fakeImage{
config: configDescriptor,
blobs: map[string]blob{
configDescriptor.Digest.String(): configBlob,
},
}, nil
}
// TestWithProcessArgsOverwritesWithImage verifies that when calling // TestWithProcessArgsOverwritesWithImage verifies that when calling
// WithImageConfig followed by WithProcessArgs when `ArgsEscaped==false` that // WithImageConfig followed by WithProcessArgs when `ArgsEscaped==false` that
// the process args overwrite the image args. // the process args overwrite the image args.
@ -152,8 +120,9 @@ func TestWithProcessArgsOverwritesWithImage(t *testing.T) {
img, err := newFakeImage(ocispec.Image{ img, err := newFakeImage(ocispec.Image{
Config: ocispec.ImageConfig{ Config: ocispec.ImageConfig{
Entrypoint: []string{"powershell.exe", "-Command", "Write-Host Hello"}, Entrypoint: []string{"powershell.exe", "-Command", "Write-Host Hello"},
Cmd: []string{"cmd.exe", "/S", "/C", "echo Hello"}, Cmd: []string{"cmd.exe", "/S", "/C", "echo Hello"},
ArgsEscaped: false,
}, },
}) })
if err != nil { if err != nil {
@ -192,9 +161,12 @@ func TestWithProcessArgsOverwritesWithImage(t *testing.T) {
func TestWithProcessArgsOverwritesWithImageArgsEscaped(t *testing.T) { func TestWithProcessArgsOverwritesWithImageArgsEscaped(t *testing.T) {
t.Parallel() t.Parallel()
img, err := newFakeArgsEscapedImage(ocispec.ImageConfig{ img, err := newFakeImage(ocispec.Image{
Entrypoint: []string{`powershell.exe -Command "C:\My Data\MyExe.exe" -arg1 "-arg2 value2"`}, Config: ocispec.ImageConfig{
Cmd: []string{`cmd.exe /S /C "C:\test path\test.exe"`}, Entrypoint: []string{`powershell.exe -Command "C:\My Data\MyExe.exe" -arg1 "-arg2 value2"`},
Cmd: []string{`cmd.exe /S /C "C:\test path\test.exe"`},
ArgsEscaped: true,
},
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -274,9 +246,12 @@ func TestWithImageOverwritesWithProcessArgs(t *testing.T) {
func TestWithImageArgsEscapedOverwritesWithProcessArgs(t *testing.T) { func TestWithImageArgsEscapedOverwritesWithProcessArgs(t *testing.T) {
t.Parallel() t.Parallel()
img, err := newFakeArgsEscapedImage(ocispec.ImageConfig{ img, err := newFakeImage(ocispec.Image{
Entrypoint: []string{`powershell.exe -Command "C:\My Data\MyExe.exe" -arg1 "-arg2 value2"`}, Config: ocispec.ImageConfig{
Cmd: []string{`cmd.exe /S /C "C:\test path\test.exe"`}, Entrypoint: []string{`powershell.exe -Command "C:\My Data\MyExe.exe" -arg1 "-arg2 value2"`},
Cmd: []string{`cmd.exe /S /C "C:\test path\test.exe"`},
ArgsEscaped: true,
},
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
@ -510,9 +485,12 @@ func TestWithImageConfigArgsEscapedWindows(t *testing.T) {
} }
for _, tc := range testcases { for _, tc := range testcases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
img, err := newFakeArgsEscapedImage(ocispec.ImageConfig{ img, err := newFakeImage(ocispec.Image{
Entrypoint: tc.entrypoint, Config: ocispec.ImageConfig{
Cmd: tc.cmd, Entrypoint: tc.entrypoint,
Cmd: tc.cmd,
ArgsEscaped: true,
},
}) })
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)

View File

@ -59,4 +59,13 @@ const (
// AnnotationBaseImageName is the annotation key for the image reference of the image's base image. // AnnotationBaseImageName is the annotation key for the image reference of the image's base image.
AnnotationBaseImageName = "org.opencontainers.image.base.name" AnnotationBaseImageName = "org.opencontainers.image.base.name"
// AnnotationArtifactCreated is the annotation key for the date and time on which the artifact was built, conforming to RFC 3339.
AnnotationArtifactCreated = "org.opencontainers.artifact.created"
// AnnotationArtifactDescription is the annotation key for the human readable description for the artifact.
AnnotationArtifactDescription = "org.opencontainers.artifact.description"
// AnnotationReferrersFiltersApplied is the annotation key for the comma separated list of filters applied by the registry in the referrers listing.
AnnotationReferrersFiltersApplied = "org.opencontainers.referrers.filtersApplied"
) )

View File

@ -0,0 +1,34 @@
// Copyright 2022 The Linux Foundation
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package v1
// Artifact describes an artifact manifest.
// This structure provides `application/vnd.oci.artifact.manifest.v1+json` mediatype when marshalled to JSON.
type Artifact struct {
// MediaType is the media type of the object this schema refers to.
MediaType string `json:"mediaType"`
// ArtifactType is the IANA media type of the artifact this schema refers to.
ArtifactType string `json:"artifactType"`
// Blobs is a collection of blobs referenced by this manifest.
Blobs []Descriptor `json:"blobs,omitempty"`
// Subject (reference) is an optional link from the artifact to another manifest forming an association between the artifact and the other manifest.
Subject *Descriptor `json:"subject,omitempty"`
// Annotations contains arbitrary metadata for the artifact manifest.
Annotations map[string]string `json:"annotations,omitempty"`
}

View File

@ -48,6 +48,15 @@ type ImageConfig struct {
// StopSignal contains the system call signal that will be sent to the container to exit. // StopSignal contains the system call signal that will be sent to the container to exit.
StopSignal string `json:"StopSignal,omitempty"` StopSignal string `json:"StopSignal,omitempty"`
// ArgsEscaped `[Deprecated]` - This field is present only for legacy
// compatibility with Docker and should not be used by new image builders.
// It is used by Docker for Windows images to indicate that the `Entrypoint`
// or `Cmd` or both, contains only a single element array, that is a
// pre-escaped, and combined into a single string `CommandLine`. If `true`
// the value in `Entrypoint` or `Cmd` should be used as-is to avoid double
// escaping.
ArgsEscaped bool `json:"ArgsEscaped,omitempty"`
} }
// RootFS describes a layer content addresses // RootFS describes a layer content addresses

View File

@ -1,4 +1,4 @@
// Copyright 2016 The Linux Foundation // Copyright 2016-2022 The Linux Foundation
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -44,6 +44,9 @@ type Descriptor struct {
// //
// This should only be used when referring to a manifest. // This should only be used when referring to a manifest.
Platform *Platform `json:"platform,omitempty"` Platform *Platform `json:"platform,omitempty"`
// ArtifactType is the IANA media type of this artifact.
ArtifactType string `json:"artifactType,omitempty"`
} }
// Platform describes the platform which the image in the manifest runs on. // Platform describes the platform which the image in the manifest runs on.

View File

@ -1,4 +1,4 @@
// Copyright 2016 The Linux Foundation // Copyright 2016-2022 The Linux Foundation
// //
// Licensed under the Apache License, Version 2.0 (the "License"); // Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License. // you may not use this file except in compliance with the License.
@ -30,6 +30,9 @@ type Manifest struct {
// Layers is an indexed list of layers referenced by the manifest. // Layers is an indexed list of layers referenced by the manifest.
Layers []Descriptor `json:"layers"` Layers []Descriptor `json:"layers"`
// Subject is an optional link from the image manifest to another manifest forming an association between the image manifest and the other manifest.
Subject *Descriptor `json:"subject,omitempty"`
// Annotations contains arbitrary metadata for the image manifest. // Annotations contains arbitrary metadata for the image manifest.
Annotations map[string]string `json:"annotations,omitempty"` Annotations map[string]string `json:"annotations,omitempty"`
} }

View File

@ -54,4 +54,7 @@ const (
// MediaTypeImageConfig specifies the media type for the image configuration. // MediaTypeImageConfig specifies the media type for the image configuration.
MediaTypeImageConfig = "application/vnd.oci.image.config.v1+json" MediaTypeImageConfig = "application/vnd.oci.image.config.v1+json"
// MediaTypeArtifactManifest specifies the media type for a content descriptor.
MediaTypeArtifactManifest = "application/vnd.oci.artifact.manifest.v1+json"
) )

View File

@ -20,9 +20,9 @@ const (
// VersionMajor is for an API incompatible changes // VersionMajor is for an API incompatible changes
VersionMajor = 1 VersionMajor = 1
// VersionMinor is for functionality in a backwards-compatible manner // VersionMinor is for functionality in a backwards-compatible manner
VersionMinor = 0 VersionMinor = 1
// VersionPatch is for backwards-compatible bug fixes // VersionPatch is for backwards-compatible bug fixes
VersionPatch = 2 VersionPatch = 0
// VersionDev indicates development branch. Releases will be empty string. // VersionDev indicates development branch. Releases will be empty string.
VersionDev = "-dev" VersionDev = "-dev"

4
vendor/modules.txt vendored
View File

@ -322,8 +322,8 @@ github.com/modern-go/reflect2
## explicit; go 1.13 ## explicit; go 1.13
github.com/opencontainers/go-digest github.com/opencontainers/go-digest
github.com/opencontainers/go-digest/digestset github.com/opencontainers/go-digest/digestset
# github.com/opencontainers/image-spec v1.0.3-0.20220303224323-02efb9a75ee1 # github.com/opencontainers/image-spec v1.1.0-rc2.0.20221005185240-3a7f492d3f1b
## explicit; go 1.16 ## explicit; go 1.17
github.com/opencontainers/image-spec/identity github.com/opencontainers/image-spec/identity
github.com/opencontainers/image-spec/specs-go github.com/opencontainers/image-spec/specs-go
github.com/opencontainers/image-spec/specs-go/v1 github.com/opencontainers/image-spec/specs-go/v1