github.com/containerd/nerdctl@v1.7.7/pkg/composer/create.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 "fmt" 22 "os" 23 "path/filepath" 24 "strings" 25 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/labels" 30 "golang.org/x/sync/errgroup" 31 ) 32 33 // FYI: https://github.com/docker/compose/blob/v2.14.1/pkg/api/api.go#L423 34 const ( 35 // RecreateNever specifies never recreating existing service containers 36 RecreateNever = "never" 37 // RecreateForce specifies always force-recreating service containers 38 RecreateForce = "force" 39 // RecreateDiverged specifies only recreating service containers which diverges from compose model. 40 // (Unimplemented, currently equal to `RecreateNever`) In docker-compose, 41 // service config is hashed and stored in a label. 42 // FYI: https://github.com/docker/compose/blob/v2.14.1/pkg/compose/convergence.go#L244 43 RecreateDiverged = "diverged" 44 ) 45 46 // CreateOptions stores all option input from `nerdctl compose create` 47 type CreateOptions struct { 48 Build bool 49 NoBuild bool 50 ForceRecreate bool 51 NoRecreate bool 52 Pull *string 53 } 54 55 func (opts CreateOptions) recreateStrategy() string { 56 switch { 57 case opts.ForceRecreate: 58 return RecreateForce 59 case opts.NoRecreate: 60 return RecreateNever 61 default: 62 return RecreateDiverged 63 } 64 } 65 66 // Create creates containers for given services. 67 func (c *Composer) Create(ctx context.Context, opt CreateOptions, services []string) error { 68 // preprocess services based on options (for all project services, in case 69 // there are dependencies not in `services`) 70 for i, service := range c.project.Services { 71 if opt.Pull != nil { 72 service.PullPolicy = *opt.Pull 73 } 74 if opt.Build && service.Build != nil { 75 service.PullPolicy = types.PullPolicyBuild 76 } 77 if opt.NoBuild { 78 service.Build = nil 79 if service.Image == "" { 80 service.Image = fmt.Sprintf("%s_%s", c.project.Name, service.Name) 81 } 82 } 83 c.project.Services[i] = service 84 } 85 86 // prepare other components (networks, volumes, configs) 87 for shortName := range c.project.Networks { 88 if err := c.upNetwork(ctx, shortName); err != nil { 89 return err 90 } 91 } 92 93 for shortName := range c.project.Volumes { 94 if err := c.upVolume(ctx, shortName); err != nil { 95 return err 96 } 97 } 98 99 for shortName, secret := range c.project.Secrets { 100 obj := types.FileObjectConfig(secret) 101 if err := validateFileObjectConfig(obj, shortName, "service", c.project); err != nil { 102 return err 103 } 104 } 105 106 for shortName, config := range c.project.Configs { 107 obj := types.FileObjectConfig(config) 108 if err := validateFileObjectConfig(obj, shortName, "config", c.project); err != nil { 109 return err 110 } 111 } 112 113 // ensure images 114 // TODO: parallelize loop for ensuring images (make sure not to mess up tty) 115 parsedServices, err := c.Services(ctx, services...) 116 if err != nil { 117 return err 118 } 119 for _, ps := range parsedServices { 120 if err := c.ensureServiceImage(ctx, ps, !opt.NoBuild, opt.Build, BuildOptions{}, false); err != nil { 121 return err 122 } 123 } 124 125 for _, ps := range parsedServices { 126 if err := c.createService(ctx, ps, opt); err != nil { 127 return err 128 } 129 } 130 131 return nil 132 } 133 134 func (c *Composer) createService(ctx context.Context, ps *serviceparser.Service, opt CreateOptions) error { 135 recreate := opt.recreateStrategy() 136 var runEG errgroup.Group 137 for _, container := range ps.Containers { 138 container := container 139 runEG.Go(func() error { 140 _, err := c.createServiceContainer(ctx, ps, container, recreate) 141 if err != nil { 142 return err 143 } 144 return nil 145 }) 146 } 147 return runEG.Wait() 148 } 149 150 // createServiceContainer must be called after ensureServiceImage 151 // createServiceContainer returns container ID 152 // TODO(djdongjin): refactor needed: 153 // 1. the logic is similar to `upServiceContainer`, need to decouple some of the logic. 154 // 2. ideally, `compose up` should equal to `compose create` + `compose start`, we should decouple and reuse the logic in `compose up`. 155 // 3. it'll be easier to refactor after related `compose` logic are moved to `pkg` from `cmd`. 156 func (c *Composer) createServiceContainer(ctx context.Context, service *serviceparser.Service, container serviceparser.Container, recreate string) (string, error) { 157 // check if container already exists 158 exists, err := c.containerExists(ctx, container.Name, service.Unparsed.Name) 159 if err != nil { 160 return "", fmt.Errorf("error while checking for containers with name %q: %s", container.Name, err) 161 } 162 163 // delete container if it already exists and force-recreate is enabled 164 if exists { 165 if recreate != RecreateForce { 166 log.G(ctx).Infof("Container %s exists, skipping", container.Name) 167 return "", nil 168 } 169 170 log.G(ctx).Debugf("Container %q already exists and force-created is enabled, deleting", container.Name) 171 delCmd := c.createNerdctlCmd(ctx, "rm", "-f", container.Name) 172 if err = delCmd.Run(); err != nil { 173 return "", fmt.Errorf("could not delete container %q: %s", container.Name, err) 174 } 175 log.G(ctx).Infof("Re-creating container %s", container.Name) 176 } else { 177 log.G(ctx).Infof("Creating container %s", container.Name) 178 } 179 180 tempDir, err := os.MkdirTemp(os.TempDir(), "compose-") 181 if err != nil { 182 return "", fmt.Errorf("error while creating/re-creating container %s: %w", container.Name, err) 183 } 184 defer os.RemoveAll(tempDir) 185 cidFilename := filepath.Join(tempDir, "cid") 186 187 //add metadata labels to container https://github.com/compose-spec/compose-spec/blob/master/spec.md#labels 188 container.RunArgs = append([]string{ 189 "--cidfile=" + cidFilename, 190 fmt.Sprintf("-l=%s=%s", labels.ComposeProject, c.project.Name), 191 fmt.Sprintf("-l=%s=%s", labels.ComposeService, service.Unparsed.Name), 192 }, container.RunArgs...) 193 194 cmd := c.createNerdctlCmd(ctx, append([]string{"create"}, container.RunArgs...)...) 195 if c.DebugPrintFull { 196 log.G(ctx).Debugf("Running %v", cmd.Args) 197 } 198 199 // FIXME 200 if service.Unparsed.StdinOpen != service.Unparsed.Tty { 201 return "", fmt.Errorf("currently StdinOpen(-i) and Tty(-t) should be same") 202 } 203 204 err = cmd.Run() 205 if err != nil { 206 return "", fmt.Errorf("error while creating container %s: %w", container.Name, err) 207 } 208 209 cid, err := os.ReadFile(cidFilename) 210 if err != nil { 211 return "", fmt.Errorf("error while creating container %s: %w", container.Name, err) 212 } 213 return strings.TrimSpace(string(cid)), nil 214 }