github.com/containerd/nerdctl@v1.7.7/pkg/composer/up_service.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  	"os"
    24  	"path/filepath"
    25  	"strings"
    26  	"sync"
    27  
    28  	"github.com/containerd/log"
    29  	"github.com/containerd/nerdctl/pkg/composer/serviceparser"
    30  	"github.com/containerd/nerdctl/pkg/labels"
    31  
    32  	"golang.org/x/sync/errgroup"
    33  )
    34  
    35  func (c *Composer) upServices(ctx context.Context, parsedServices []*serviceparser.Service, uo UpOptions) error {
    36  	if len(parsedServices) == 0 {
    37  		return errors.New("no service was provided")
    38  	}
    39  
    40  	// TODO: parallelize loop for ensuring images (make sure not to mess up tty)
    41  	for _, ps := range parsedServices {
    42  		if err := c.ensureServiceImage(ctx, ps, !uo.NoBuild, uo.ForceBuild, BuildOptions{}, uo.QuietPull); err != nil {
    43  			return err
    44  		}
    45  	}
    46  
    47  	var (
    48  		containers   = make(map[string]serviceparser.Container) // key: container ID
    49  		services     = []string{}
    50  		containersMu sync.Mutex
    51  	)
    52  	for _, ps := range parsedServices {
    53  		ps := ps
    54  		var runEG errgroup.Group
    55  		services = append(services, ps.Unparsed.Name)
    56  		for _, container := range ps.Containers {
    57  			container := container
    58  			runEG.Go(func() error {
    59  				id, err := c.upServiceContainer(ctx, ps, container)
    60  				if err != nil {
    61  					return err
    62  				}
    63  				containersMu.Lock()
    64  				containers[id] = container
    65  				containersMu.Unlock()
    66  				return nil
    67  			})
    68  		}
    69  		if err := runEG.Wait(); err != nil {
    70  			return err
    71  		}
    72  	}
    73  
    74  	if uo.Detach {
    75  		return nil
    76  	}
    77  
    78  	log.G(ctx).Info("Attaching to logs")
    79  	lo := LogsOptions{
    80  		Follow:      true,
    81  		NoColor:     uo.NoColor,
    82  		NoLogPrefix: uo.NoLogPrefix,
    83  	}
    84  	if err := c.Logs(ctx, lo, services); err != nil {
    85  		return err
    86  	}
    87  
    88  	log.G(ctx).Infof("Stopping containers (forcibly)") // TODO: support gracefully stopping
    89  	c.stopContainersFromParsedServices(ctx, containers)
    90  	return nil
    91  }
    92  
    93  func (c *Composer) ensureServiceImage(ctx context.Context, ps *serviceparser.Service, allowBuild, forceBuild bool, bo BuildOptions, quiet bool) error {
    94  	if ps.Build != nil && allowBuild {
    95  		if ps.Build.Force || forceBuild {
    96  			return c.buildServiceImage(ctx, ps.Image, ps.Build, ps.Unparsed.Platform, bo)
    97  		}
    98  		if ok, err := c.ImageExists(ctx, ps.Image); err != nil {
    99  			return err
   100  		} else if !ok {
   101  			return c.buildServiceImage(ctx, ps.Image, ps.Build, ps.Unparsed.Platform, bo)
   102  		}
   103  		// even when c.ImageExists returns true, we need to call c.EnsureImage
   104  		// because ps.PullMode can be "always". So no return here.
   105  		log.G(ctx).Debugf("Image %s already exists, not building", ps.Image)
   106  	}
   107  
   108  	log.G(ctx).Infof("Ensuring image %s", ps.Image)
   109  	return c.EnsureImage(ctx, ps.Image, ps.PullMode, ps.Unparsed.Platform, ps, quiet)
   110  }
   111  
   112  // upServiceContainer must be called after ensureServiceImage
   113  // upServiceContainer returns container ID
   114  func (c *Composer) upServiceContainer(ctx context.Context, service *serviceparser.Service, container serviceparser.Container) (string, error) {
   115  	// check if container already exists
   116  	exists, err := c.containerExists(ctx, container.Name, service.Unparsed.Name)
   117  	if err != nil {
   118  		return "", fmt.Errorf("error while checking for containers with name %q: %s", container.Name, err)
   119  	}
   120  
   121  	// delete container if it already exists
   122  	if exists {
   123  		log.G(ctx).Debugf("Container %q already exists, deleting", container.Name)
   124  		delCmd := c.createNerdctlCmd(ctx, "rm", "-f", container.Name)
   125  		if err = delCmd.Run(); err != nil {
   126  			return "", fmt.Errorf("could not delete container %q: %s", container.Name, err)
   127  		}
   128  		log.G(ctx).Infof("Re-creating container %s", container.Name)
   129  	} else {
   130  		log.G(ctx).Infof("Creating container %s", container.Name)
   131  	}
   132  
   133  	for _, f := range container.Mkdir {
   134  		log.G(ctx).Debugf("Creating a directory %q", f)
   135  		if err = os.MkdirAll(f, 0o755); err != nil {
   136  			return "", fmt.Errorf("failed to create a directory %q: %w", f, err)
   137  		}
   138  	}
   139  
   140  	tempDir, err := os.MkdirTemp(os.TempDir(), "compose-")
   141  	if err != nil {
   142  		return "", fmt.Errorf("error while creating/re-creating container %s: %w", container.Name, err)
   143  	}
   144  	defer os.RemoveAll(tempDir)
   145  	cidFilename := filepath.Join(tempDir, "cid")
   146  
   147  	var runFlagD bool
   148  	if !service.Unparsed.StdinOpen && !service.Unparsed.Tty {
   149  		container.RunArgs = append([]string{"-d"}, container.RunArgs...)
   150  		runFlagD = true
   151  	}
   152  
   153  	//add metadata labels to container https://github.com/compose-spec/compose-spec/blob/master/spec.md#labels
   154  	container.RunArgs = append([]string{
   155  		"--cidfile=" + cidFilename,
   156  		fmt.Sprintf("-l=%s=%s", labels.ComposeProject, c.project.Name),
   157  		fmt.Sprintf("-l=%s=%s", labels.ComposeService, service.Unparsed.Name),
   158  	}, container.RunArgs...)
   159  
   160  	cmd := c.createNerdctlCmd(ctx, append([]string{"run"}, container.RunArgs...)...)
   161  	if c.DebugPrintFull {
   162  		log.G(ctx).Debugf("Running %v", cmd.Args)
   163  	}
   164  
   165  	// FIXME
   166  	if service.Unparsed.StdinOpen != service.Unparsed.Tty {
   167  		return "", fmt.Errorf("currently StdinOpen(-i) and Tty(-t) should be same")
   168  	}
   169  
   170  	if service.Unparsed.StdinOpen {
   171  		cmd.Stdin = os.Stdin
   172  	}
   173  	if !runFlagD {
   174  		cmd.Stdout = os.Stdout
   175  	}
   176  	// Always propagate stderr to print detailed error messages (https://github.com/containerd/nerdctl/issues/1942)
   177  	cmd.Stderr = os.Stderr
   178  
   179  	err = cmd.Run()
   180  	if err != nil {
   181  		return "", fmt.Errorf("error while creating container %s: %w", container.Name, err)
   182  	}
   183  
   184  	cid, err := os.ReadFile(cidFilename)
   185  	if err != nil {
   186  		return "", fmt.Errorf("error while creating container %s: %w", container.Name, err)
   187  	}
   188  	return strings.TrimSpace(string(cid)), nil
   189  }