github.com/containerd/nerdctl@v1.7.7/cmd/nerdctl/container_diff.go (about)

     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 main
    18  
    19  import (
    20  	"context"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"time"
    25  
    26  	"github.com/containerd/containerd"
    27  	"github.com/containerd/containerd/leases"
    28  	"github.com/containerd/containerd/mount"
    29  	"github.com/containerd/continuity/fs"
    30  	"github.com/containerd/log"
    31  	"github.com/containerd/nerdctl/pkg/api/types"
    32  	"github.com/containerd/nerdctl/pkg/clientutil"
    33  	"github.com/containerd/nerdctl/pkg/idgen"
    34  	"github.com/containerd/nerdctl/pkg/idutil/containerwalker"
    35  	"github.com/containerd/nerdctl/pkg/imgutil"
    36  	"github.com/containerd/nerdctl/pkg/labels"
    37  	"github.com/containerd/platforms"
    38  	"github.com/opencontainers/image-spec/identity"
    39  	"github.com/spf13/cobra"
    40  )
    41  
    42  func newDiffCommand() *cobra.Command {
    43  	var diffCommand = &cobra.Command{
    44  		Use:               "diff [CONTAINER]",
    45  		Short:             "Inspect changes to files or directories on a container's filesystem",
    46  		Args:              cobra.MinimumNArgs(1),
    47  		RunE:              diffAction,
    48  		ValidArgsFunction: diffShellComplete,
    49  		SilenceUsage:      true,
    50  		SilenceErrors:     true,
    51  	}
    52  	return diffCommand
    53  }
    54  
    55  func processContainerDiffOptions(cmd *cobra.Command) (types.ContainerDiffOptions, error) {
    56  	globalOptions, err := processRootCmdFlags(cmd)
    57  	if err != nil {
    58  		return types.ContainerDiffOptions{}, err
    59  	}
    60  
    61  	return types.ContainerDiffOptions{
    62  		Stdout:   cmd.OutOrStdout(),
    63  		GOptions: globalOptions,
    64  	}, nil
    65  }
    66  
    67  func diffAction(cmd *cobra.Command, args []string) error {
    68  	options, err := processContainerDiffOptions(cmd)
    69  	if err != nil {
    70  		return err
    71  	}
    72  
    73  	client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), options.GOptions.Namespace, options.GOptions.Address)
    74  	if err != nil {
    75  		return err
    76  	}
    77  	defer cancel()
    78  
    79  	walker := &containerwalker.ContainerWalker{
    80  		Client: client,
    81  		OnFound: func(ctx context.Context, found containerwalker.Found) error {
    82  			if found.MatchCount > 1 {
    83  				return fmt.Errorf("multiple IDs found with provided prefix: %s", found.Req)
    84  			}
    85  			changes, err := getChanges(ctx, client, found.Container)
    86  			if err != nil {
    87  				return err
    88  			}
    89  
    90  			for _, change := range changes {
    91  				switch change.Kind {
    92  				case fs.ChangeKindAdd:
    93  					fmt.Fprintln(options.Stdout, "A", change.Path)
    94  				case fs.ChangeKindModify:
    95  					fmt.Fprintln(options.Stdout, "C", change.Path)
    96  				case fs.ChangeKindDelete:
    97  					fmt.Fprintln(options.Stdout, "D", change.Path)
    98  				default:
    99  				}
   100  			}
   101  
   102  			return nil
   103  		},
   104  	}
   105  
   106  	container := args[0]
   107  
   108  	n, err := walker.Walk(ctx, container)
   109  	if err != nil {
   110  		return err
   111  	} else if n == 0 {
   112  		return fmt.Errorf("no such container %s", container)
   113  	}
   114  	return nil
   115  }
   116  
   117  func getChanges(ctx context.Context, client *containerd.Client, container containerd.Container) ([]fs.Change, error) {
   118  	id := container.ID()
   119  	info, err := container.Info(ctx)
   120  	if err != nil {
   121  		return nil, err
   122  	}
   123  
   124  	var (
   125  		snName = info.Snapshotter
   126  		sn     = client.SnapshotService(snName)
   127  	)
   128  
   129  	mounts, err := sn.Mounts(ctx, id)
   130  	if err != nil {
   131  		return nil, err
   132  	}
   133  
   134  	// NOTE: Moby uses provided rootfs to run container. It doesn't support
   135  	// to commit container created by moby.
   136  	baseImgWithoutPlatform, err := client.ImageService().Get(ctx, info.Image)
   137  	if err != nil {
   138  		return nil, fmt.Errorf("container %q lacks image (wasn't created by nerdctl?): %w", id, err)
   139  	}
   140  	platformLabel := info.Labels[labels.Platform]
   141  	if platformLabel == "" {
   142  		platformLabel = platforms.DefaultString()
   143  		log.G(ctx).Warnf("Image lacks label %q, assuming the platform to be %q", labels.Platform, platformLabel)
   144  	}
   145  	ocispecPlatform, err := platforms.Parse(platformLabel)
   146  	if err != nil {
   147  		return nil, err
   148  	}
   149  	log.G(ctx).Debugf("ocispecPlatform=%q", platforms.Format(ocispecPlatform))
   150  	platformMC := platforms.Only(ocispecPlatform)
   151  	baseImg := containerd.NewImageWithPlatform(client, baseImgWithoutPlatform, platformMC)
   152  
   153  	baseImgConfig, _, err := imgutil.ReadImageConfig(ctx, baseImg)
   154  	if err != nil {
   155  		return nil, err
   156  	}
   157  
   158  	// Don't gc me and clean the dirty data after 1 hour!
   159  	ctx, done, err := client.WithLease(ctx, leases.WithRandomID(), leases.WithExpiration(1*time.Hour))
   160  	if err != nil {
   161  		return nil, fmt.Errorf("failed to create lease for diff: %w", err)
   162  	}
   163  	defer done(ctx)
   164  
   165  	rootfsID := identity.ChainID(baseImgConfig.RootFS.DiffIDs).String()
   166  
   167  	randomID := idgen.GenerateID()
   168  	parent, err := sn.View(ctx, randomID, rootfsID)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  	defer sn.Remove(ctx, randomID)
   173  
   174  	var changes []fs.Change
   175  	err = mount.WithReadonlyTempMount(ctx, parent, func(lower string) error {
   176  		return mount.WithReadonlyTempMount(ctx, mounts, func(upper string) error {
   177  			return fs.Changes(ctx, lower, upper, func(ck fs.ChangeKind, s string, fi os.FileInfo, err error) error {
   178  				if err != nil {
   179  					return err
   180  				}
   181  				changes = appendChanges(changes, fs.Change{
   182  					Kind: ck,
   183  					Path: s,
   184  				})
   185  				return nil
   186  			})
   187  		})
   188  	})
   189  	if err != nil {
   190  		return nil, err
   191  	}
   192  
   193  	return changes, err
   194  }
   195  
   196  func appendChanges(changes []fs.Change, new fs.Change) []fs.Change {
   197  	newDir, _ := filepath.Split(new.Path)
   198  	newDirPath := filepath.SplitList(newDir)
   199  
   200  	if len(changes) == 0 {
   201  		for i := 1; i < len(newDirPath); i++ {
   202  			changes = append(changes, fs.Change{
   203  				Kind: fs.ChangeKindModify,
   204  				Path: filepath.Join(newDirPath[:i+1]...),
   205  			})
   206  		}
   207  		return append(changes, new)
   208  	}
   209  	last := changes[len(changes)-1]
   210  	lastDir, _ := filepath.Split(last.Path)
   211  	lastDirPath := filepath.SplitList(lastDir)
   212  	for i := range newDirPath {
   213  		if len(lastDirPath) > i && lastDirPath[i] == newDirPath[i] {
   214  			continue
   215  		}
   216  		changes = append(changes, fs.Change{
   217  			Kind: fs.ChangeKindModify,
   218  			Path: filepath.Join(newDirPath[:i+1]...),
   219  		})
   220  	}
   221  	return append(changes, new)
   222  }
   223  
   224  func diffShellComplete(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) {
   225  	// show container names
   226  	return shellCompleteContainerNames(cmd, nil)
   227  }