Skip to content

Commit 8c80d4f

Browse files
committed
manifest: support nerdctl manifest push command
support nerdctl manifest push command Signed-off-by: ChengyuZhu6 <[email protected]>
1 parent 781eeff commit 8c80d4f

File tree

5 files changed

+341
-5
lines changed

5 files changed

+341
-5
lines changed

cmd/nerdctl/manifest/manifest.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ func Command() *cobra.Command {
3737
CreateCommand(),
3838
AnnotateCommand(),
3939
RemoveCommand(),
40+
PushCommand(),
4041
)
4142

4243
return cmd

cmd/nerdctl/manifest/manifest_push.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package manifest
18+
19+
import (
20+
"github.com/spf13/cobra"
21+
22+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/completion"
23+
"github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers"
24+
"github.com/containerd/nerdctl/v2/pkg/api/types"
25+
"github.com/containerd/nerdctl/v2/pkg/cmd/manifest"
26+
)
27+
28+
func PushCommand() *cobra.Command {
29+
var cmd = &cobra.Command{
30+
Use: "push [OPTIONS] INDEX/MANIFESTLIST",
31+
Short: "Push a manifest list to a registry",
32+
Args: cobra.ExactArgs(1),
33+
RunE: pushAction,
34+
ValidArgsFunction: pushShellComplete,
35+
SilenceUsage: true,
36+
SilenceErrors: true,
37+
}
38+
cmd.Flags().Bool("insecure", false, "Allow communication with an insecure registry")
39+
cmd.Flags().Bool("purge", false, "Remove the manifest list after pushing")
40+
return cmd
41+
}
42+
43+
func processPushFlags(cmd *cobra.Command) (types.ManifestPushOptions, error) {
44+
globalOptions, err := helpers.ProcessRootCmdFlags(cmd)
45+
if err != nil {
46+
return types.ManifestPushOptions{}, err
47+
}
48+
49+
insecure, err := cmd.Flags().GetBool("insecure")
50+
if err != nil {
51+
return types.ManifestPushOptions{}, err
52+
}
53+
purge, err := cmd.Flags().GetBool("purge")
54+
if err != nil {
55+
return types.ManifestPushOptions{}, err
56+
}
57+
58+
return types.ManifestPushOptions{
59+
Stdout: cmd.OutOrStdout(),
60+
GOptions: globalOptions,
61+
Insecure: insecure,
62+
Purge: purge,
63+
}, nil
64+
}
65+
66+
func pushAction(cmd *cobra.Command, args []string) error {
67+
pushOptions, err := processPushFlags(cmd)
68+
if err != nil {
69+
return err
70+
}
71+
err = manifest.Push(cmd.Context(), args[0], pushOptions)
72+
if err != nil {
73+
return err
74+
}
75+
return nil
76+
}
77+
78+
func pushShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
79+
return completion.ImageNames(cmd)
80+
}

pkg/api/types/manifest_types.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,13 @@ type ManifestInspectOptions struct {
4848
// Allow communication with an insecure registry
4949
Insecure bool
5050
}
51+
52+
// ManifestPushOptions specifies options for `nerdctl manifest push`.
53+
type ManifestPushOptions struct {
54+
Stdout io.Writer
55+
GOptions GlobalCommandOptions
56+
// Allow communication with an insecure registry
57+
Insecure bool
58+
// Remove the manifest list after pushing
59+
Purge bool
60+
}

pkg/cmd/manifest/push.go

Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/*
2+
Copyright The containerd Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package manifest
18+
19+
import (
20+
"context"
21+
"encoding/base64"
22+
"encoding/json"
23+
"fmt"
24+
"strings"
25+
26+
"github.com/opencontainers/go-digest"
27+
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
28+
29+
"github.com/containerd/containerd/v2/core/images"
30+
"github.com/containerd/containerd/v2/core/remotes"
31+
"github.com/containerd/errdefs"
32+
33+
"github.com/containerd/nerdctl/v2/pkg/api/types"
34+
"github.com/containerd/nerdctl/v2/pkg/manifeststore"
35+
"github.com/containerd/nerdctl/v2/pkg/manifesttypes"
36+
"github.com/containerd/nerdctl/v2/pkg/manifestutil"
37+
"github.com/containerd/nerdctl/v2/pkg/referenceutil"
38+
)
39+
40+
func Push(ctx context.Context, listRef string, options types.ManifestPushOptions) error {
41+
parsedTargetRef, err := referenceutil.Parse(listRef)
42+
if err != nil {
43+
return fmt.Errorf("failed to parse target reference %s: %w", listRef, err)
44+
}
45+
46+
manifestStore, err := manifeststore.NewStore(options.GOptions.DataRoot)
47+
if err != nil {
48+
return fmt.Errorf("failed to create manifest store: %w", err)
49+
}
50+
51+
manifests, err := manifestStore.GetList(parsedTargetRef)
52+
if err != nil {
53+
return fmt.Errorf("failed to get manifests: %w", err)
54+
}
55+
56+
if len(manifests) == 0 {
57+
return fmt.Errorf("no manifests found for %s", listRef)
58+
}
59+
60+
resolver, err := manifestutil.CreateResolver(ctx, parsedTargetRef.Domain, options.GOptions, options.Insecure)
61+
if err != nil {
62+
return fmt.Errorf("failed to create resolver: %w", err)
63+
}
64+
65+
if err := pushIndividualManifests(ctx, resolver, manifests, parsedTargetRef, options); err != nil {
66+
return fmt.Errorf("failed to push individual manifests: %w", err)
67+
}
68+
69+
manifestList, err := buildManifestList(manifests)
70+
if err != nil {
71+
return fmt.Errorf("failed to build manifest list: %w", err)
72+
}
73+
74+
digest, err := pushManifestList(ctx, resolver, parsedTargetRef, manifestList)
75+
if err != nil {
76+
return fmt.Errorf("failed to push manifest list: %w", err)
77+
}
78+
79+
fmt.Fprintln(options.Stdout, digest)
80+
81+
if options.Purge {
82+
if err := manifestStore.Remove(parsedTargetRef); err != nil {
83+
return fmt.Errorf("failed to remove manifest list from store: %w", err)
84+
}
85+
}
86+
87+
return nil
88+
}
89+
90+
func buildManifestList(manifests []*manifesttypes.DockerManifestEntry) (manifesttypes.DockerManifestList, error) {
91+
if len(manifests) == 0 {
92+
return manifesttypes.DockerManifestList{}, fmt.Errorf("no manifests to build list from")
93+
}
94+
95+
var descriptors []manifesttypes.DockerManifestDescriptor
96+
useOCIIndex := false
97+
98+
for _, manifest := range manifests {
99+
if manifest.Descriptor.Platform == nil ||
100+
manifest.Descriptor.Platform.Architecture == "" ||
101+
manifest.Descriptor.Platform.OS == "" {
102+
return manifesttypes.DockerManifestList{}, fmt.Errorf("manifest %s must have an OS and Architecture to be pushed to a registry", manifest.Ref)
103+
}
104+
105+
if manifest.Descriptor.MediaType == ocispec.MediaTypeImageManifest {
106+
useOCIIndex = true
107+
}
108+
109+
descriptors = append(descriptors, manifesttypes.DockerManifestDescriptor{
110+
MediaType: manifest.Descriptor.MediaType,
111+
Size: manifest.Descriptor.Size,
112+
Digest: manifest.Descriptor.Digest,
113+
Platform: *manifest.Descriptor.Platform,
114+
})
115+
}
116+
manifestList := manifesttypes.DockerManifestList{
117+
SchemaVersion: 2,
118+
MediaType: images.MediaTypeDockerSchema2ManifestList,
119+
Manifests: descriptors,
120+
}
121+
if useOCIIndex {
122+
manifestList.MediaType = ocispec.MediaTypeImageIndex
123+
}
124+
125+
return manifestList, nil
126+
}
127+
128+
func pushIndividualManifests(ctx context.Context, resolver remotes.Resolver, manifests []*manifesttypes.DockerManifestEntry, targetRef *referenceutil.ImageReference, options types.ManifestPushOptions) error {
129+
targetDomain := targetRef.Domain
130+
targetRepo := targetRef.Path
131+
132+
for _, manifest := range manifests {
133+
manifestRef, err := referenceutil.Parse(manifest.Ref)
134+
if err != nil {
135+
return fmt.Errorf("failed to parse manifest reference %s: %w", manifest.Ref, err)
136+
}
137+
138+
var targetManifestRef string
139+
if manifestRef.Domain != targetDomain {
140+
targetManifestRef = fmt.Sprintf("%s/%s@%s", targetDomain, manifestRef.Path, manifest.Descriptor.Digest)
141+
} else {
142+
targetManifestRef = fmt.Sprintf("%s/%s@%s", targetDomain, targetRepo, manifest.Descriptor.Digest)
143+
}
144+
145+
if err := pushManifest(ctx, resolver, targetManifestRef, manifest); err != nil {
146+
return fmt.Errorf("failed to push manifest %s: %w", targetManifestRef, err)
147+
}
148+
149+
fmt.Fprintf(options.Stdout, "Pushed ref %s with digest: %s\n", targetManifestRef, manifest.Descriptor.Digest)
150+
}
151+
152+
return nil
153+
}
154+
155+
func pushManifest(ctx context.Context, resolver remotes.Resolver, ref string, manifest *manifesttypes.DockerManifestEntry) error {
156+
rawData, err := base64.StdEncoding.DecodeString(manifest.Raw)
157+
if err != nil {
158+
return fmt.Errorf("failed to decode manifest data: %w", err)
159+
}
160+
161+
pusher, err := resolver.Pusher(ctx, ref)
162+
if err != nil {
163+
return fmt.Errorf("failed to create pusher: %w", err)
164+
}
165+
166+
writer, err := pusher.Push(ctx, manifest.Descriptor)
167+
if err != nil {
168+
if errdefs.IsAlreadyExists(err) || strings.Contains(err.Error(), "already exists") {
169+
return nil
170+
}
171+
return fmt.Errorf("failed to create content writer: %w", err)
172+
}
173+
defer writer.Close()
174+
175+
if _, err := writer.Write(rawData); err != nil {
176+
return fmt.Errorf("failed to write manifest data: %w", err)
177+
}
178+
179+
if err := writer.Commit(ctx, manifest.Descriptor.Size, manifest.Descriptor.Digest); err != nil {
180+
if errdefs.IsAlreadyExists(err) || strings.Contains(err.Error(), "already exists") {
181+
return nil
182+
}
183+
return fmt.Errorf("failed to commit manifest: %w", err)
184+
}
185+
186+
return nil
187+
}
188+
189+
func pushManifestList(ctx context.Context, resolver remotes.Resolver, targetRef *referenceutil.ImageReference, manifestList manifesttypes.DockerManifestList) (digest.Digest, error) {
190+
data, err := json.MarshalIndent(manifestList, "", " ")
191+
if err != nil {
192+
return "", fmt.Errorf("failed to marshal manifest list: %w", err)
193+
}
194+
195+
dgst := digest.FromBytes(data)
196+
197+
desc := ocispec.Descriptor{
198+
MediaType: manifestList.MediaType,
199+
Size: int64(len(data)),
200+
Digest: dgst,
201+
}
202+
203+
pusher, err := resolver.Pusher(ctx, targetRef.String())
204+
if err != nil {
205+
return "", fmt.Errorf("failed to create pusher: %w", err)
206+
}
207+
208+
writer, err := pusher.Push(ctx, desc)
209+
if err != nil {
210+
if errdefs.IsAlreadyExists(err) || strings.Contains(err.Error(), "already exists") {
211+
return dgst, nil
212+
}
213+
return "", fmt.Errorf("failed to create content writer: %w", err)
214+
}
215+
defer writer.Close()
216+
217+
if _, err := writer.Write(data); err != nil {
218+
return "", fmt.Errorf("failed to write manifest list data: %w", err)
219+
}
220+
221+
if err := writer.Commit(ctx, desc.Size, desc.Digest); err != nil {
222+
if errdefs.IsAlreadyExists(err) || strings.Contains(err.Error(), "already exists") {
223+
return dgst, nil
224+
}
225+
return "", fmt.Errorf("failed to commit manifest list: %w", err)
226+
}
227+
228+
return dgst, nil
229+
}

pkg/manifesttypes/manifesttypes.go

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,11 +17,12 @@
1717
package manifesttypes
1818

1919
import (
20+
"github.com/opencontainers/go-digest"
2021
ocispec "github.com/opencontainers/image-spec/specs-go/v1"
2122
)
2223

24+
// For Docker's verbose format
2325
type (
24-
2526
// DockerManifestEntry represents a single manifest entry in Docker's verbose format
2627
DockerManifestEntry struct {
2728
Ref string `json:"Ref"`
@@ -30,6 +31,7 @@ type (
3031
SchemaV2Manifest interface{} `json:"SchemaV2Manifest,omitempty"`
3132
OCIManifest interface{} `json:"OCIManifest,omitempty"`
3233
}
34+
3335
ManifestStruct struct {
3436
SchemaVersion int `json:"schemaVersion"`
3537
MediaType string `json:"mediaType"`
@@ -38,15 +40,29 @@ type (
3840
Annotations map[string]string `json:"annotations,omitempty"`
3941
}
4042

41-
DockerManifestStruct ManifestStruct
42-
4343
DockerManifestListStruct struct {
4444
SchemaVersion int `json:"schemaVersion"`
4545
MediaType string `json:"mediaType"`
4646
Manifests []ocispec.Descriptor `json:"manifests"`
4747
}
4848

49-
OCIIndexStruct ocispec.Index
49+
DockerManifestStruct = ManifestStruct
50+
OCIManifestStruct = ManifestStruct
51+
OCIIndexStruct = ocispec.Index
52+
)
53+
54+
// For manifest push, compatible with Docker distribution spec
55+
type (
56+
DockerManifestDescriptor struct {
57+
MediaType string `json:"mediaType"`
58+
Size int64 `json:"size"`
59+
Digest digest.Digest `json:"digest"`
60+
Platform ocispec.Platform `json:"platform"`
61+
}
5062

51-
OCIManifestStruct ManifestStruct
63+
DockerManifestList struct {
64+
SchemaVersion int `json:"schemaVersion"`
65+
MediaType string `json:"mediaType,omitempty"`
66+
Manifests []DockerManifestDescriptor `json:"manifests"`
67+
}
5268
)

0 commit comments

Comments
 (0)