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 }