Skip to content

Commit

Permalink
kind build node-image: Support Docker v25.0.1
Browse files Browse the repository at this point in the history
`docker save` in Docker v25 produces Docker/OCI dual-format archives:
- `repositories`, `manifest.json`: for legacy Docker format
- `oci-layout`, `index.json` (and blobs): for OCI format

However, `pkg/build/nodeimage/internal/container/docker.EditArchive` did not
support rewriting OCI Index.

This was resulting in producing broken images with Docker v25.

See kubernetes/kubernetes issue 122894

NOTE: This is still incompatible with Docker v25.0.0 due to
moby/moby issue 47150. The issue was fixed in v25.0.1.

Signed-off-by: Akihiro Suda <[email protected]>
  • Loading branch information
AkihiroSuda committed Jan 24, 2024
1 parent ad3437c commit 867cbde
Show file tree
Hide file tree
Showing 3 changed files with 59 additions and 6 deletions.
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ require (
github.com/evanphx/json-patch/v5 v5.6.0
github.com/google/safetext v0.0.0-20220905092116-b49f7bc46da2
github.com/mattn/go-isatty v0.0.14
github.com/opencontainers/go-digest v1.0.0
github.com/opencontainers/image-spec v1.0.2
github.com/pelletier/go-toml v1.9.4
github.com/pkg/errors v0.9.1
github.com/spf13/cobra v1.4.0
Expand Down
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
github.com/pelletier/go-toml v1.9.4 h1:tjENF6MfZAg8e4ZmZTeWaWiT2vXtsoO6+iuOjFhECwM=
github.com/pelletier/go-toml v1.9.4/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
Expand Down
59 changes: 53 additions & 6 deletions pkg/build/nodeimage/internal/container/docker/archive.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,15 +26,21 @@ import (
"os"
"strings"

"github.com/opencontainers/go-digest"
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
"sigs.k8s.io/kind/pkg/errors"
)

// ioContainerdImageName appears as an annotation in a manifest entry in "index.json".
const ioContainerdImageName = "io.containerd.image.name"

// GetArchiveTags obtains a list of "repo:tag" docker image tags from a
// given docker image archive (tarball) path
// compatible with all known specs:
// https://github.com/moby/moby/blob/master/image/spec/v1.md
// https://github.com/moby/moby/blob/master/image/spec/v1.1.md
// https://github.com/moby/moby/blob/master/image/spec/v1.2.md
// https://github.com/opencontainers/image-spec/blob/v1.0.2/image-index.md (annotation "io.containerd.image.name")
func GetArchiveTags(path string) ([]string, error) {
// open the archive and find the repositories entry
f, err := os.Open(path)
Expand All @@ -52,7 +58,7 @@ func GetArchiveTags(path string) ([]string, error) {
if err != nil {
return nil, err
}
if hdr.Name == "manifest.json" || hdr.Name == "repositories" {
if hdr.Name == "manifest.json" || hdr.Name == "repositories" || hdr.Name == "index.json" {
break
}
}
Expand Down Expand Up @@ -80,20 +86,31 @@ func GetArchiveTags(path string) ([]string, error) {
return nil, err
}
res = append(res, manifest[0].RepoTags...)
} else if hdr.Name == "index.json" {
var idx ocispec.Index
if err := json.Unmarshal(b, &idx); err != nil {
return nil, err
}
for _, mani := range idx.Manifests {
if repoTag, ok := mani.Annotations[ioContainerdImageName]; ok {
res = append(res, repoTag)
}
}
}
return res, nil
}

// EditArchive applies edit to reader's image repositories,
// IE the repository part of repository:tag in image tags
// This supports v1 / v1.1 / v1.2 Docker Image Archives
// This supports v1 / v1.1 / v1.2 Docker Image Archives and OCI Image Spec v1.0 Archives.
//
// editRepositories should be a function that returns the input or an edited
// form, where the input is the image repository
//
// https://github.com/moby/moby/blob/master/image/spec/v1.md
// https://github.com/moby/moby/blob/master/image/spec/v1.1.md
// https://github.com/moby/moby/blob/master/image/spec/v1.2.md
// https://github.com/opencontainers/image-spec/blob/v1.0.2/image-index.md (annotation "io.containerd.image.name")
func EditArchive(reader io.Reader, writer io.Writer, editRepositories func(string) string, architectureOverride string) error {
tarReader := tar.NewReader(reader)
tarWriter := tar.NewWriter(writer)
Expand Down Expand Up @@ -124,6 +141,12 @@ func EditArchive(reader io.Reader, writer io.Writer, editRepositories func(strin
return err
}
hdr.Size = int64(len(b))
} else if hdr.Name == "index.json" {
b, err = editIndexJSON(b, editRepositories)
if err != nil {
return err
}
hdr.Size = int64(len(b))
// edit image config when we find that
} else if strings.HasSuffix(hdr.Name, ".json") {
if architectureOverride != "" {
Expand Down Expand Up @@ -185,9 +208,10 @@ func editRepositoriesFile(raw []byte, editRepositories func(string) string) ([]b

// https://github.com/moby/moby/blob/master/image/spec/v1.2.md#combined-image-json--filesystem-changeset-format
type metadataEntry struct {
Config string `json:"Config"`
RepoTags []string `json:"RepoTags"`
Layers []string `json:"Layers"`
Config string `json:"Config"`
RepoTags []string `json:"RepoTags"`
Layers []string `json:"Layers"`
LayerSources map[digest.Digest]ocispec.Descriptor `json:"LayerSources,omitempty"` // since Docker v25
}

// applies
Expand All @@ -202,7 +226,7 @@ func editManifestRepositories(raw []byte, editRepositories func(string) string)
for i, tag := range entry.RepoTags {
parts := strings.Split(tag, ":")
if len(parts) > 2 {
return nil, fmt.Errorf("invalid repotag: %s", entry)
return nil, fmt.Errorf("invalid repotag: %v", entry)
}
parts[0] = editRepositories(parts[0])
fixed[i] = strings.Join(parts, ":")
Expand All @@ -214,6 +238,29 @@ func editManifestRepositories(raw []byte, editRepositories func(string) string)
return json.Marshal(entries)
}

// editIndexJSON edits "index.json" that appears in OCI Image Spec v1 archives.
func editIndexJSON(raw []byte, editRepositories func(string) string) ([]byte, error) {
var idx ocispec.Index
if err := json.Unmarshal(raw, &idx); err != nil {
return nil, err
}

for i := range idx.Manifests {
mani := &idx.Manifests[i]
if repoTag, ok := mani.Annotations[ioContainerdImageName]; ok {
parts := strings.Split(repoTag, ":")
if len(parts) > 2 {
return nil, fmt.Errorf("invalid repotag: %s", repoTag)
}
parts[0] = editRepositories(parts[0])
mani.Annotations[ioContainerdImageName] = strings.Join(parts, ":")
}
}

// NOTE: we may lose some JSON fields if the original JSON was produced with a newer version of OCI Image Spec.
return json.Marshal(idx)
}

// returns repository:tag:ref
func parseRepositories(data []byte) (archiveRepositories, error) {
var repoTags archiveRepositories
Expand Down

0 comments on commit 867cbde

Please sign in to comment.