diff --git a/cmd/nerdctl/container/container_commit.go b/cmd/nerdctl/container/container_commit.go index 14db2be2a0b..dd486660e4a 100644 --- a/cmd/nerdctl/container/container_commit.go +++ b/cmd/nerdctl/container/container_commit.go @@ -51,6 +51,7 @@ func CommitCommand() *cobra.Command { cmd.Flags().Bool("zstdchunked", false, "Convert the committed layer to zstd:chunked for lazy pulling") cmd.Flags().Int("zstdchunked-compression-level", 3, "zstd:chunked compression level") cmd.Flags().Int("zstdchunked-chunk-size", 0, "zstd:chunked chunk size") + cmd.Flags().Bool("devbox-remove-layer", false, "Remove the top layer of the base image when committing a devbox container") return cmd } @@ -128,6 +129,11 @@ func commitOptions(cmd *cobra.Command) (types.ContainerCommitOptions, error) { return types.ContainerCommitOptions{}, errors.New("options --estargz and --zstdchunked lead to conflict, only one of them can be used") } + removeBaseImageTopLayer, err := cmd.Flags().GetBool("devbox-remove-layer") + if err != nil { + return types.ContainerCommitOptions{}, err + } + return types.ContainerCommitOptions{ Stdout: cmd.OutOrStdout(), GOptions: globalOptions, @@ -148,6 +154,9 @@ func commitOptions(cmd *cobra.Command) (types.ContainerCommitOptions, error) { ZstdChunkedCompressionLevel: zstdchunkedCompressionLevel, ZstdChunkedChunkSize: zstdchunkedChunkSize, }, + DevboxOptions: types.DevboxOptions{ + RemoveBaseImageTopLayer: removeBaseImageTopLayer, + }, }, nil } diff --git a/cmd/nerdctl/container/container_create.go b/cmd/nerdctl/container/container_create.go index e30d529481a..ff049d0e4f9 100644 --- a/cmd/nerdctl/container/container_create.go +++ b/cmd/nerdctl/container/container_create.go @@ -19,6 +19,7 @@ package container import ( "fmt" "runtime" + "strings" "github.com/spf13/cobra" cdiparser "tags.cncf.io/container-device-interface/pkg/parser" @@ -413,6 +414,23 @@ func createOptions(cmd *cobra.Command) (types.ContainerCreateOptions, error) { if err != nil { return opt, err } + + // Parse snapshot labels + snapshotLabels, err := cmd.Flags().GetStringArray("snapshot-label") + if err != nil { + return opt, err + } + if len(snapshotLabels) > 0 { + opt.SnapshotLabels = make(map[string]string) + for _, label := range snapshotLabels { + if key, value, ok := strings.Cut(label, "="); ok { + opt.SnapshotLabels[key] = value + } else { + opt.SnapshotLabels[label] = "" + } + } + } + opt.Annotations, err = cmd.Flags().GetStringArray("annotation") if err != nil { return opt, err diff --git a/cmd/nerdctl/container/container_run.go b/cmd/nerdctl/container/container_run.go index 9b44feb19c8..11084195106 100644 --- a/cmd/nerdctl/container/container_run.go +++ b/cmd/nerdctl/container/container_run.go @@ -261,6 +261,8 @@ func setCreateFlags(cmd *cobra.Command) { cmd.Flags().String("name", "", "Assign a name to the container") // label needs to be StringArray, not StringSlice, to prevent "foo=foo1,foo2" from being split to {"foo=foo1", "foo2"} cmd.Flags().StringArrayP("label", "l", nil, "Set metadata on container") + // snapshot-label needs to be StringArray, not StringSlice, to prevent "foo=foo1,foo2" from being split to {"foo=foo1", "foo2"} + cmd.Flags().StringArray("snapshot-label", nil, "Set metadata on snapshot") // annotation needs to be StringArray, not StringSlice, to prevent "foo=foo1,foo2" from being split to {"foo=foo1", "foo2"} cmd.Flags().StringArray("annotation", nil, "Add an annotation to the container (passed through to the OCI runtime)") cmd.RegisterFlagCompletionFunc("annotation", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { diff --git a/cmd/nerdctl/main.go b/cmd/nerdctl/main.go index c5abcc60a6c..8fd871290e7 100644 --- a/cmd/nerdctl/main.go +++ b/cmd/nerdctl/main.go @@ -43,6 +43,7 @@ import ( "github.com/containerd/nerdctl/v2/cmd/nerdctl/manifest" "github.com/containerd/nerdctl/v2/cmd/nerdctl/namespace" "github.com/containerd/nerdctl/v2/cmd/nerdctl/network" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/snapshot" "github.com/containerd/nerdctl/v2/cmd/nerdctl/system" "github.com/containerd/nerdctl/v2/cmd/nerdctl/volume" "github.com/containerd/nerdctl/v2/pkg/config" @@ -331,6 +332,7 @@ Config file ($NERDCTL_TOML): %s system.Command(), namespace.Command(), builder.Command(), + snapshot.Command(), // #endregion // Internal diff --git a/cmd/nerdctl/snapshot/snapshot.go b/cmd/nerdctl/snapshot/snapshot.go new file mode 100644 index 00000000000..0eba19adb6f --- /dev/null +++ b/cmd/nerdctl/snapshot/snapshot.go @@ -0,0 +1,40 @@ +/* + Copyright The containerd Authors. + + 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 snapshot + +import ( + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" +) + +func Command() *cobra.Command { + cmd := &cobra.Command{ + Annotations: map[string]string{helpers.Category: helpers.Management}, + Use: "snapshot", + Short: "Manage snapshots", + RunE: helpers.UnknownSubcommandAction, + SilenceUsage: true, + SilenceErrors: true, + } + cmd.AddCommand( + InfoCommand(), + ListCommand(), + UpdateCommand(), + ) + return cmd +} diff --git a/cmd/nerdctl/snapshot/snapshot_info.go b/cmd/nerdctl/snapshot/snapshot_info.go new file mode 100644 index 00000000000..6154e3ce3da --- /dev/null +++ b/cmd/nerdctl/snapshot/snapshot_info.go @@ -0,0 +1,79 @@ +/* + Copyright The containerd Authors. + + 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 snapshot + +import ( + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/cmd/snapshot" +) + +func InfoCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "info [flags] SNAPSHOT_ID", + Short: "Get detailed information about a snapshot", + Args: helpers.IsExactArgs(1), + RunE: infoAction, + ValidArgsFunction: infoShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + cmd.Flags().StringP("snapshotter", "", "", "Snapshotter name (default: auto-detect)") + cmd.RegisterFlagCompletionFunc("snapshotter", completion.SnapshotterNames) + return cmd +} + +func infoOptions(cmd *cobra.Command) (types.SnapshotInfoOptions, error) { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return types.SnapshotInfoOptions{}, err + } + + snapshotter, err := cmd.Flags().GetString("snapshotter") + if err != nil { + return types.SnapshotInfoOptions{}, err + } + + return types.SnapshotInfoOptions{ + Stdout: cmd.OutOrStdout(), + GOptions: globalOptions, + Snapshotter: snapshotter, + }, nil +} + +func infoAction(cmd *cobra.Command, args []string) error { + options, err := infoOptions(cmd) + if err != nil { + return err + } + client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) + if err != nil { + return err + } + defer cancel() + + return snapshot.Info(ctx, client, args[0], options) +} + +func infoShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // TODO: We could potentially add snapshot ID completion here in the future + return nil, cobra.ShellCompDirectiveNoFileComp +} diff --git a/cmd/nerdctl/snapshot/snapshot_list.go b/cmd/nerdctl/snapshot/snapshot_list.go new file mode 100644 index 00000000000..1c3da1c0a83 --- /dev/null +++ b/cmd/nerdctl/snapshot/snapshot_list.go @@ -0,0 +1,74 @@ +/* + Copyright The containerd Authors. + + 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 snapshot + +import ( + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/cmd/snapshot" +) + +func ListCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "list [flags]", + Short: "List snapshots", + Aliases: []string{"ls"}, + Args: helpers.IsExactArgs(0), + RunE: listAction, + SilenceUsage: true, + SilenceErrors: true, + } + cmd.Flags().StringP("snapshotter", "", "", "Snapshotter name (default: auto-detect)") + cmd.RegisterFlagCompletionFunc("snapshotter", completion.SnapshotterNames) + return cmd +} + +func listOptions(cmd *cobra.Command) (types.SnapshotListOptions, error) { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return types.SnapshotListOptions{}, err + } + + snapshotter, err := cmd.Flags().GetString("snapshotter") + if err != nil { + return types.SnapshotListOptions{}, err + } + + return types.SnapshotListOptions{ + Stdout: cmd.OutOrStdout(), + GOptions: globalOptions, + Snapshotter: snapshotter, + }, nil +} + +func listAction(cmd *cobra.Command, args []string) error { + options, err := listOptions(cmd) + if err != nil { + return err + } + client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) + if err != nil { + return err + } + defer cancel() + + return snapshot.List(ctx, client, options) +} diff --git a/cmd/nerdctl/snapshot/snapshot_update.go b/cmd/nerdctl/snapshot/snapshot_update.go new file mode 100644 index 00000000000..4ca71aad00b --- /dev/null +++ b/cmd/nerdctl/snapshot/snapshot_update.go @@ -0,0 +1,81 @@ +/* + Copyright The containerd Authors. + + 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 snapshot + +import ( + "github.com/spf13/cobra" + + "github.com/containerd/nerdctl/v2/cmd/nerdctl/completion" + "github.com/containerd/nerdctl/v2/cmd/nerdctl/helpers" + "github.com/containerd/nerdctl/v2/pkg/api/types" + "github.com/containerd/nerdctl/v2/pkg/clientutil" + "github.com/containerd/nerdctl/v2/pkg/cmd/snapshot" +) + +func UpdateCommand() *cobra.Command { + var cmd = &cobra.Command{ + Use: "update [flags] SNAPSHOT_ID [LABEL=VALUE...]", + Short: "Update snapshot metadata", + Args: cobra.MinimumNArgs(1), + RunE: updateAction, + ValidArgsFunction: updateShellComplete, + SilenceUsage: true, + SilenceErrors: true, + } + cmd.Flags().StringP("snapshotter", "", "", "Snapshotter name (default: auto-detect)") + cmd.RegisterFlagCompletionFunc("snapshotter", completion.SnapshotterNames) + return cmd +} + +func updateOptions(cmd *cobra.Command) (types.SnapshotUpdateOptions, error) { + globalOptions, err := helpers.ProcessRootCmdFlags(cmd) + if err != nil { + return types.SnapshotUpdateOptions{}, err + } + + snapshotter, err := cmd.Flags().GetString("snapshotter") + if err != nil { + return types.SnapshotUpdateOptions{}, err + } + + return types.SnapshotUpdateOptions{ + Stdout: cmd.OutOrStdout(), + GOptions: globalOptions, + Snapshotter: snapshotter, + }, nil +} + +func updateAction(cmd *cobra.Command, args []string) error { + options, err := updateOptions(cmd) + if err != nil { + return err + } + client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address) + if err != nil { + return err + } + defer cancel() + + snapshotID := args[0] + labels := args[1:] + return snapshot.Update(ctx, client, snapshotID, labels, options) +} + +func updateShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + // TODO: We could potentially add snapshot ID completion here in the future + return nil, cobra.ShellCompDirectiveNoFileComp +} diff --git a/pkg/api/types/container_types.go b/pkg/api/types/container_types.go index a19fb5aea1f..dc4ca1b58cf 100644 --- a/pkg/api/types/container_types.go +++ b/pkg/api/types/container_types.go @@ -301,6 +301,9 @@ type ContainerCreateOptions struct { // UserNS name for user namespace mapping of container UserNS string + + // SnapshotLabels set snapshot's labels + SnapshotLabels map[string]string } // ContainerStopOptions specifies options for `nerdctl (container) stop`. @@ -406,6 +409,8 @@ type ContainerCommitOptions struct { EstargzOptions // Embed ZstdChunkedOptions for zstd:chunked conversion options ZstdChunkedOptions + // DevboxOptions for devbox specific options + DevboxOptions } type CompressionType string diff --git a/pkg/api/types/image_types.go b/pkg/api/types/image_types.go index 0ceb3148896..55527da9dec 100644 --- a/pkg/api/types/image_types.go +++ b/pkg/api/types/image_types.go @@ -76,6 +76,11 @@ type ImageConvertOptions struct { SociConvertOptions } +type DevboxOptions struct { + // RemoveBaseImageTopLayer remove the top layer of the base image + RemoveBaseImageTopLayer bool +} + // EstargzOptions contains eStargz conversion options type EstargzOptions struct { // Estargz convert legacy tar(.gz) layers to eStargz for lazy pulling. Should be used in conjunction with '--oci' diff --git a/pkg/api/types/snapshot_types.go b/pkg/api/types/snapshot_types.go new file mode 100644 index 00000000000..18bc23ece66 --- /dev/null +++ b/pkg/api/types/snapshot_types.go @@ -0,0 +1,40 @@ +/* + Copyright The containerd Authors. + + 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 types + +import "io" + +// SnapshotInfoOptions specifies options for getting snapshot information. +type SnapshotInfoOptions struct { + Stdout io.Writer + GOptions GlobalCommandOptions + Snapshotter string +} + +// SnapshotListOptions specifies options for listing snapshots. +type SnapshotListOptions struct { + Stdout io.Writer + GOptions GlobalCommandOptions + Snapshotter string +} + +// SnapshotUpdateOptions specifies options for updating snapshot metadata. +type SnapshotUpdateOptions struct { + Stdout io.Writer + GOptions GlobalCommandOptions + Snapshotter string +} diff --git a/pkg/cmd/container/commit.go b/pkg/cmd/container/commit.go index 4b447004247..0b9cca4bf9e 100644 --- a/pkg/cmd/container/commit.go +++ b/pkg/cmd/container/commit.go @@ -53,6 +53,7 @@ func Commit(ctx context.Context, client *containerd.Client, rawRef string, req s Format: options.Format, EstargzOptions: options.EstargzOptions, ZstdChunkedOptions: options.ZstdChunkedOptions, + DevboxOptions: options.DevboxOptions, } walker := &containerwalker.ContainerWalker{ diff --git a/pkg/cmd/container/create.go b/pkg/cmd/container/create.go index 232d8a27b77..c1718fb1495 100644 --- a/pkg/cmd/container/create.go +++ b/pkg/cmd/container/create.go @@ -39,6 +39,7 @@ import ( "github.com/containerd/containerd/v2/pkg/oci" "github.com/containerd/log" + "github.com/containerd/containerd/v2/core/snapshots" "github.com/containerd/nerdctl/v2/pkg/annotations" "github.com/containerd/nerdctl/v2/pkg/api/types" "github.com/containerd/nerdctl/v2/pkg/clientutil" @@ -226,8 +227,13 @@ func Create(ctx context.Context, client *containerd.Client, args []string, netMa } else { if !options.Rootfs { // UserNS not set and its a normal image - cOpts = append(cOpts, containerd.WithNewSnapshot(id, ensuredImage.Image)) - } + var snapshotOpts []snapshots.Opt + if len(options.SnapshotLabels) > 0 { + snapshotOpts = append(snapshotOpts, snapshots.WithLabels(options.SnapshotLabels)) + } + // 指定snapshotter和snapshot label + cOpts = append(cOpts, containerd.WithNewSnapshot(id, ensuredImage.Image, snapshotOpts...)) + } } if options.Workdir != "" { diff --git a/pkg/cmd/snapshot/info.go b/pkg/cmd/snapshot/info.go new file mode 100644 index 00000000000..0dfe63dca69 --- /dev/null +++ b/pkg/cmd/snapshot/info.go @@ -0,0 +1,95 @@ +/* + Copyright The containerd Authors. + + 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 snapshot + +import ( + "context" + "encoding/json" + "fmt" + "time" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/snapshots" + "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/api/types" +) + +// SnapshotInfo represents detailed information about a snapshot +type SnapshotInfo struct { + Name string `json:"name"` + Parent string `json:"parent,omitempty"` + Kind snapshots.Kind `json:"kind"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Labels map[string]string `json:"labels,omitempty"` + Usage *SnapshotUsage `json:"usage,omitempty"` + Snapshotter string `json:"snapshotter"` +} + +// SnapshotUsage represents usage information about a snapshot +type SnapshotUsage struct { + Inodes int64 `json:"inodes"` + Size int64 `json:"size"` +} + +// Info retrieves detailed information about a snapshot +func Info(ctx context.Context, client *containerd.Client, snapshotID string, options types.SnapshotInfoOptions) error { + var snapshotterName string + if options.Snapshotter != "" { + snapshotterName = options.Snapshotter + } else { + // Use default snapshotter if not specified + snapshotterName = "overlayfs" + } + + snapshotter := client.SnapshotService(snapshotterName) + + // Get snapshot stat information + info, err := snapshotter.Stat(ctx, snapshotID) + if err != nil { + return fmt.Errorf("failed to get snapshot info: %w", err) + } + + // Get snapshot usage information + usage, err := snapshotter.Usage(ctx, snapshotID) + if err != nil { + log.L.WithError(err).Warnf("failed to get snapshot usage for %s", snapshotID) + } + + snapshotInfo := SnapshotInfo{ + Name: info.Name, + Parent: info.Parent, + Kind: info.Kind, + Created: info.Created, + Updated: info.Updated, + Labels: info.Labels, + Snapshotter: snapshotterName, + } + + if err == nil { + snapshotInfo.Usage = &SnapshotUsage{ + Inodes: usage.Inodes, + Size: usage.Size, + } + } + + // Output as JSON for now (can be extended to support different formats) + encoder := json.NewEncoder(options.Stdout) + encoder.SetIndent("", " ") + return encoder.Encode(snapshotInfo) +} diff --git a/pkg/cmd/snapshot/list.go b/pkg/cmd/snapshot/list.go new file mode 100644 index 00000000000..489bb4e3146 --- /dev/null +++ b/pkg/cmd/snapshot/list.go @@ -0,0 +1,79 @@ +/* + Copyright The containerd Authors. + + 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 snapshot + +import ( + "context" + "encoding/json" + "fmt" + "time" + + containerd "github.com/containerd/containerd/v2/client" + "github.com/containerd/containerd/v2/core/snapshots" + "github.com/containerd/log" + + "github.com/containerd/nerdctl/v2/pkg/api/types" +) + +// SnapshotListItem represents a snapshot in the list +type SnapshotListItem struct { + Name string `json:"name"` + Parent string `json:"parent,omitempty"` + Kind snapshots.Kind `json:"kind"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Snapshotter string `json:"snapshotter"` + Labels map[string]string `json:"labels,omitempty"` +} + +// List retrieves a list of snapshots +func List(ctx context.Context, client *containerd.Client, options types.SnapshotListOptions) error { + var snapshotterName string + if options.Snapshotter != "" { + snapshotterName = options.Snapshotter + } else { + // Use default snapshotter if not specified + snapshotterName = "overlayfs" + } + + snapshotter := client.SnapshotService(snapshotterName) + + // Walk through all snapshots + var snapshotList []SnapshotListItem + err := snapshotter.Walk(ctx, func(ctx context.Context, info snapshots.Info) error { + snapshotList = append(snapshotList, SnapshotListItem{ + Name: info.Name, + Parent: info.Parent, + Kind: info.Kind, + Created: info.Created, + Updated: info.Updated, + Snapshotter: snapshotterName, + Labels: info.Labels, + }) + return nil + }) + + if err != nil { + log.L.WithError(err).Errorf("failed to walk snapshots") + return fmt.Errorf("failed to list snapshots: %w", err) + } + + // Output as JSON for now (can be extended to support different formats) + encoder := json.NewEncoder(options.Stdout) + encoder.SetIndent("", " ") + return encoder.Encode(snapshotList) +} diff --git a/pkg/cmd/snapshot/update.go b/pkg/cmd/snapshot/update.go new file mode 100644 index 00000000000..303592bd9ea --- /dev/null +++ b/pkg/cmd/snapshot/update.go @@ -0,0 +1,73 @@ +/* + Copyright The containerd Authors. + + 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 snapshot + +import ( + "context" + "fmt" + "strings" + + containerd "github.com/containerd/containerd/v2/client" + + "github.com/containerd/nerdctl/v2/pkg/api/types" +) + +// Update updates snapshot metadata (labels) +func Update(ctx context.Context, client *containerd.Client, snapshotID string, labelArgs []string, options types.SnapshotUpdateOptions) error { + var snapshotterName string + if options.Snapshotter != "" { + snapshotterName = options.Snapshotter + } else { + // Use default snapshotter if not specified + snapshotterName = "overlayfs" + } + + snapshotter := client.SnapshotService(snapshotterName) + + // Parse label arguments (LABEL=VALUE format) + labels := make(map[string]string) + for _, labelArg := range labelArgs { + parts := strings.SplitN(labelArg, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid label format %q (expected LABEL=VALUE)", labelArg) + } + labels[parts[0]] = parts[1] + } + + // Get current snapshot info + info, err := snapshotter.Stat(ctx, snapshotID) + if err != nil { + return fmt.Errorf("failed to get snapshot info: %w", err) + } + + // Merge new labels with existing ones + if info.Labels == nil { + info.Labels = make(map[string]string) + } + for k, v := range labels { + info.Labels[k] = v + } + + // Update the snapshot + _, err = snapshotter.Update(ctx, info, "labels") + if err != nil { + return fmt.Errorf("failed to update snapshot: %w", err) + } + + fmt.Fprintf(options.Stdout, "Successfully updated snapshot %s\n", snapshotID) + return nil +} diff --git a/pkg/imgutil/commit/commit.go b/pkg/imgutil/commit/commit.go index f283a4dd290..381c3ae327a 100644 --- a/pkg/imgutil/commit/commit.go +++ b/pkg/imgutil/commit/commit.go @@ -40,7 +40,6 @@ import ( "github.com/containerd/containerd/v2/core/leases" "github.com/containerd/containerd/v2/core/snapshots" "github.com/containerd/containerd/v2/pkg/cio" - "github.com/containerd/containerd/v2/pkg/rootfs" "github.com/containerd/errdefs" "github.com/containerd/log" "github.com/containerd/platforms" @@ -70,6 +69,7 @@ type Opts struct { Format types.ImageFormat types.EstargzOptions types.ZstdChunkedOptions + types.DevboxOptions } var ( @@ -198,7 +198,7 @@ func Commit(ctx context.Context, client *containerd.Client, container containerd } rootfsID := identity.ChainID(imageConfig.RootFS.DiffIDs).String() - if err := applyDiffLayer(ctx, rootfsID, baseImgConfig, sn, differ, diffLayerDesc); err != nil { + if err := applyDiffLayer(ctx, rootfsID, baseImgConfig, sn, differ, diffLayerDesc, opts.DevboxOptions.RemoveBaseImageTopLayer); err != nil { return emptyDigest, fmt.Errorf("failed to apply diff: %w", err) } @@ -273,6 +273,11 @@ func generateCommitImageConfig(ctx context.Context, container containerd.Contain log.G(ctx).Warnf("assuming os=%q", os) } log.G(ctx).Debugf("generateCommitImageConfig(): arch=%q, os=%q", arch, os) + // remove last diiffID + if opts.DevboxOptions.RemoveBaseImageTopLayer && len(baseConfig.RootFS.DiffIDs) > 1 { + baseConfig.RootFS.DiffIDs = baseConfig.RootFS.DiffIDs[:len(baseConfig.RootFS.DiffIDs)-1] + baseConfig.History = baseConfig.History[:len(baseConfig.History)-1] + } return ocispec.Image{ Platform: ocispec.Platform{ Architecture: arch, @@ -329,6 +334,10 @@ func writeContentsForImage(ctx context.Context, snName string, baseImg container if err != nil { return ocispec.Descriptor{}, emptyDigest, err } + // remove last layer + if opts.DevboxOptions.RemoveBaseImageTopLayer && len(baseMfst.Layers) > 1 { + baseMfst.Layers = baseMfst.Layers[:len(baseMfst.Layers)-1] + } layers := append(baseMfst.Layers, diffLayerDesc) newMfst := struct { @@ -420,7 +429,7 @@ func createDiff(ctx context.Context, name string, sn snapshots.Snapshotter, cs c } } - newDesc, err := rootfs.CreateDiff(ctx, name, sn, comparer, diffOpts...) + newDesc, err := CreateDiff(ctx, name, sn, comparer, opts.DevboxOptions.RemoveBaseImageTopLayer, diffOpts...) if err != nil { return ocispec.Descriptor{}, digest.Digest(""), err } @@ -530,12 +539,20 @@ func createDiff(ctx context.Context, name string, sn snapshots.Snapshotter, cs c } // applyDiffLayer will apply diff layer content created by createDiff into the snapshotter. -func applyDiffLayer(ctx context.Context, name string, baseImg ocispec.Image, sn snapshots.Snapshotter, differ diff.Applier, diffDesc ocispec.Descriptor) (retErr error) { +func applyDiffLayer(ctx context.Context, name string, baseImg ocispec.Image, sn snapshots.Snapshotter, differ diff.Applier, diffDesc ocispec.Descriptor, removeTopLayer bool) (retErr error) { var ( key = uniquePart() + "-" + name parent = identity.ChainID(baseImg.RootFS.DiffIDs).String() ) + if removeTopLayer { + info, err := sn.Stat(ctx, parent) + if err != nil { + return err + } + parent = info.Parent + } + mount, err := sn.Prepare(ctx, key, parent) if err != nil { return err diff --git a/pkg/imgutil/commit/diff.go b/pkg/imgutil/commit/diff.go new file mode 100644 index 00000000000..a330b8bf7c2 --- /dev/null +++ b/pkg/imgutil/commit/diff.go @@ -0,0 +1,107 @@ +/* + Copyright The containerd Authors. + + 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 commit + +import ( + "context" + "fmt" + "time" + + "github.com/containerd/containerd/v2/core/diff" + "github.com/containerd/containerd/v2/core/mount" + "github.com/containerd/containerd/v2/core/snapshots" + ocispec "github.com/opencontainers/image-spec/specs-go/v1" +) + +type clearCancel struct { + context.Context +} + +func (cc clearCancel) Deadline() (deadline time.Time, ok bool) { + return +} + +func (cc clearCancel) Done() <-chan struct{} { + return nil +} + +func (cc clearCancel) Err() error { + return nil +} + +// Background creates a new context which clears out the parent errors +func Background(ctx context.Context) context.Context { + return clearCancel{ctx} +} + +// Do runs the provided function with a context in which the +// errors are cleared out and will timeout after 10 seconds. +func Do(ctx context.Context, do func(context.Context)) { + ctx, cancel := context.WithTimeout(clearCancel{ctx}, 10*time.Second) + do(ctx) + cancel() +} + +// CreateDiff creates a layer diff for the given snapshot identifier from the +// parent of the snapshot. A content ref is provided to track the progress of +// the content creation and the provided snapshotter and mount differ are used +// for calculating the diff. The descriptor for the layer diff is returned. +func CreateDiff(ctx context.Context, snapshotID string, sn snapshots.Snapshotter, d diff.Comparer, removeTopLayer bool, opts ...diff.Opt) (ocispec.Descriptor, error) { + info, err := sn.Stat(ctx, snapshotID) + if err != nil { + return ocispec.Descriptor{}, err + } + + parent := info.Parent + if removeTopLayer { + secondInfo, err := sn.Stat(ctx, parent) + if err != nil { + return ocispec.Descriptor{}, err + } + if secondInfo.Parent != "" { + parent = secondInfo.Parent + } + } + + lowerKey := fmt.Sprintf("%s-parent-view-%s", parent, uniquePart()) + lower, err := sn.View(ctx, lowerKey, parent) + if err != nil { + return ocispec.Descriptor{}, err + } + defer Do(ctx, func(ctx context.Context) { + sn.Remove(ctx, lowerKey) + }) + + var upper []mount.Mount + if info.Kind == snapshots.KindActive { + upper, err = sn.Mounts(ctx, snapshotID) + if err != nil { + return ocispec.Descriptor{}, err + } + } else { + upperKey := fmt.Sprintf("%s-view-%s", snapshotID, uniquePart()) + upper, err = sn.View(ctx, upperKey, snapshotID) + if err != nil { + return ocispec.Descriptor{}, err + } + defer Do(ctx, func(ctx context.Context) { + sn.Remove(ctx, upperKey) + }) + } + + return d.Compare(ctx, lower, upper, opts...) +}