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  }