github.com/containerd/nerdctl@v1.7.7/cmd/nerdctl/compose_ps.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 "strings" 23 "text/tabwriter" 24 "time" 25 26 "github.com/containerd/containerd" 27 "github.com/containerd/containerd/runtime/restart" 28 "github.com/containerd/errdefs" 29 gocni "github.com/containerd/go-cni" 30 "github.com/containerd/log" 31 "github.com/containerd/nerdctl/pkg/clientutil" 32 "github.com/containerd/nerdctl/pkg/cmd/compose" 33 "github.com/containerd/nerdctl/pkg/containerutil" 34 "github.com/containerd/nerdctl/pkg/formatter" 35 "github.com/containerd/nerdctl/pkg/labels" 36 "github.com/containerd/nerdctl/pkg/portutil" 37 "github.com/spf13/cobra" 38 "golang.org/x/sync/errgroup" 39 ) 40 41 func newComposePsCommand() *cobra.Command { 42 var composePsCommand = &cobra.Command{ 43 Use: "ps [flags] [SERVICE...]", 44 Short: "List containers of services", 45 RunE: composePsAction, 46 SilenceUsage: true, 47 SilenceErrors: true, 48 } 49 composePsCommand.Flags().String("format", "table", "Format the output. Supported values: [table|json]") 50 composePsCommand.Flags().String("filter", "", "Filter matches containers based on given conditions") 51 composePsCommand.Flags().StringArray("status", []string{}, "Filter services by status. Values: [paused | restarting | removing | running | dead | created | exited]") 52 composePsCommand.Flags().BoolP("quiet", "q", false, "Only display container IDs") 53 composePsCommand.Flags().Bool("services", false, "Display services") 54 composePsCommand.Flags().BoolP("all", "a", false, "Show all containers (default shows just running)") 55 return composePsCommand 56 } 57 58 type composeContainerPrintable struct { 59 ID string 60 Name string 61 Image string 62 Command string 63 Project string 64 Service string 65 State string 66 Health string // placeholder, lack containerd support. 67 ExitCode uint32 68 // `Publishers` stores docker-compatible ports and used for json output. 69 // `Ports` stores formatted ports and only used for console output. 70 Publishers []PortPublisher 71 Ports string `json:"-"` 72 } 73 74 func composePsAction(cmd *cobra.Command, args []string) error { 75 globalOptions, err := processRootCmdFlags(cmd) 76 if err != nil { 77 return err 78 } 79 format, err := cmd.Flags().GetString("format") 80 if err != nil { 81 return err 82 } 83 if format != "json" && format != "table" { 84 return fmt.Errorf("unsupported format %s, supported formats are: [table|json]", format) 85 } 86 status, err := cmd.Flags().GetStringArray("status") 87 if err != nil { 88 return err 89 } 90 quiet, err := cmd.Flags().GetBool("quiet") 91 if err != nil { 92 return err 93 } 94 displayServices, err := cmd.Flags().GetBool("services") 95 if err != nil { 96 return err 97 } 98 filter, err := cmd.Flags().GetString("filter") 99 if err != nil { 100 return err 101 } 102 if filter != "" { 103 splited := strings.SplitN(filter, "=", 2) 104 if len(splited) != 2 { 105 return fmt.Errorf("invalid argument \"%s\" for \"-f, --filter\": bad format of filter (expected name=value)", filter) 106 } 107 // currently only the 'status' filter is supported 108 if splited[0] != "status" { 109 return fmt.Errorf("invalid filter '%s'", splited[0]) 110 } 111 status = append(status, splited[1]) 112 } 113 114 all, err := cmd.Flags().GetBool("all") 115 if err != nil { 116 return err 117 } 118 119 client, ctx, cancel, err := clientutil.NewClient(cmd.Context(), globalOptions.Namespace, globalOptions.Address) 120 if err != nil { 121 return err 122 } 123 defer cancel() 124 options, err := getComposeOptions(cmd, globalOptions.DebugFull, globalOptions.Experimental) 125 if err != nil { 126 return err 127 } 128 c, err := compose.New(client, globalOptions, options, cmd.OutOrStdout(), cmd.ErrOrStderr()) 129 if err != nil { 130 return err 131 } 132 serviceNames, err := c.ServiceNames(args...) 133 if err != nil { 134 return err 135 } 136 containers, err := c.Containers(ctx, serviceNames...) 137 if err != nil { 138 return err 139 } 140 141 if !all { 142 var upContainers []containerd.Container 143 for _, container := range containers { 144 // cStatus := formatter.ContainerStatus(ctx, c) 145 cStatus, err := containerutil.ContainerStatus(ctx, container) 146 if err != nil { 147 continue 148 } 149 if cStatus.Status == containerd.Running { 150 upContainers = append(upContainers, container) 151 } 152 } 153 containers = upContainers 154 } 155 156 if len(status) != 0 { 157 var filterdContainers []containerd.Container 158 for _, container := range containers { 159 cStatus := statusForFilter(ctx, container) 160 for _, s := range status { 161 if cStatus == s { 162 filterdContainers = append(filterdContainers, container) 163 } 164 } 165 } 166 containers = filterdContainers 167 } 168 169 if quiet { 170 for _, c := range containers { 171 fmt.Fprintln(cmd.OutOrStdout(), c.ID()) 172 } 173 return nil 174 } 175 176 containersPrintable := make([]composeContainerPrintable, len(containers)) 177 eg, ctx := errgroup.WithContext(ctx) 178 for i, container := range containers { 179 i, container := i, container 180 eg.Go(func() error { 181 var p composeContainerPrintable 182 var err error 183 if format == "json" { 184 p, err = composeContainerPrintableJSON(ctx, container) 185 } else { 186 p, err = composeContainerPrintableTab(ctx, container) 187 } 188 if err != nil { 189 return err 190 } 191 containersPrintable[i] = p 192 return nil 193 }) 194 } 195 196 if err := eg.Wait(); err != nil { 197 return err 198 } 199 200 if displayServices { 201 for _, p := range containersPrintable { 202 fmt.Fprintln(cmd.OutOrStdout(), p.Service) 203 } 204 return nil 205 } 206 if format == "json" { 207 outJSON, err := formatter.ToJSON(containersPrintable, "", "") 208 if err != nil { 209 return err 210 } 211 _, err = fmt.Fprint(cmd.OutOrStdout(), outJSON) 212 return err 213 } 214 215 w := tabwriter.NewWriter(cmd.OutOrStdout(), 4, 8, 4, ' ', 0) 216 fmt.Fprintln(w, "NAME\tIMAGE\tCOMMAND\tSERVICE\tSTATUS\tPORTS") 217 for _, p := range containersPrintable { 218 if _, err := fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", 219 p.Name, 220 p.Image, 221 p.Command, 222 p.Service, 223 p.State, 224 p.Ports, 225 ); err != nil { 226 return err 227 } 228 } 229 230 return w.Flush() 231 } 232 233 // composeContainerPrintableTab constructs composeContainerPrintable with fields 234 // only for console output. 235 func composeContainerPrintableTab(ctx context.Context, container containerd.Container) (composeContainerPrintable, error) { 236 info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata) 237 if err != nil { 238 return composeContainerPrintable{}, err 239 } 240 spec, err := container.Spec(ctx) 241 if err != nil { 242 return composeContainerPrintable{}, err 243 } 244 status := formatter.ContainerStatus(ctx, container) 245 if status == "Up" { 246 status = "running" // corresponds to Docker Compose v2.0.1 247 } 248 image, err := container.Image(ctx) 249 if err != nil { 250 return composeContainerPrintable{}, err 251 } 252 253 return composeContainerPrintable{ 254 Name: info.Labels[labels.Name], 255 Image: image.Metadata().Name, 256 Command: formatter.InspectContainerCommandTrunc(spec), 257 Service: info.Labels[labels.ComposeService], 258 State: status, 259 Ports: formatter.FormatPorts(info.Labels), 260 }, nil 261 } 262 263 // composeContainerPrintableTab constructs composeContainerPrintable with fields 264 // only for json output and compatible docker output. 265 func composeContainerPrintableJSON(ctx context.Context, container containerd.Container) (composeContainerPrintable, error) { 266 info, err := container.Info(ctx, containerd.WithoutRefreshedMetadata) 267 if err != nil { 268 return composeContainerPrintable{}, err 269 } 270 spec, err := container.Spec(ctx) 271 if err != nil { 272 return composeContainerPrintable{}, err 273 } 274 275 var ( 276 state string 277 exitCode uint32 278 ) 279 status, err := containerutil.ContainerStatus(ctx, container) 280 if err == nil { 281 // show exitCode only when container is exited/stopped 282 if status.Status == containerd.Stopped { 283 state = "exited" 284 exitCode = status.ExitStatus 285 } else { 286 state = string(status.Status) 287 } 288 } else { 289 state = string(containerd.Unknown) 290 } 291 image, err := container.Image(ctx) 292 if err != nil { 293 return composeContainerPrintable{}, err 294 } 295 296 return composeContainerPrintable{ 297 ID: container.ID(), 298 Name: info.Labels[labels.Name], 299 Image: image.Metadata().Name, 300 Command: formatter.InspectContainerCommand(spec, false, false), 301 Project: info.Labels[labels.ComposeProject], 302 Service: info.Labels[labels.ComposeService], 303 State: state, 304 Health: "", 305 ExitCode: exitCode, 306 Publishers: formatPublishers(info.Labels), 307 }, nil 308 } 309 310 // PortPublisher hold status about published port 311 // Use this to match the json output with docker compose 312 // FYI: https://github.com/docker/compose/blob/v2.13.0/pkg/api/api.go#L305C27-L311 313 type PortPublisher struct { 314 URL string 315 TargetPort int 316 PublishedPort int 317 Protocol string 318 } 319 320 // formatPublishers parses and returns docker-compatible []PortPublisher from 321 // label map. If an error happens, an empty slice is returned. 322 func formatPublishers(labelMap map[string]string) []PortPublisher { 323 mapper := func(pm gocni.PortMapping) PortPublisher { 324 return PortPublisher{ 325 URL: pm.HostIP, 326 TargetPort: int(pm.ContainerPort), 327 PublishedPort: int(pm.HostPort), 328 Protocol: pm.Protocol, 329 } 330 } 331 332 var dockerPorts []PortPublisher 333 if portMappings, err := portutil.ParsePortsLabel(labelMap); err == nil { 334 for _, p := range portMappings { 335 dockerPorts = append(dockerPorts, mapper(p)) 336 } 337 } else { 338 log.L.Error(err.Error()) 339 } 340 return dockerPorts 341 } 342 343 // statusForFilter returns the status value to be matched with the 'status' filter 344 func statusForFilter(ctx context.Context, c containerd.Container) string { 345 // Just in case, there is something wrong in server. 346 ctx, cancel := context.WithTimeout(ctx, 5*time.Second) 347 defer cancel() 348 349 task, err := c.Task(ctx, nil) 350 if err != nil { 351 // NOTE: NotFound doesn't mean that container hasn't started. 352 // In docker/CRI-containerd plugin, the task will be deleted 353 // when it exits. So, the status will be "created" for this 354 // case. 355 if errdefs.IsNotFound(err) { 356 return string(containerd.Created) 357 } 358 return string(containerd.Unknown) 359 } 360 361 status, err := task.Status(ctx) 362 if err != nil { 363 return string(containerd.Unknown) 364 } 365 labels, err := c.Labels(ctx) 366 if err != nil { 367 return string(containerd.Unknown) 368 } 369 370 switch s := status.Status; s { 371 case containerd.Stopped: 372 if labels[restart.StatusLabel] == string(containerd.Running) && restart.Reconcile(status, labels) { 373 return "restarting" 374 } 375 return "exited" 376 default: 377 return string(s) 378 } 379 }