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  }