Skip to content

Commit d871679

Browse files
feat: add oci image layout mutation
Signed-off-by: Batuhan Apaydın <[email protected]> Co-authored-by: Furkan Türkal <[email protected]> Signed-off-by: Batuhan Apaydın <[email protected]>
1 parent 0f2db49 commit d871679

File tree

2 files changed

+140
-32
lines changed

2 files changed

+140
-32
lines changed

cmd/crane/cmd/mutate.go

+139-32
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,9 @@ import (
1919
"fmt"
2020
"strings"
2121

22+
"github.com/google/go-containerregistry/pkg/v1/layout"
23+
"github.com/google/go-containerregistry/pkg/v1/match"
24+
2225
"github.com/google/go-containerregistry/pkg/crane"
2326
"github.com/google/go-containerregistry/pkg/name"
2427
v1 "github.com/google/go-containerregistry/pkg/v1"
@@ -37,33 +40,78 @@ func NewCmdMutate(options *[]crane.Option) *cobra.Command {
3740
var newRef string
3841
var newRepo string
3942
var user string
43+
var ociImageLayoutPath string
4044

4145
mutateCmd := &cobra.Command{
4246
Use: "mutate",
4347
Short: "Modify image labels and annotations. The container must be pushed to a registry, and the manifest is updated there.",
44-
Args: cobra.ExactArgs(1),
4548
RunE: func(_ *cobra.Command, args []string) error {
49+
var img v1.Image
50+
var err error
4651
// Pull image and get config.
47-
ref := args[0]
52+
var ref string
53+
if len(args) > 0 {
54+
ref = args[0]
55+
}
56+
57+
if ref != "" && ociImageLayoutPath != "" {
58+
return errors.New("cannot specify both image and oci image layout")
59+
}
4860

49-
if len(annotations) != 0 {
50-
desc, err := crane.Head(ref, *options...)
61+
if ref != "" {
62+
if len(annotations) != 0 {
63+
desc, err := crane.Head(ref, *options...)
64+
if err != nil {
65+
return err
66+
}
67+
if desc.MediaType.IsIndex() {
68+
return errors.New("mutating annotations on an index is not yet supported")
69+
}
70+
}
71+
72+
i, err := crane.Pull(ref, *options...)
73+
if err != nil {
74+
return fmt.Errorf("pulling %s: %w", ref, err)
75+
}
76+
img = i
77+
} else if ociImageLayoutPath != "" {
78+
oil, err := layout.FromPath(ociImageLayoutPath)
5179
if err != nil {
5280
return err
5381
}
54-
if desc.MediaType.IsIndex() {
55-
return errors.New("mutating annotations on an index is not yet supported")
82+
ii, err := oil.ImageIndex()
83+
if err != nil {
84+
return err
5685
}
57-
}
5886

59-
if newRepo != "" && newRef != "" {
60-
return errors.New("repository can't be set when a tag is specified")
61-
}
87+
m, err := ii.IndexManifest()
88+
if err != nil {
89+
return err
90+
}
6291

63-
img, err := crane.Pull(ref, *options...)
64-
if err != nil {
65-
return fmt.Errorf("pulling %s: %w", ref, err)
92+
desc := m.Manifests[0]
93+
94+
if len(annotations) != 0 {
95+
if err != nil {
96+
return err
97+
}
98+
if desc.MediaType.IsIndex() {
99+
return errors.New("mutating annotations on an index is not yet supported")
100+
}
101+
}
102+
103+
var i v1.Image
104+
if desc.MediaType.IsImage() {
105+
i, err = oil.Image(desc.Digest)
106+
if err != nil {
107+
return err
108+
}
109+
} else if desc.MediaType.IsIndex() {
110+
return errors.New("mutating layers on an index is not yet supported")
111+
}
112+
img = i
66113
}
114+
67115
if len(newLayers) != 0 {
68116
img, err = crane.Append(img, newLayers...)
69117
if err != nil {
@@ -122,35 +170,93 @@ func NewCmdMutate(options *[]crane.Option) *cobra.Command {
122170

123171
img = mutate.Annotations(img, annotations).(v1.Image)
124172

125-
// If the new ref isn't provided, write over the original image.
126-
// If that ref was provided by digest (e.g., output from
127-
// another crane command), then strip that and push the
128-
// mutated image by digest instead.
129-
if newRepo != "" {
130-
newRef = newRepo
131-
} else if newRef == "" {
132-
newRef = ref
133-
}
134173
digest, err := img.Digest()
135174
if err != nil {
136175
return fmt.Errorf("digesting new image: %w", err)
137176
}
138-
if outFile != "" {
139-
if err := crane.Save(img, newRef, outFile); err != nil {
140-
return fmt.Errorf("writing output %q: %w", outFile, err)
177+
if ref != "" {
178+
if newRepo != "" && newRef != "" {
179+
return errors.New("repository can't be set when a tag is specified")
180+
}
181+
182+
// If the new ref isn't provided, write over the original image.
183+
// If that ref was provided by digest (e.g., output from
184+
// another crane command), then strip that and push the
185+
// mutated image by digest instead.
186+
if newRepo != "" {
187+
newRef = newRepo
188+
} else if newRef == "" {
189+
newRef = ref
190+
}
191+
192+
if outFile != "" {
193+
if err := crane.Save(img, newRef, outFile); err != nil {
194+
return fmt.Errorf("writing output %q: %w", outFile, err)
195+
}
196+
} else {
197+
r, err := name.ParseReference(newRef)
198+
if err != nil {
199+
return fmt.Errorf("parsing %s: %w", newRef, err)
200+
}
201+
if _, ok := r.(name.Digest); ok || newRepo != "" {
202+
newRef = r.Context().Digest(digest.String()).String()
203+
}
204+
if err := crane.Push(img, newRef, *options...); err != nil {
205+
return fmt.Errorf("pushing %s: %w", newRef, err)
206+
}
207+
fmt.Println(r.Context().Digest(digest.String()))
141208
}
142209
} else {
143-
r, err := name.ParseReference(newRef)
210+
// TODO: this adds the new manifest to the index, but doesn't remove the old one
211+
p, err := layout.FromPath(ociImageLayoutPath)
144212
if err != nil {
145-
return fmt.Errorf("parsing %s: %w", newRef, err)
213+
return err
146214
}
147-
if _, ok := r.(name.Digest); ok || newRepo != "" {
148-
newRef = r.Context().Digest(digest.String()).String()
215+
216+
ii, err := p.ImageIndex()
217+
if err != nil {
218+
return err
219+
}
220+
221+
m, err := ii.IndexManifest()
222+
if err != nil {
223+
return err
224+
}
225+
226+
desc := m.Manifests[0]
227+
228+
i, err := p.Image(desc.Digest)
229+
if err != nil {
230+
return err
231+
}
232+
233+
im, err := i.Manifest()
234+
if err != nil {
235+
return err
149236
}
150-
if err := crane.Push(img, newRef, *options...); err != nil {
151-
return fmt.Errorf("pushing %s: %w", newRef, err)
237+
238+
icd := im.Config.Digest
239+
240+
err = p.ReplaceImage(img, match.Digests(digest))
241+
if err != nil {
242+
return err
243+
}
244+
245+
fmt.Printf("Removing old config layer: %s\n", icd.String())
246+
err = p.RemoveBlob(icd)
247+
if err != nil {
248+
return err
249+
}
250+
fmt.Printf("Removing old manifest from index: %s\n", desc.Digest.String())
251+
err = p.RemoveDescriptors(match.Digests(desc.Digest))
252+
if err != nil {
253+
return err
254+
}
255+
fmt.Printf("Removing old manifest blob: %s\n", desc.Digest.String())
256+
err = p.RemoveBlob(desc.Digest)
257+
if err != nil {
258+
return err
152259
}
153-
fmt.Println(r.Context().Digest(digest.String()))
154260
}
155261
return nil
156262
},
@@ -165,6 +271,7 @@ func NewCmdMutate(options *[]crane.Option) *cobra.Command {
165271
mutateCmd.Flags().StringVarP(&outFile, "output", "o", "", "Path to new tarball of resulting image")
166272
mutateCmd.Flags().StringSliceVar(&newLayers, "append", []string{}, "Path to tarball to append to image")
167273
mutateCmd.Flags().StringVarP(&user, "user", "u", "", "New user to set")
274+
mutateCmd.Flags().StringVarP(&ociImageLayoutPath, "oci-image-layout", "", "", "A path to OCI Image Layout directory")
168275
return mutateCmd
169276
}
170277

cmd/crane/doc/crane_mutate.md

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)