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  }