Skip to content

Commit e347031

Browse files
committed
feat: add oci image layout mutation
Signed-off-by: Batuhan Apaydın <[email protected]>
1 parent e2d575c commit e347031

File tree

1 file changed

+139
-32
lines changed

1 file changed

+139
-32
lines changed

cmd/crane/cmd/mutate.go

+139-32
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ package cmd
1717
import (
1818
"errors"
1919
"fmt"
20+
"github.com/google/go-containerregistry/pkg/v1/layout"
21+
"github.com/google/go-containerregistry/pkg/v1/match"
2022
"strings"
2123

2224
"github.com/google/go-containerregistry/pkg/crane"
@@ -37,33 +39,78 @@ func NewCmdMutate(options *[]crane.Option) *cobra.Command {
3739
var newRef string
3840
var newRepo string
3941
var user string
42+
var ociImageLayoutPath string
4043

4144
mutateCmd := &cobra.Command{
4245
Use: "mutate",
4346
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),
4547
RunE: func(_ *cobra.Command, args []string) error {
48+
var img v1.Image
49+
var err error
4650
// Pull image and get config.
47-
ref := args[0]
51+
var ref string
52+
if len(args) > 0 {
53+
ref = args[0]
54+
}
55+
56+
if ref != "" && ociImageLayoutPath != "" {
57+
return errors.New("cannot specify both image and oci image layout")
58+
}
59+
60+
if ref != "" {
61+
if len(annotations) != 0 {
62+
desc, err := crane.Head(ref, *options...)
63+
if err != nil {
64+
return err
65+
}
66+
if desc.MediaType.IsIndex() {
67+
return errors.New("mutating annotations on an index is not yet supported")
68+
}
69+
}
4870

49-
if len(annotations) != 0 {
50-
desc, err := crane.Head(ref, *options...)
71+
i, err := crane.Pull(ref, *options...)
72+
if err != nil {
73+
return fmt.Errorf("pulling %s: %w", ref, err)
74+
}
75+
img = i
76+
} else if ociImageLayoutPath != "" {
77+
oil, err := layout.FromPath(ociImageLayoutPath)
5178
if err != nil {
5279
return err
5380
}
54-
if desc.MediaType.IsIndex() {
55-
return errors.New("mutating annotations on an index is not yet supported")
81+
ii, err := oil.ImageIndex()
82+
if err != nil {
83+
return err
5684
}
57-
}
5885

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

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

123170
img = mutate.Annotations(img, annotations).(v1.Image)
124171

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-
}
134172
digest, err := img.Digest()
135173
if err != nil {
136174
return fmt.Errorf("digesting new image: %w", err)
137175
}
138-
if outFile != "" {
139-
if err := crane.Save(img, newRef, outFile); err != nil {
140-
return fmt.Errorf("writing output %q: %w", outFile, err)
176+
if ref != "" {
177+
if newRepo != "" && newRef != "" {
178+
return errors.New("repository can't be set when a tag is specified")
179+
}
180+
181+
// If the new ref isn't provided, write over the original image.
182+
// If that ref was provided by digest (e.g., output from
183+
// another crane command), then strip that and push the
184+
// mutated image by digest instead.
185+
if newRepo != "" {
186+
newRef = newRepo
187+
} else if newRef == "" {
188+
newRef = ref
189+
}
190+
191+
if outFile != "" {
192+
if err := crane.Save(img, newRef, outFile); err != nil {
193+
return fmt.Errorf("writing output %q: %w", outFile, err)
194+
}
195+
} else {
196+
r, err := name.ParseReference(newRef)
197+
if err != nil {
198+
return fmt.Errorf("parsing %s: %w", newRef, err)
199+
}
200+
if _, ok := r.(name.Digest); ok || newRepo != "" {
201+
newRef = r.Context().Digest(digest.String()).String()
202+
}
203+
if err := crane.Push(img, newRef, *options...); err != nil {
204+
return fmt.Errorf("pushing %s: %w", newRef, err)
205+
}
206+
fmt.Println(r.Context().Digest(digest.String()))
141207
}
142208
} else {
143-
r, err := name.ParseReference(newRef)
209+
// TODO: this adds the new manifest to the index, but doesn't remove the old one
210+
p, err := layout.FromPath(ociImageLayoutPath)
144211
if err != nil {
145-
return fmt.Errorf("parsing %s: %w", newRef, err)
212+
return err
146213
}
147-
if _, ok := r.(name.Digest); ok || newRepo != "" {
148-
newRef = r.Context().Digest(digest.String()).String()
214+
215+
ii, err := p.ImageIndex()
216+
if err != nil {
217+
return err
149218
}
150-
if err := crane.Push(img, newRef, *options...); err != nil {
151-
return fmt.Errorf("pushing %s: %w", newRef, err)
219+
220+
m, err := ii.IndexManifest()
221+
if err != nil {
222+
return err
223+
}
224+
225+
desc := m.Manifests[0]
226+
227+
i, err := p.Image(desc.Digest)
228+
if err != nil {
229+
return err
230+
}
231+
232+
im, err := i.Manifest()
233+
if err != nil {
234+
return err
235+
}
236+
237+
icd := im.Config.Digest
238+
239+
err = p.ReplaceImage(img, match.Digests(digest))
240+
if err != nil {
241+
return err
242+
}
243+
244+
fmt.Printf("Removing old config layer: %s\n", icd.String())
245+
err = p.RemoveBlob(icd)
246+
if err != nil {
247+
return err
152248
}
153-
fmt.Println(r.Context().Digest(digest.String()))
249+
fmt.Printf("Removing old manifest from index: %s\n", desc.Digest.String())
250+
err = p.RemoveDescriptors(match.Digests(desc.Digest))
251+
if err != nil {
252+
return err
253+
}
254+
fmt.Printf("Removing old manifest blob: %s\n", desc.Digest.String())
255+
err = p.RemoveBlob(desc.Digest)
256+
if err != nil {
257+
return err
258+
}
259+
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

0 commit comments

Comments
 (0)