github.com/pdmccormick/importable-docker-buildx@v0.0.0-20240426161518-e47091289030/controller/build/build.go (about) 1 package build 2 3 import ( 4 "context" 5 "io" 6 "os" 7 "path/filepath" 8 "strings" 9 "sync" 10 11 "github.com/docker/buildx/build" 12 "github.com/docker/buildx/builder" 13 controllerapi "github.com/docker/buildx/controller/pb" 14 "github.com/docker/buildx/store" 15 "github.com/docker/buildx/store/storeutil" 16 "github.com/docker/buildx/util/buildflags" 17 "github.com/docker/buildx/util/confutil" 18 "github.com/docker/buildx/util/dockerutil" 19 "github.com/docker/buildx/util/platformutil" 20 "github.com/docker/buildx/util/progress" 21 "github.com/docker/cli/cli/command" 22 "github.com/docker/cli/cli/config" 23 dockeropts "github.com/docker/cli/opts" 24 "github.com/docker/go-units" 25 "github.com/moby/buildkit/client" 26 "github.com/moby/buildkit/session/auth/authprovider" 27 "github.com/moby/buildkit/util/grpcerrors" 28 "github.com/pkg/errors" 29 "google.golang.org/grpc/codes" 30 ) 31 32 const defaultTargetName = "default" 33 34 // RunBuild runs the specified build and returns the result. 35 // 36 // NOTE: When an error happens during the build and this function acquires the debuggable *build.ResultHandle, 37 // this function returns it in addition to the error (i.e. it does "return nil, res, err"). The caller can 38 // inspect the result and debug the cause of that error. 39 func RunBuild(ctx context.Context, dockerCli command.Cli, in controllerapi.BuildOptions, inStream io.Reader, progress progress.Writer, generateResult bool) (*client.SolveResponse, *build.ResultHandle, error) { 40 if in.NoCache && len(in.NoCacheFilter) > 0 { 41 return nil, nil, errors.Errorf("--no-cache and --no-cache-filter cannot currently be used together") 42 } 43 44 contexts := map[string]build.NamedContext{} 45 for name, path := range in.NamedContexts { 46 contexts[name] = build.NamedContext{Path: path} 47 } 48 49 opts := build.Options{ 50 Inputs: build.Inputs{ 51 ContextPath: in.ContextPath, 52 DockerfilePath: in.DockerfileName, 53 InStream: inStream, 54 NamedContexts: contexts, 55 }, 56 Ref: in.Ref, 57 BuildArgs: in.BuildArgs, 58 CgroupParent: in.CgroupParent, 59 ExtraHosts: in.ExtraHosts, 60 Labels: in.Labels, 61 NetworkMode: in.NetworkMode, 62 NoCache: in.NoCache, 63 NoCacheFilter: in.NoCacheFilter, 64 Pull: in.Pull, 65 ShmSize: dockeropts.MemBytes(in.ShmSize), 66 Tags: in.Tags, 67 Target: in.Target, 68 Ulimits: controllerUlimitOpt2DockerUlimit(in.Ulimits), 69 GroupRef: in.GroupRef, 70 WithProvenanceResponse: in.WithProvenanceResponse, 71 } 72 73 platforms, err := platformutil.Parse(in.Platforms) 74 if err != nil { 75 return nil, nil, err 76 } 77 opts.Platforms = platforms 78 79 dockerConfig := config.LoadDefaultConfigFile(os.Stderr) 80 opts.Session = append(opts.Session, authprovider.NewDockerAuthProvider(dockerConfig, nil)) 81 82 secrets, err := controllerapi.CreateSecrets(in.Secrets) 83 if err != nil { 84 return nil, nil, err 85 } 86 opts.Session = append(opts.Session, secrets) 87 88 sshSpecs := in.SSH 89 if len(sshSpecs) == 0 && buildflags.IsGitSSH(in.ContextPath) { 90 sshSpecs = append(sshSpecs, &controllerapi.SSH{ID: "default"}) 91 } 92 ssh, err := controllerapi.CreateSSH(sshSpecs) 93 if err != nil { 94 return nil, nil, err 95 } 96 opts.Session = append(opts.Session, ssh) 97 98 outputs, err := controllerapi.CreateExports(in.Exports) 99 if err != nil { 100 return nil, nil, err 101 } 102 if in.ExportPush { 103 var pushUsed bool 104 for i := range outputs { 105 if outputs[i].Type == client.ExporterImage { 106 outputs[i].Attrs["push"] = "true" 107 pushUsed = true 108 } 109 } 110 if !pushUsed { 111 outputs = append(outputs, client.ExportEntry{ 112 Type: client.ExporterImage, 113 Attrs: map[string]string{ 114 "push": "true", 115 }, 116 }) 117 } 118 } 119 if in.ExportLoad { 120 var loadUsed bool 121 for i := range outputs { 122 if outputs[i].Type == client.ExporterDocker { 123 if _, ok := outputs[i].Attrs["dest"]; !ok { 124 loadUsed = true 125 break 126 } 127 } 128 } 129 if !loadUsed { 130 outputs = append(outputs, client.ExportEntry{ 131 Type: client.ExporterDocker, 132 Attrs: map[string]string{}, 133 }) 134 } 135 } 136 137 annotations, err := buildflags.ParseAnnotations(in.Annotations) 138 if err != nil { 139 return nil, nil, err 140 } 141 for _, o := range outputs { 142 for k, v := range annotations { 143 o.Attrs[k.String()] = v 144 } 145 } 146 147 opts.Exports = outputs 148 149 opts.CacheFrom = controllerapi.CreateCaches(in.CacheFrom) 150 opts.CacheTo = controllerapi.CreateCaches(in.CacheTo) 151 152 opts.Attests = controllerapi.CreateAttestations(in.Attests) 153 154 opts.SourcePolicy = in.SourcePolicy 155 156 allow, err := buildflags.ParseEntitlements(in.Allow) 157 if err != nil { 158 return nil, nil, err 159 } 160 opts.Allow = allow 161 162 if in.PrintFunc != nil { 163 opts.PrintFunc = &build.PrintFunc{ 164 Name: in.PrintFunc.Name, 165 Format: in.PrintFunc.Format, 166 IgnoreStatus: in.PrintFunc.IgnoreStatus, 167 } 168 } 169 170 // key string used for kubernetes "sticky" mode 171 contextPathHash, err := filepath.Abs(in.ContextPath) 172 if err != nil { 173 contextPathHash = in.ContextPath 174 } 175 176 // TODO: this should not be loaded this side of the controller api 177 b, err := builder.New(dockerCli, 178 builder.WithName(in.Builder), 179 builder.WithContextPathHash(contextPathHash), 180 ) 181 if err != nil { 182 return nil, nil, err 183 } 184 if err = updateLastActivity(dockerCli, b.NodeGroup); err != nil { 185 return nil, nil, errors.Wrapf(err, "failed to update builder last activity time") 186 } 187 nodes, err := b.LoadNodes(ctx) 188 if err != nil { 189 return nil, nil, err 190 } 191 192 resp, res, err := buildTargets(ctx, dockerCli, nodes, map[string]build.Options{defaultTargetName: opts}, progress, generateResult) 193 err = wrapBuildError(err, false) 194 if err != nil { 195 // NOTE: buildTargets can return *build.ResultHandle even on error. 196 return nil, res, err 197 } 198 return resp, res, nil 199 } 200 201 // buildTargets runs the specified build and returns the result. 202 // 203 // NOTE: When an error happens during the build and this function acquires the debuggable *build.ResultHandle, 204 // this function returns it in addition to the error (i.e. it does "return nil, res, err"). The caller can 205 // inspect the result and debug the cause of that error. 206 func buildTargets(ctx context.Context, dockerCli command.Cli, nodes []builder.Node, opts map[string]build.Options, progress progress.Writer, generateResult bool) (*client.SolveResponse, *build.ResultHandle, error) { 207 var res *build.ResultHandle 208 var resp map[string]*client.SolveResponse 209 var err error 210 if generateResult { 211 var mu sync.Mutex 212 var idx int 213 resp, err = build.BuildWithResultHandler(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.ConfigDir(dockerCli), progress, func(driverIndex int, gotRes *build.ResultHandle) { 214 mu.Lock() 215 defer mu.Unlock() 216 if res == nil || driverIndex < idx { 217 idx, res = driverIndex, gotRes 218 } 219 }) 220 } else { 221 resp, err = build.Build(ctx, nodes, opts, dockerutil.NewClient(dockerCli), confutil.ConfigDir(dockerCli), progress) 222 } 223 if err != nil { 224 return nil, res, err 225 } 226 return resp[defaultTargetName], res, err 227 } 228 229 func wrapBuildError(err error, bake bool) error { 230 if err == nil { 231 return nil 232 } 233 st, ok := grpcerrors.AsGRPCStatus(err) 234 if ok { 235 if st.Code() == codes.Unimplemented && strings.Contains(st.Message(), "unsupported frontend capability moby.buildkit.frontend.contexts") { 236 msg := "current frontend does not support --build-context." 237 if bake { 238 msg = "current frontend does not support defining additional contexts for targets." 239 } 240 msg += " Named contexts are supported since Dockerfile v1.4. Use #syntax directive in Dockerfile or update to latest BuildKit." 241 return &wrapped{err, msg} 242 } 243 } 244 return err 245 } 246 247 type wrapped struct { 248 err error 249 msg string 250 } 251 252 func (w *wrapped) Error() string { 253 return w.msg 254 } 255 256 func (w *wrapped) Unwrap() error { 257 return w.err 258 } 259 260 func updateLastActivity(dockerCli command.Cli, ng *store.NodeGroup) error { 261 txn, release, err := storeutil.GetStore(dockerCli) 262 if err != nil { 263 return err 264 } 265 defer release() 266 return txn.UpdateLastActivity(ng) 267 } 268 269 func controllerUlimitOpt2DockerUlimit(u *controllerapi.UlimitOpt) *dockeropts.UlimitOpt { 270 if u == nil { 271 return nil 272 } 273 values := make(map[string]*units.Ulimit) 274 for k, v := range u.Values { 275 values[k] = &units.Ulimit{ 276 Name: v.Name, 277 Hard: v.Hard, 278 Soft: v.Soft, 279 } 280 } 281 return dockeropts.NewUlimitOpt(&values) 282 }