get.porter.sh/porter@v1.3.0/pkg/build/buildkit/buildx.go (about) 1 package buildkit 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "os" 9 "path/filepath" 10 "regexp" 11 "strings" 12 13 "get.porter.sh/porter/pkg/build" 14 "get.porter.sh/porter/pkg/cnab" 15 "get.porter.sh/porter/pkg/config" 16 "get.porter.sh/porter/pkg/manifest" 17 "get.porter.sh/porter/pkg/tracing" 18 "github.com/cnabio/cnab-go/driver/docker" 19 buildx "github.com/docker/buildx/build" 20 "github.com/docker/buildx/builder" 21 _ "github.com/docker/buildx/driver/docker" // Register the docker driver with buildkit 22 "github.com/docker/buildx/util/buildflags" 23 "github.com/docker/buildx/util/confutil" 24 "github.com/docker/buildx/util/dockerutil" 25 "github.com/docker/buildx/util/progress" 26 dockerconfig "github.com/docker/cli/cli/config" 27 "github.com/moby/buildkit/session" 28 "github.com/moby/buildkit/session/auth/authprovider" 29 "github.com/moby/buildkit/util/progress/progressui" 30 "go.opentelemetry.io/otel/attribute" 31 ) 32 33 // MaxArgLength is the maximum length of a build argument that can be passed to Docker 34 // Each system has its own max (see https://stackoverflow.com/questions/70737793/max-size-of-string-cmd-that-can-be-passed-to-docker) 35 // I am choosing 5,000 characters because that's lower than all supported OS/ARCH combinations 36 const MaxArgLength = 5000 37 38 type Builder struct { 39 *config.Config 40 } 41 42 func NewBuilder(cfg *config.Config) *Builder { 43 return &Builder{ 44 Config: cfg, 45 } 46 } 47 48 var _ io.Writer = unstructuredLogger{} 49 50 // take lines from the docker output, and write them as info messages 51 // This allows the docker library to use our logger like an io.Writer 52 type unstructuredLogger struct { 53 logger tracing.TraceLogger 54 } 55 56 var newline = []byte("\n") 57 58 func (l unstructuredLogger) Write(p []byte) (n int, err error) { 59 if l.logger == nil { 60 return 0, nil 61 } 62 63 msg := string(bytes.TrimSuffix(p, newline)) 64 l.logger.Info(msg) 65 return len(p), nil 66 } 67 68 func (b *Builder) BuildBundleImage(ctx context.Context, manifest *manifest.Manifest, opts build.BuildImageOptions) error { 69 ctx, span := tracing.StartSpan(ctx, attribute.String("image", manifest.Image)) 70 defer span.EndSpan() 71 72 span.Info("Building bundle image") 73 74 cli, err := docker.GetDockerClient() 75 if err != nil { 76 return span.Error(err) 77 } 78 79 bldr, err := builder.New(cli, 80 builder.WithName(cli.CurrentContext()), 81 builder.WithContextPathHash(b.Getwd()), 82 ) 83 if err != nil { 84 return span.Error(err) 85 } 86 nodes, err := bldr.LoadNodes(ctx) 87 if err != nil { 88 return span.Error(err) 89 } 90 91 currentSession := []session.Attachable{authprovider.NewDockerAuthProvider(authprovider.DockerAuthProviderConfig{ 92 ConfigFile: dockerconfig.LoadDefaultConfigFile(b.Err), 93 TLSConfigs: make(map[string]*authprovider.AuthTLSConfig), 94 })} 95 96 ssh, err := buildflags.ParseSSHSpecs(opts.SSH) 97 if err != nil { 98 return span.Errorf("error parsing the --ssh flags: %w", err) 99 } 100 101 pbssh, err := buildx.CreateSSH(ssh) 102 if err != nil { 103 return span.Errorf("error creating ssh ", err) 104 } 105 106 currentSession = append(currentSession, pbssh) 107 108 secrets, err := buildflags.ParseSecretSpecs(opts.Secrets) 109 if err != nil { 110 return span.Errorf("error parsing the --secret flags: %w", err) 111 } 112 pbsecrets, err := buildx.CreateSecrets(secrets) 113 if err != nil { 114 return span.Errorf("error creating secrets %w", err) 115 } 116 117 currentSession = append(currentSession, pbsecrets) 118 119 args, err := b.determineBuildArgs(ctx, manifest, opts) 120 if err != nil { 121 return err 122 } 123 span.SetAttributes(tracing.ObjectAttribute("build-args", args)) 124 125 buildContexts, err := buildflags.ParseContextNames(opts.BuildContexts) 126 if err != nil { 127 return span.Errorf("error parsing the --build-context flags: %w", err) 128 } 129 130 buildxOpts := map[string]buildx.Options{ 131 "default": { 132 Tags: []string{manifest.Image}, 133 Inputs: buildx.Inputs{ 134 ContextPath: b.Getwd(), 135 DockerfilePath: b.getDockerfilePath(), 136 InStream: buildx.NewSyncMultiReader(b.In), 137 NamedContexts: toNamedContexts(buildContexts), 138 }, 139 BuildArgs: args, 140 Session: currentSession, 141 NoCache: opts.NoCache, 142 }, 143 } 144 145 mode := progressui.AutoMode // Auto writes to stderr regardless of what you pass in 146 printer, err := progress.NewPrinter(ctx, os.Stderr, mode) 147 if err != nil { 148 return span.Error(err) 149 } 150 151 _, buildErr := buildx.Build(ctx, nodes, buildxOpts, dockerutil.NewClient(cli), confutil.NewConfig(cli), printer) 152 printErr := printer.Wait() 153 154 if buildErr == nil && printErr != nil { 155 return span.Errorf("error with docker printer: %w", printErr) 156 } 157 158 if buildErr != nil { 159 return span.Errorf("error building docker image: %w", buildErr) 160 } 161 162 return nil 163 } 164 165 func (b *Builder) getDockerfilePath() string { 166 return filepath.Join(b.Getwd(), build.DOCKER_FILE) 167 } 168 169 func (b *Builder) determineBuildArgs( 170 ctx context.Context, 171 manifest *manifest.Manifest, 172 opts build.BuildImageOptions) (map[string]string, error) { 173 174 _, span := tracing.StartSpan(ctx) 175 defer span.EndSpan() 176 177 // This will grow later when we add custom build args from the porter.yaml 178 args := make(map[string]string, len(opts.BuildArgs)+1) 179 180 // Create a map of key/values from the custom field in porter.yaml 181 convertedCustomInput, err := flattenMap(manifest.Custom) 182 if err != nil { 183 return nil, span.Error(err) 184 } 185 186 // Determine which (if any) custom fields from porter.yaml are used in the Dockerfile 187 dockerfilePath := b.getDockerfilePath() 188 dockerfileContents, err := b.FileSystem.ReadFile(dockerfilePath) 189 if err != nil { 190 return nil, span.Errorf("Error reading Dockerfile at %s: %w", dockerfilePath, err) 191 } 192 customBuildArgs, err := detectCustomBuildArgsUsed(string(dockerfileContents)) 193 if err != nil { 194 return nil, span.Errorf("Error parsing custom build arguments from the Dockerfile at %s: %w", dockerfilePath, err) 195 } 196 197 // Pass custom values as build args when building the bundle image 198 argNameRegex := regexp.MustCompile(`[^A-Z0-9_]`) 199 for k, v := range convertedCustomInput { 200 // Make all arg names upper-case 201 argName := fmt.Sprintf("CUSTOM_%s", strings.ToUpper(k)) 202 203 // replace characters that can't be in an argument name with _ 204 argName = argNameRegex.ReplaceAllString(argName, "_") 205 206 // Only add build args for custom values used in the Dockerfile 207 if _, ok := customBuildArgs[argName]; ok { 208 args[argName] = v 209 } 210 } 211 212 // Add explicit build arguments next they should override what was determined from the porter.yaml to allow the user to fix anything unexpected 213 parseBuildArgs(opts.BuildArgs, args) 214 215 // Add porter defined build arguments last 216 args["BUNDLE_DIR"] = build.BUNDLE_DIR 217 218 // Check if any arguments are too long 219 for k, v := range args { 220 if len(v) > MaxArgLength { 221 return nil, span.Errorf("The length of the build argument %s is longer than the max (%d characters). Save the value to a file in the bundle directory, and then read the file contents out in a custom dockerfile or in the bundle at runtime to work around this limitation.", k, MaxArgLength) 222 } 223 } 224 225 return args, nil 226 } 227 228 func detectCustomBuildArgsUsed(dockerFileContents string) (map[string]struct{}, error) { 229 customBuildArgRegex := regexp.MustCompile(`ARG (CUSTOM_([a-zA-Z0-9_]+))`) 230 231 matches := customBuildArgRegex.FindAllStringSubmatch(dockerFileContents, -1) 232 argNames := make(map[string]struct{}, len(matches)) 233 for _, match := range matches { 234 argNames[match[1]] = struct{}{} 235 } 236 237 return argNames, nil 238 } 239 240 func parseBuildArgs(unparsed []string, parsed map[string]string) { 241 for _, arg := range unparsed { 242 parts := strings.SplitN(arg, "=", 2) 243 if len(parts) < 2 { 244 // docker ignores --build-arg with only one part, so we will too 245 continue 246 } 247 248 name := parts[0] 249 value := parts[1] 250 parsed[name] = value 251 } 252 } 253 254 func toNamedContexts(m map[string]string) map[string]buildx.NamedContext { 255 m2 := make(map[string]buildx.NamedContext, len(m)) 256 for k, v := range m { 257 m2[k] = buildx.NamedContext{Path: v} 258 } 259 return m2 260 } 261 262 func (b *Builder) TagBundleImage(ctx context.Context, origTag, newTag string) error { 263 ctx, log := tracing.StartSpan(ctx, attribute.String("source-tag", origTag), attribute.String("destination-tag", newTag)) 264 defer log.EndSpan() 265 266 cli, err := docker.GetDockerClient() 267 if err != nil { 268 return log.Error(err) 269 } 270 271 if err := cli.Client().ImageTag(ctx, origTag, newTag); err != nil { 272 return log.Errorf("could not tag image %s with value %s: %w", origTag, newTag, err) 273 } 274 return nil 275 } 276 277 // flattenMap recursively walks through nested map and flattens it 278 // to one-level map of key-value with string type. 279 func flattenMap(mapInput map[string]interface{}) (map[string]string, error) { 280 out := make(map[string]string) 281 282 for key, value := range mapInput { 283 switch v := value.(type) { 284 case map[string]interface{}: 285 tmp, err := flattenMap(v) 286 if err != nil { 287 return nil, err 288 } 289 for innerKey, innerValue := range tmp { 290 out[key+"."+innerKey] = innerValue 291 } 292 case map[string]string: 293 for innerKey, innerValue := range v { 294 out[key+"."+innerKey] = innerValue 295 } 296 default: 297 innerValue, err := cnab.WriteParameterToString(key, value) 298 if err != nil { 299 return nil, fmt.Errorf("error representing %s as a build argument: %w", key, err) 300 } 301 out[key] = innerValue 302 } 303 } 304 return out, nil 305 }