github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/pkg/client/create_builder.go (about)

     1  package client
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"sort"
     7  	"strings"
     8  
     9  	"github.com/Masterminds/semver"
    10  	"github.com/buildpacks/imgutil"
    11  	"github.com/pkg/errors"
    12  	"golang.org/x/text/cases"
    13  	"golang.org/x/text/language"
    14  
    15  	pubbldr "github.com/buildpacks/pack/builder"
    16  	"github.com/buildpacks/pack/internal/builder"
    17  	"github.com/buildpacks/pack/internal/paths"
    18  	"github.com/buildpacks/pack/internal/style"
    19  	"github.com/buildpacks/pack/pkg/buildpack"
    20  	"github.com/buildpacks/pack/pkg/image"
    21  )
    22  
    23  // CreateBuilderOptions is a configuration object used to change the behavior of
    24  // CreateBuilder.
    25  type CreateBuilderOptions struct {
    26  	// The base directory to use to resolve relative assets
    27  	RelativeBaseDir string
    28  
    29  	// Name of the builder.
    30  	BuilderName string
    31  
    32  	// BuildConfigEnv for Builder
    33  	BuildConfigEnv map[string]string
    34  
    35  	// Map of labels to add to the Buildpack
    36  	Labels map[string]string
    37  
    38  	// Configuration that defines the functionality a builder provides.
    39  	Config pubbldr.Config
    40  
    41  	// Skip building image locally, directly publish to a registry.
    42  	// Requires BuilderName to be a valid registry location.
    43  	Publish bool
    44  
    45  	// Buildpack registry name. Defines where all registry buildpacks will be pulled from.
    46  	Registry string
    47  
    48  	// Strategy for updating images before a build.
    49  	PullPolicy image.PullPolicy
    50  
    51  	// List of modules to be flattened
    52  	Flatten buildpack.FlattenModuleInfos
    53  }
    54  
    55  // CreateBuilder creates and saves a builder image to a registry with the provided options.
    56  // If any configuration is invalid, it will error and exit without creating any images.
    57  func (c *Client) CreateBuilder(ctx context.Context, opts CreateBuilderOptions) error {
    58  	if err := c.validateConfig(ctx, opts); err != nil {
    59  		return err
    60  	}
    61  
    62  	bldr, err := c.createBaseBuilder(ctx, opts)
    63  	if err != nil {
    64  		return errors.Wrap(err, "failed to create builder")
    65  	}
    66  
    67  	if err := c.addBuildpacksToBuilder(ctx, opts, bldr); err != nil {
    68  		return errors.Wrap(err, "failed to add buildpacks to builder")
    69  	}
    70  
    71  	if err := c.addExtensionsToBuilder(ctx, opts, bldr); err != nil {
    72  		return errors.Wrap(err, "failed to add extensions to builder")
    73  	}
    74  
    75  	bldr.SetOrder(opts.Config.Order)
    76  	bldr.SetOrderExtensions(opts.Config.OrderExtensions)
    77  
    78  	if opts.Config.Stack.ID != "" {
    79  		bldr.SetStack(opts.Config.Stack)
    80  	}
    81  	bldr.SetRunImage(opts.Config.Run)
    82  	bldr.SetBuildConfigEnv(opts.BuildConfigEnv)
    83  
    84  	return bldr.Save(c.logger, builder.CreatorMetadata{Version: c.version})
    85  }
    86  
    87  func (c *Client) validateConfig(ctx context.Context, opts CreateBuilderOptions) error {
    88  	if err := pubbldr.ValidateConfig(opts.Config); err != nil {
    89  		return errors.Wrap(err, "invalid builder config")
    90  	}
    91  
    92  	if err := c.validateRunImageConfig(ctx, opts); err != nil {
    93  		return errors.Wrap(err, "invalid run image config")
    94  	}
    95  
    96  	return nil
    97  }
    98  
    99  func (c *Client) validateRunImageConfig(ctx context.Context, opts CreateBuilderOptions) error {
   100  	var runImages []imgutil.Image
   101  	for _, r := range opts.Config.Run.Images {
   102  		for _, i := range append([]string{r.Image}, r.Mirrors...) {
   103  			if !opts.Publish {
   104  				img, err := c.imageFetcher.Fetch(ctx, i, image.FetchOptions{Daemon: true, PullPolicy: opts.PullPolicy})
   105  				if err != nil {
   106  					if errors.Cause(err) != image.ErrNotFound {
   107  						return errors.Wrap(err, "failed to fetch image")
   108  					}
   109  				} else {
   110  					runImages = append(runImages, img)
   111  					continue
   112  				}
   113  			}
   114  
   115  			img, err := c.imageFetcher.Fetch(ctx, i, image.FetchOptions{Daemon: false, PullPolicy: opts.PullPolicy})
   116  			if err != nil {
   117  				if errors.Cause(err) != image.ErrNotFound {
   118  					return errors.Wrap(err, "failed to fetch image")
   119  				}
   120  				c.logger.Warnf("run image %s is not accessible", style.Symbol(i))
   121  			} else {
   122  				runImages = append(runImages, img)
   123  			}
   124  		}
   125  	}
   126  
   127  	for _, img := range runImages {
   128  		if opts.Config.Stack.ID != "" {
   129  			stackID, err := img.Label("io.buildpacks.stack.id")
   130  			if err != nil {
   131  				return errors.Wrap(err, "failed to label image")
   132  			}
   133  
   134  			if stackID != opts.Config.Stack.ID {
   135  				return fmt.Errorf(
   136  					"stack %s from builder config is incompatible with stack %s from run image %s",
   137  					style.Symbol(opts.Config.Stack.ID),
   138  					style.Symbol(stackID),
   139  					style.Symbol(img.Name()),
   140  				)
   141  			}
   142  		}
   143  	}
   144  
   145  	return nil
   146  }
   147  
   148  func (c *Client) createBaseBuilder(ctx context.Context, opts CreateBuilderOptions) (*builder.Builder, error) {
   149  	baseImage, err := c.imageFetcher.Fetch(ctx, opts.Config.Build.Image, image.FetchOptions{Daemon: !opts.Publish, PullPolicy: opts.PullPolicy})
   150  	if err != nil {
   151  		return nil, errors.Wrap(err, "fetch build image")
   152  	}
   153  
   154  	c.logger.Debugf("Creating builder %s from build-image %s", style.Symbol(opts.BuilderName), style.Symbol(baseImage.Name()))
   155  
   156  	var builderOpts []builder.BuilderOption
   157  	if opts.Flatten != nil && len(opts.Flatten.FlattenModules()) > 0 {
   158  		builderOpts = append(builderOpts, builder.WithFlattened(opts.Flatten))
   159  	}
   160  	if opts.Labels != nil && len(opts.Labels) > 0 {
   161  		builderOpts = append(builderOpts, builder.WithLabels(opts.Labels))
   162  	}
   163  
   164  	bldr, err := builder.New(baseImage, opts.BuilderName, builderOpts...)
   165  	if err != nil {
   166  		return nil, errors.Wrap(err, "invalid build-image")
   167  	}
   168  
   169  	architecture, err := baseImage.Architecture()
   170  	if err != nil {
   171  		return nil, errors.Wrap(err, "lookup image Architecture")
   172  	}
   173  
   174  	os, err := baseImage.OS()
   175  	if err != nil {
   176  		return nil, errors.Wrap(err, "lookup image OS")
   177  	}
   178  
   179  	if os == "windows" && !c.experimental {
   180  		return nil, NewExperimentError("Windows containers support is currently experimental.")
   181  	}
   182  
   183  	bldr.SetDescription(opts.Config.Description)
   184  
   185  	if opts.Config.Stack.ID != "" && bldr.StackID != opts.Config.Stack.ID {
   186  		return nil, fmt.Errorf(
   187  			"stack %s from builder config is incompatible with stack %s from build image",
   188  			style.Symbol(opts.Config.Stack.ID),
   189  			style.Symbol(bldr.StackID),
   190  		)
   191  	}
   192  
   193  	lifecycle, err := c.fetchLifecycle(ctx, opts.Config.Lifecycle, opts.RelativeBaseDir, os, architecture)
   194  	if err != nil {
   195  		return nil, errors.Wrap(err, "fetch lifecycle")
   196  	}
   197  
   198  	bldr.SetLifecycle(lifecycle)
   199  	bldr.SetBuildConfigEnv(opts.BuildConfigEnv)
   200  
   201  	return bldr, nil
   202  }
   203  
   204  func (c *Client) fetchLifecycle(ctx context.Context, config pubbldr.LifecycleConfig, relativeBaseDir, os string, architecture string) (builder.Lifecycle, error) {
   205  	if config.Version != "" && config.URI != "" {
   206  		return nil, errors.Errorf(
   207  			"%s can only declare %s or %s, not both",
   208  			style.Symbol("lifecycle"), style.Symbol("version"), style.Symbol("uri"),
   209  		)
   210  	}
   211  
   212  	var uri string
   213  	var err error
   214  	switch {
   215  	case config.Version != "":
   216  		v, err := semver.NewVersion(config.Version)
   217  		if err != nil {
   218  			return nil, errors.Wrapf(err, "%s must be a valid semver", style.Symbol("lifecycle.version"))
   219  		}
   220  
   221  		uri = uriFromLifecycleVersion(*v, os, architecture)
   222  	case config.URI != "":
   223  		uri, err = paths.FilePathToURI(config.URI, relativeBaseDir)
   224  		if err != nil {
   225  			return nil, err
   226  		}
   227  	default:
   228  		uri = uriFromLifecycleVersion(*semver.MustParse(builder.DefaultLifecycleVersion), os, architecture)
   229  	}
   230  
   231  	blob, err := c.downloader.Download(ctx, uri)
   232  	if err != nil {
   233  		return nil, errors.Wrap(err, "downloading lifecycle")
   234  	}
   235  
   236  	lifecycle, err := builder.NewLifecycle(blob)
   237  	if err != nil {
   238  		return nil, errors.Wrap(err, "invalid lifecycle")
   239  	}
   240  
   241  	return lifecycle, nil
   242  }
   243  
   244  func (c *Client) addBuildpacksToBuilder(ctx context.Context, opts CreateBuilderOptions, bldr *builder.Builder) error {
   245  	for _, b := range opts.Config.Buildpacks {
   246  		if err := c.addConfig(ctx, buildpack.KindBuildpack, b, opts, bldr); err != nil {
   247  			return err
   248  		}
   249  	}
   250  	return nil
   251  }
   252  
   253  func (c *Client) addExtensionsToBuilder(ctx context.Context, opts CreateBuilderOptions, bldr *builder.Builder) error {
   254  	for _, e := range opts.Config.Extensions {
   255  		if err := c.addConfig(ctx, buildpack.KindExtension, e, opts, bldr); err != nil {
   256  			return err
   257  		}
   258  	}
   259  	return nil
   260  }
   261  
   262  func (c *Client) addConfig(ctx context.Context, kind string, config pubbldr.ModuleConfig, opts CreateBuilderOptions, bldr *builder.Builder) error {
   263  	c.logger.Debugf("Looking up %s %s", kind, style.Symbol(config.DisplayString()))
   264  
   265  	builderOS, err := bldr.Image().OS()
   266  	if err != nil {
   267  		return errors.Wrapf(err, "getting builder OS")
   268  	}
   269  	builderArch, err := bldr.Image().Architecture()
   270  	if err != nil {
   271  		return errors.Wrapf(err, "getting builder architecture")
   272  	}
   273  
   274  	mainBP, depBPs, err := c.buildpackDownloader.Download(ctx, config.URI, buildpack.DownloadOptions{
   275  		Daemon:          !opts.Publish,
   276  		ImageName:       config.ImageName,
   277  		ImageOS:         builderOS,
   278  		Platform:        fmt.Sprintf("%s/%s", builderOS, builderArch),
   279  		ModuleKind:      kind,
   280  		PullPolicy:      opts.PullPolicy,
   281  		RegistryName:    opts.Registry,
   282  		RelativeBaseDir: opts.RelativeBaseDir,
   283  	})
   284  	if err != nil {
   285  		return errors.Wrapf(err, "downloading %s", kind)
   286  	}
   287  	err = validateModule(kind, mainBP, config.URI, config.ID, config.Version)
   288  	if err != nil {
   289  		return errors.Wrapf(err, "invalid %s", kind)
   290  	}
   291  
   292  	bpDesc := mainBP.Descriptor()
   293  	for _, deprecatedAPI := range bldr.LifecycleDescriptor().APIs.Buildpack.Deprecated {
   294  		if deprecatedAPI.Equal(bpDesc.API()) {
   295  			c.logger.Warnf(
   296  				"%s %s is using deprecated Buildpacks API version %s",
   297  				cases.Title(language.AmericanEnglish).String(kind),
   298  				style.Symbol(bpDesc.Info().FullName()),
   299  				style.Symbol(bpDesc.API().String()),
   300  			)
   301  			break
   302  		}
   303  	}
   304  
   305  	// Fixes 1453
   306  	sort.Slice(depBPs, func(i, j int) bool {
   307  		compareID := strings.Compare(depBPs[i].Descriptor().Info().ID, depBPs[j].Descriptor().Info().ID)
   308  		if compareID == 0 {
   309  			return strings.Compare(depBPs[i].Descriptor().Info().Version, depBPs[j].Descriptor().Info().Version) <= 0
   310  		}
   311  		return compareID < 0
   312  	})
   313  
   314  	switch kind {
   315  	case buildpack.KindBuildpack:
   316  		bldr.AddBuildpacks(mainBP, depBPs)
   317  	case buildpack.KindExtension:
   318  		// Extensions can't be composite
   319  		bldr.AddExtension(mainBP)
   320  	default:
   321  		return fmt.Errorf("unknown module kind: %s", kind)
   322  	}
   323  	return nil
   324  }
   325  
   326  func validateModule(kind string, module buildpack.BuildModule, source, expectedID, expectedVersion string) error {
   327  	info := module.Descriptor().Info()
   328  	if expectedID != "" && info.ID != expectedID {
   329  		return fmt.Errorf(
   330  			"%s from URI %s has ID %s which does not match ID %s from builder config",
   331  			kind,
   332  			style.Symbol(source),
   333  			style.Symbol(info.ID),
   334  			style.Symbol(expectedID),
   335  		)
   336  	}
   337  
   338  	if expectedVersion != "" && info.Version != expectedVersion {
   339  		return fmt.Errorf(
   340  			"%s from URI %s has version %s which does not match version %s from builder config",
   341  			kind,
   342  			style.Symbol(source),
   343  			style.Symbol(info.Version),
   344  			style.Symbol(expectedVersion),
   345  		)
   346  	}
   347  
   348  	return nil
   349  }
   350  
   351  func uriFromLifecycleVersion(version semver.Version, os string, architecture string) string {
   352  	arch := "x86-64"
   353  
   354  	if os == "windows" {
   355  		return fmt.Sprintf("https://github.com/buildpacks/lifecycle/releases/download/v%s/lifecycle-v%s+windows.%s.tgz", version.String(), version.String(), arch)
   356  	}
   357  
   358  	if architecture == "arm64" {
   359  		arch = architecture
   360  	}
   361  
   362  	return fmt.Sprintf("https://github.com/buildpacks/lifecycle/releases/download/v%s/lifecycle-v%s+linux.%s.tgz", version.String(), version.String(), arch)
   363  }