github.com/containerd/nerdctl@v1.7.7/pkg/composer/composer.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  	"encoding/json"
    22  	"errors"
    23  	"fmt"
    24  	"os/exec"
    25  
    26  	composecli "github.com/compose-spec/compose-go/cli"
    27  	compose "github.com/compose-spec/compose-go/types"
    28  	"github.com/containerd/containerd"
    29  	"github.com/containerd/containerd/identifiers"
    30  	"github.com/containerd/log"
    31  	"github.com/containerd/nerdctl/pkg/composer/serviceparser"
    32  	"github.com/containerd/nerdctl/pkg/reflectutil"
    33  )
    34  
    35  // Options groups the command line options recommended for a Compose implementation (ProjectOptions) and extra options for nerdctl
    36  type Options struct {
    37  	Project          string // empty for default
    38  	ProjectDirectory string
    39  	ConfigPaths      []string
    40  	Profiles         []string
    41  	Services         []string
    42  	EnvFile          string
    43  	NerdctlCmd       string
    44  	NerdctlArgs      []string
    45  	NetworkInUse     func(ctx context.Context, netName string) (bool, error)
    46  	NetworkExists    func(string) (bool, error)
    47  	VolumeExists     func(string) (bool, error)
    48  	ImageExists      func(ctx context.Context, imageName string) (bool, error)
    49  	EnsureImage      func(ctx context.Context, imageName, pullMode, platform string, ps *serviceparser.Service, quiet bool) error
    50  	DebugPrintFull   bool // full debug print, may leak secret env var to logs
    51  	Experimental     bool // enable experimental features
    52  	IPFSAddress      string
    53  }
    54  
    55  func New(o Options, client *containerd.Client) (*Composer, error) {
    56  	if o.NerdctlCmd == "" {
    57  		return nil, errors.New("got empty nerdctl cmd")
    58  	}
    59  	if o.NetworkExists == nil || o.VolumeExists == nil || o.EnsureImage == nil {
    60  		return nil, errors.New("got empty functions")
    61  	}
    62  
    63  	if o.Project != "" {
    64  		if err := identifiers.Validate(o.Project); err != nil {
    65  			return nil, fmt.Errorf("got invalid project name %q: %w", o.Project, err)
    66  		}
    67  	}
    68  
    69  	var optionsFn []composecli.ProjectOptionsFn
    70  	optionsFn = append(optionsFn,
    71  		composecli.WithOsEnv,
    72  		composecli.WithWorkingDirectory(o.ProjectDirectory),
    73  	)
    74  	if o.EnvFile != "" {
    75  		optionsFn = append(optionsFn,
    76  			composecli.WithEnvFiles(o.EnvFile),
    77  		)
    78  	}
    79  	optionsFn = append(optionsFn,
    80  		composecli.WithConfigFileEnv,
    81  		composecli.WithDefaultConfigPath,
    82  		composecli.WithDotEnv,
    83  		composecli.WithName(o.Project),
    84  	)
    85  
    86  	projectOptions, err := composecli.NewProjectOptions(o.ConfigPaths, optionsFn...)
    87  	if err != nil {
    88  		return nil, err
    89  	}
    90  	project, err := composecli.ProjectFromOptions(projectOptions)
    91  	if err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	if len(o.Services) > 0 {
    96  		s, err := project.GetServices(o.Services...)
    97  		if err != nil {
    98  			return nil, err
    99  		}
   100  		o.Profiles = append(o.Profiles, s.GetProfiles()...)
   101  	}
   102  	project.ApplyProfiles(o.Profiles)
   103  
   104  	if o.DebugPrintFull {
   105  		projectJSON, _ := json.MarshalIndent(project, "", "    ")
   106  		log.L.Debug("printing project JSON")
   107  		log.L.Debugf("%s", projectJSON)
   108  	}
   109  
   110  	if unknown := reflectutil.UnknownNonEmptyFields(project,
   111  		"Name",
   112  		"WorkingDir",
   113  		"Environment",
   114  		"Services",
   115  		"Networks",
   116  		"Volumes",
   117  		"Secrets",
   118  		"Configs",
   119  		"ComposeFiles"); len(unknown) > 0 {
   120  		log.L.Warnf("Ignoring: %+v", unknown)
   121  	}
   122  
   123  	c := &Composer{
   124  		Options: o,
   125  		project: project,
   126  		client:  client,
   127  	}
   128  
   129  	return c, nil
   130  }
   131  
   132  type Composer struct {
   133  	Options
   134  	project *compose.Project
   135  	client  *containerd.Client
   136  }
   137  
   138  func (c *Composer) createNerdctlCmd(ctx context.Context, args ...string) *exec.Cmd {
   139  	return exec.CommandContext(ctx, c.NerdctlCmd, append(c.NerdctlArgs, args...)...)
   140  }
   141  
   142  func (c *Composer) runNerdctlCmd(ctx context.Context, args ...string) error {
   143  	cmd := c.createNerdctlCmd(ctx, args...)
   144  	if c.DebugPrintFull {
   145  		log.G(ctx).Debugf("Running %v", cmd.Args)
   146  	}
   147  	if out, err := cmd.CombinedOutput(); err != nil {
   148  		return fmt.Errorf("error while executing %v: %q: %w", cmd.Args, string(out), err)
   149  	}
   150  	return nil
   151  }
   152  
   153  // Services returns the parsed Service objects in dependency order.
   154  func (c *Composer) Services(ctx context.Context, svcs ...string) ([]*serviceparser.Service, error) {
   155  	var services []*serviceparser.Service
   156  	if err := c.project.WithServices(svcs, func(svc compose.ServiceConfig) error {
   157  		parsed, err := serviceparser.Parse(c.project, svc)
   158  		if err != nil {
   159  			return err
   160  		}
   161  		services = append(services, parsed)
   162  		return nil
   163  	}); err != nil {
   164  		return nil, err
   165  	}
   166  	return services, nil
   167  }
   168  
   169  // ServiceNames returns service names in dependency order.
   170  func (c *Composer) ServiceNames(svcs ...string) ([]string, error) {
   171  	var names []string
   172  	if err := c.project.WithServices(svcs, func(svc compose.ServiceConfig) error {
   173  		names = append(names, svc.Name)
   174  		return nil
   175  	}); err != nil {
   176  		return nil, err
   177  	}
   178  	return names, nil
   179  }