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 }