github.com/containerd/nerdctl@v1.7.7/pkg/composer/run.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 composer 18 19 import ( 20 "context" 21 "errors" 22 "fmt" 23 "sync" 24 25 "github.com/compose-spec/compose-go/loader" 26 "github.com/compose-spec/compose-go/types" 27 "github.com/containerd/log" 28 "github.com/containerd/nerdctl/pkg/composer/serviceparser" 29 "github.com/containerd/nerdctl/pkg/idgen" 30 "golang.org/x/sync/errgroup" 31 ) 32 33 type RunOptions struct { 34 ServiceName string 35 Args []string 36 37 NoBuild bool 38 NoColor bool 39 NoLogPrefix bool 40 ForceBuild bool 41 QuietPull bool 42 RemoveOrphans bool 43 44 Name string 45 Detach bool 46 NoDeps bool 47 Tty bool 48 SigProxy bool 49 Interactive bool 50 Rm bool 51 User string 52 Volume []string 53 Entrypoint []string 54 Env []string 55 Label []string 56 WorkDir string 57 ServicePorts bool 58 Publish []string 59 } 60 61 func (c *Composer) Run(ctx context.Context, ro RunOptions) error { 62 for shortName := range c.project.Networks { 63 if err := c.upNetwork(ctx, shortName); err != nil { 64 return err 65 } 66 } 67 68 for shortName := range c.project.Volumes { 69 if err := c.upVolume(ctx, shortName); err != nil { 70 return err 71 } 72 } 73 74 for shortName, secret := range c.project.Secrets { 75 obj := types.FileObjectConfig(secret) 76 if err := validateFileObjectConfig(obj, shortName, "service", c.project); err != nil { 77 return err 78 } 79 } 80 81 for shortName, config := range c.project.Configs { 82 obj := types.FileObjectConfig(config) 83 if err := validateFileObjectConfig(obj, shortName, "config", c.project); err != nil { 84 return err 85 } 86 } 87 88 var svcs []types.ServiceConfig 89 90 if ro.NoDeps { 91 svc, err := c.project.GetService(ro.ServiceName) 92 if err != nil { 93 return err 94 } 95 svcs = append(svcs, svc) 96 } else { 97 if err := c.project.WithServices([]string{ro.ServiceName}, func(svc types.ServiceConfig) error { 98 svcs = append(svcs, svc) 99 return nil 100 }); err != nil { 101 return err 102 } 103 } 104 105 var targetSvc *types.ServiceConfig 106 for i := range svcs { 107 if svcs[i].Name == ro.ServiceName { 108 targetSvc = &svcs[i] 109 break 110 } 111 } 112 if targetSvc == nil { 113 return fmt.Errorf("error cannot find service name: %s", ro.ServiceName) 114 } 115 116 for i := range svcs { 117 // FYI: https://github.com/docker/compose/blob/v2.18.1/pkg/compose/run.go#L65 118 svcs[i].ContainerName = fmt.Sprintf("%[1]s%[4]s%[2]s%[4]srun%[4]s%[3]s", c.project.Name, svcs[i].Name, idgen.TruncateID(idgen.GenerateID()), serviceparser.Separator) 119 } 120 121 targetSvc.Tty = ro.Tty 122 targetSvc.StdinOpen = ro.Interactive 123 124 if ro.Name != "" { 125 targetSvc.ContainerName = ro.Name 126 } 127 if ro.User != "" { 128 targetSvc.User = ro.User 129 } 130 if len(ro.Volume) > 0 { 131 for _, v := range ro.Volume { 132 vc, err := loader.ParseVolume(v) 133 if err != nil { 134 return err 135 } 136 targetSvc.Volumes = append(targetSvc.Volumes, vc) 137 } 138 } 139 if len(ro.Entrypoint) > 0 { 140 targetSvc.Entrypoint = make([]string, len(ro.Entrypoint)) 141 copy(targetSvc.Entrypoint, ro.Entrypoint) 142 } 143 if len(ro.Env) > 0 { 144 envs := types.NewMappingWithEquals(ro.Env) 145 targetSvc.Environment.OverrideBy(envs) 146 } 147 if len(ro.Label) > 0 { 148 label := types.NewMappingWithEquals(ro.Label) 149 for k, v := range label { 150 if v != nil { 151 targetSvc.Labels.Add(k, *v) 152 } 153 } 154 } 155 if ro.WorkDir != "" { 156 c.project.WorkingDir = ro.WorkDir 157 } 158 159 // `compose run` command does not create any of the ports specified in the service configuration. 160 if !ro.ServicePorts { 161 for k := range svcs { 162 svcs[k].Ports = []types.ServicePortConfig{} 163 } 164 if len(ro.Publish) > 0 { 165 for _, p := range ro.Publish { 166 pc, err := types.ParsePortConfig(p) 167 if err != nil { 168 return fmt.Errorf("error parse --publish: %s", err) 169 } 170 targetSvc.Ports = append(targetSvc.Ports, pc...) 171 } 172 } 173 } 174 175 // `compose run` command overrides the command defined in the service configuration. 176 if len(ro.Args) != 0 { 177 targetSvc.Command = make([]string, len(ro.Args)) 178 copy(targetSvc.Command, ro.Args) 179 } 180 181 parsedServices := make([]*serviceparser.Service, 0) 182 for _, svc := range svcs { 183 ps, err := serviceparser.Parse(c.project, svc) 184 if err != nil { 185 return err 186 } 187 parsedServices = append(parsedServices, ps) 188 } 189 190 // remove orphan containers before the service has be started 191 // FYI: https://github.com/docker/compose/blob/v2.3.4/pkg/compose/create.go#L91-L112 192 orphans, err := c.getOrphanContainers(ctx, parsedServices) 193 if err != nil && ro.RemoveOrphans { 194 return fmt.Errorf("error getting orphaned containers: %s", err) 195 } 196 if len(orphans) > 0 { 197 if ro.RemoveOrphans { 198 if err := c.removeContainers(ctx, orphans, RemoveOptions{Stop: true, Volumes: true}); err != nil { 199 return fmt.Errorf("error removing orphaned containers: %s", err) 200 } 201 } else { 202 log.G(ctx).Warnf("found %d orphaned containers: %v, you can run this command with the --remove-orphans flag to clean it up", len(orphans), orphans) 203 } 204 } 205 206 return c.runServices(ctx, parsedServices, ro) 207 } 208 209 func (c *Composer) runServices(ctx context.Context, parsedServices []*serviceparser.Service, ro RunOptions) error { 210 if len(parsedServices) == 0 { 211 return errors.New("no service was provided") 212 } 213 214 // TODO: parallelize loop for ensuring images (make sure not to mess up tty) 215 for _, ps := range parsedServices { 216 if err := c.ensureServiceImage(ctx, ps, !ro.NoBuild, ro.ForceBuild, BuildOptions{}, ro.QuietPull); err != nil { 217 return err 218 } 219 } 220 221 var ( 222 containers = make(map[string]serviceparser.Container) // key: container ID 223 services = []string{} 224 containersMu sync.Mutex 225 runEG errgroup.Group 226 cid string // For printing cid when -d exists 227 ) 228 229 for _, ps := range parsedServices { 230 ps := ps 231 services = append(services, ps.Unparsed.Name) 232 233 if len(ps.Containers) != 1 { 234 log.G(ctx).Warnf("compose run does not support scale but %s is currently %v, automatically it will configure 1", ps.Unparsed.Name, len(ps.Containers)) 235 } 236 237 if len(ps.Containers) == 0 { 238 return fmt.Errorf("error, a service should have at least one container but %s does not have any container", ps.Unparsed.Name) 239 } 240 container := ps.Containers[0] 241 242 runEG.Go(func() error { 243 id, err := c.upServiceContainer(ctx, ps, container) 244 if err != nil { 245 return err 246 } 247 containersMu.Lock() 248 containers[id] = container 249 containersMu.Unlock() 250 if ps.Unparsed.Name == ro.ServiceName { 251 cid = id 252 } 253 return nil 254 }) 255 } 256 if err := runEG.Wait(); err != nil { 257 return err 258 } 259 260 if ro.Detach { 261 log.G(ctx).Printf("%s\n", cid) 262 return nil 263 } 264 265 // TODO: fix it when `nerdctl logs` supports `nerdctl run` without detach 266 // https://github.com/containerd/nerdctl/blob/v0.22.2/pkg/taskutil/taskutil.go#L55 267 if !ro.Interactive && !ro.Tty { 268 log.G(ctx).Info("Attaching to logs") 269 lo := LogsOptions{ 270 Follow: true, 271 NoColor: ro.NoColor, 272 NoLogPrefix: ro.NoLogPrefix, 273 } 274 // it finally causes to show logs of some containers which are stopped but not deleted. 275 if err := c.Logs(ctx, lo, services); err != nil { 276 return err 277 } 278 } 279 280 log.G(ctx).Infof("Stopping containers (forcibly)") // TODO: support gracefully stopping 281 c.stopContainersFromParsedServices(ctx, containers) 282 283 if ro.Rm { 284 c.removeContainersFromParsedServices(ctx, containers) 285 } 286 return nil 287 }