github.com/buildpacks/pack@v0.33.3-0.20240516162812-884dd1837311/internal/commands/build.go (about)

     1  package commands
     2  
     3  import (
     4  	"os"
     5  	"path/filepath"
     6  	"strconv"
     7  	"strings"
     8  	"time"
     9  
    10  	"github.com/buildpacks/pack/pkg/cache"
    11  
    12  	"github.com/google/go-containerregistry/pkg/name"
    13  	"github.com/pkg/errors"
    14  	"github.com/spf13/cobra"
    15  
    16  	"github.com/buildpacks/pack/internal/config"
    17  	"github.com/buildpacks/pack/internal/style"
    18  	"github.com/buildpacks/pack/pkg/client"
    19  	"github.com/buildpacks/pack/pkg/image"
    20  	"github.com/buildpacks/pack/pkg/logging"
    21  	"github.com/buildpacks/pack/pkg/project"
    22  	projectTypes "github.com/buildpacks/pack/pkg/project/types"
    23  )
    24  
    25  type BuildFlags struct {
    26  	Publish              bool
    27  	ClearCache           bool
    28  	TrustBuilder         bool
    29  	Interactive          bool
    30  	Sparse               bool
    31  	DockerHost           string
    32  	CacheImage           string
    33  	Cache                cache.CacheOpts
    34  	AppPath              string
    35  	Builder              string
    36  	Registry             string
    37  	RunImage             string
    38  	Policy               string
    39  	Network              string
    40  	DescriptorPath       string
    41  	DefaultProcessType   string
    42  	LifecycleImage       string
    43  	Env                  []string
    44  	EnvFiles             []string
    45  	Buildpacks           []string
    46  	Extensions           []string
    47  	Volumes              []string
    48  	AdditionalTags       []string
    49  	Workspace            string
    50  	GID                  int
    51  	UID                  int
    52  	PreviousImage        string
    53  	SBOMDestinationDir   string
    54  	ReportDestinationDir string
    55  	DateTime             string
    56  	PreBuildpacks        []string
    57  	PostBuildpacks       []string
    58  }
    59  
    60  // Build an image from source code
    61  func Build(logger logging.Logger, cfg config.Config, packClient PackClient) *cobra.Command {
    62  	var flags BuildFlags
    63  
    64  	cmd := &cobra.Command{
    65  		Use:     "build <image-name>",
    66  		Args:    cobra.ExactArgs(1),
    67  		Short:   "Generate app image from source code",
    68  		Example: "pack build test_img --path apps/test-app --builder cnbs/sample-builder:bionic",
    69  		Long: "Pack Build uses Cloud Native Buildpacks to create a runnable app image from source code.\n\nPack Build " +
    70  			"requires an image name, which will be generated from the source code. Build defaults to the current directory, " +
    71  			"but you can use `--path` to specify another source code directory. Build requires a `builder`, which can either " +
    72  			"be provided directly to build using `--builder`, or can be set using the `set-default-builder` command. For more " +
    73  			"on how to use `pack build`, see: https://buildpacks.io/docs/app-developer-guide/build-an-app/.",
    74  		RunE: logError(logger, func(cmd *cobra.Command, args []string) error {
    75  			inputImageName := client.ParseInputImageReference(args[0])
    76  			if err := validateBuildFlags(&flags, cfg, inputImageName, logger); err != nil {
    77  				return err
    78  			}
    79  
    80  			inputPreviousImage := client.ParseInputImageReference(flags.PreviousImage)
    81  
    82  			descriptor, actualDescriptorPath, err := parseProjectToml(flags.AppPath, flags.DescriptorPath, logger)
    83  			if err != nil {
    84  				return err
    85  			}
    86  
    87  			if actualDescriptorPath != "" {
    88  				logger.Debugf("Using project descriptor located at %s", style.Symbol(actualDescriptorPath))
    89  			}
    90  
    91  			builder := flags.Builder
    92  			// We only override the builder to the one in the project descriptor
    93  			// if it was not explicitly set by the user
    94  			if !cmd.Flags().Changed("builder") && descriptor.Build.Builder != "" {
    95  				builder = descriptor.Build.Builder
    96  			}
    97  
    98  			if builder == "" {
    99  				suggestSettingBuilder(logger, packClient)
   100  				return client.NewSoftError()
   101  			}
   102  
   103  			buildpacks := flags.Buildpacks
   104  			extensions := flags.Extensions
   105  
   106  			env, err := parseEnv(flags.EnvFiles, flags.Env)
   107  			if err != nil {
   108  				return err
   109  			}
   110  
   111  			trustBuilder := isTrustedBuilder(cfg, builder) || flags.TrustBuilder
   112  			if trustBuilder {
   113  				logger.Debugf("Builder %s is trusted", style.Symbol(builder))
   114  				if flags.LifecycleImage != "" {
   115  					logger.Warn("Ignoring the provided lifecycle image as the builder is trusted, running the creator in a single container using the provided builder")
   116  				}
   117  			} else {
   118  				logger.Debugf("Builder %s is untrusted", style.Symbol(builder))
   119  				logger.Debug("As a result, the phases of the lifecycle which require root access will be run in separate trusted ephemeral containers.")
   120  				logger.Debug("For more information, see https://medium.com/buildpacks/faster-more-secure-builds-with-pack-0-11-0-4d0c633ca619")
   121  			}
   122  
   123  			if !trustBuilder && len(flags.Volumes) > 0 {
   124  				logger.Warn("Using untrusted builder with volume mounts. If there is sensitive data in the volumes, this may present a security vulnerability.")
   125  			}
   126  
   127  			stringPolicy := flags.Policy
   128  			if stringPolicy == "" {
   129  				stringPolicy = cfg.PullPolicy
   130  			}
   131  			pullPolicy, err := image.ParsePullPolicy(stringPolicy)
   132  			if err != nil {
   133  				return errors.Wrapf(err, "parsing pull policy %s", flags.Policy)
   134  			}
   135  			var lifecycleImage string
   136  			if flags.LifecycleImage != "" {
   137  				ref, err := name.ParseReference(flags.LifecycleImage)
   138  				if err != nil {
   139  					return errors.Wrapf(err, "parsing lifecycle image %s", flags.LifecycleImage)
   140  				}
   141  				lifecycleImage = ref.Name()
   142  			}
   143  			var gid = -1
   144  			if cmd.Flags().Changed("gid") {
   145  				gid = flags.GID
   146  			}
   147  
   148  			var uid = -1
   149  			if cmd.Flags().Changed("uid") {
   150  				uid = flags.UID
   151  			}
   152  
   153  			dateTime, err := parseTime(flags.DateTime)
   154  			if err != nil {
   155  				return errors.Wrapf(err, "parsing creation time %s", flags.DateTime)
   156  			}
   157  			if err := packClient.Build(cmd.Context(), client.BuildOptions{
   158  				AppPath:           flags.AppPath,
   159  				Builder:           builder,
   160  				Registry:          flags.Registry,
   161  				AdditionalMirrors: getMirrors(cfg),
   162  				AdditionalTags:    flags.AdditionalTags,
   163  				RunImage:          flags.RunImage,
   164  				Env:               env,
   165  				Image:             inputImageName.Name(),
   166  				Publish:           flags.Publish,
   167  				DockerHost:        flags.DockerHost,
   168  				PullPolicy:        pullPolicy,
   169  				ClearCache:        flags.ClearCache,
   170  				TrustBuilder: func(string) bool {
   171  					return trustBuilder
   172  				},
   173  				Buildpacks: buildpacks,
   174  				Extensions: extensions,
   175  				ContainerConfig: client.ContainerConfig{
   176  					Network: flags.Network,
   177  					Volumes: flags.Volumes,
   178  				},
   179  				DefaultProcessType:       flags.DefaultProcessType,
   180  				ProjectDescriptorBaseDir: filepath.Dir(actualDescriptorPath),
   181  				ProjectDescriptor:        descriptor,
   182  				Cache:                    flags.Cache,
   183  				CacheImage:               flags.CacheImage,
   184  				Workspace:                flags.Workspace,
   185  				LifecycleImage:           lifecycleImage,
   186  				GroupID:                  gid,
   187  				UserID:                   uid,
   188  				PreviousImage:            inputPreviousImage.Name(),
   189  				Interactive:              flags.Interactive,
   190  				SBOMDestinationDir:       flags.SBOMDestinationDir,
   191  				ReportDestinationDir:     flags.ReportDestinationDir,
   192  				CreationTime:             dateTime,
   193  				PreBuildpacks:            flags.PreBuildpacks,
   194  				PostBuildpacks:           flags.PostBuildpacks,
   195  				LayoutConfig: &client.LayoutConfig{
   196  					Sparse:             flags.Sparse,
   197  					InputImage:         inputImageName,
   198  					PreviousInputImage: inputPreviousImage,
   199  					LayoutRepoDir:      cfg.LayoutRepositoryDir,
   200  				},
   201  			}); err != nil {
   202  				return errors.Wrap(err, "failed to build")
   203  			}
   204  			logger.Infof("Successfully built image %s", style.Symbol(inputImageName.Name()))
   205  			return nil
   206  		}),
   207  	}
   208  	buildCommandFlags(cmd, &flags, cfg)
   209  	AddHelpFlag(cmd, "build")
   210  	return cmd
   211  }
   212  
   213  func parseTime(providedTime string) (*time.Time, error) {
   214  	var parsedTime time.Time
   215  	switch providedTime {
   216  	case "":
   217  		return nil, nil
   218  	case "now":
   219  		parsedTime = time.Now().UTC()
   220  	default:
   221  		intTime, err := strconv.ParseInt(providedTime, 10, 64)
   222  		if err != nil {
   223  			return nil, errors.Wrap(err, "parsing unix timestamp")
   224  		}
   225  		parsedTime = time.Unix(intTime, 0).UTC()
   226  	}
   227  	return &parsedTime, nil
   228  }
   229  
   230  func buildCommandFlags(cmd *cobra.Command, buildFlags *BuildFlags, cfg config.Config) {
   231  	cmd.Flags().StringVarP(&buildFlags.AppPath, "path", "p", "", "Path to app dir or zip-formatted file (defaults to current working directory)")
   232  	cmd.Flags().StringSliceVarP(&buildFlags.Buildpacks, "buildpack", "b", nil, "Buildpack to use. One of:\n  a buildpack by id and version in the form of '<buildpack>@<version>',\n  path to a buildpack directory (not supported on Windows),\n  path/URL to a buildpack .tar or .tgz file, or\n  a packaged buildpack image name in the form of '<hostname>/<repo>[:<tag>]'"+stringSliceHelp("buildpack"))
   233  	cmd.Flags().StringSliceVarP(&buildFlags.Extensions, "extension", "", nil, "Extension to use. One of:\n  an extension by id and version in the form of '<extension>@<version>',\n  path to an extension directory (not supported on Windows),\n  path/URL to an extension .tar or .tgz file, or\n  a packaged extension image name in the form of '<hostname>/<repo>[:<tag>]'"+stringSliceHelp("extension"))
   234  	cmd.Flags().StringVarP(&buildFlags.Builder, "builder", "B", cfg.DefaultBuilder, "Builder image")
   235  	cmd.Flags().Var(&buildFlags.Cache, "cache",
   236  		`Cache options used to define cache techniques for build process.
   237  - Cache as bind: 'type=<build/launch>;format=bind;source=<path to directory>'
   238  - Cache as image (requires --publish): 'type=<build/launch>;format=image;name=<registry image name>'
   239  - Cache as volume: 'type=<build/launch>;format=volume;[name=<volume name>]'
   240      - If no name is provided, a random name will be generated.
   241  `)
   242  	cmd.Flags().StringVar(&buildFlags.CacheImage, "cache-image", "", `Cache build layers in remote registry. Requires --publish`)
   243  	cmd.Flags().BoolVar(&buildFlags.ClearCache, "clear-cache", false, "Clear image's associated cache before building")
   244  	cmd.Flags().StringVar(&buildFlags.DateTime, "creation-time", "", "Desired create time in the output image config. Accepted values are Unix timestamps (e.g., '1641013200'), or 'now'. Platform API version must be at least 0.9 to use this feature.")
   245  	cmd.Flags().StringVarP(&buildFlags.DescriptorPath, "descriptor", "d", "", "Path to the project descriptor file")
   246  	cmd.Flags().StringVarP(&buildFlags.DefaultProcessType, "default-process", "D", "", `Set the default process type. (default "web")`)
   247  	cmd.Flags().StringArrayVarP(&buildFlags.Env, "env", "e", []string{}, "Build-time environment variable, in the form 'VAR=VALUE' or 'VAR'.\nWhen using latter value-less form, value will be taken from current\n  environment at the time this command is executed.\nThis flag may be specified multiple times and will override\n  individual values defined by --env-file."+stringArrayHelp("env")+"\nNOTE: These are NOT available at image runtime.")
   248  	cmd.Flags().StringArrayVar(&buildFlags.EnvFiles, "env-file", []string{}, "Build-time environment variables file\nOne variable per line, of the form 'VAR=VALUE' or 'VAR'\nWhen using latter value-less form, value will be taken from current\n  environment at the time this command is executed\nNOTE: These are NOT available at image runtime.\"")
   249  	cmd.Flags().StringVar(&buildFlags.Network, "network", "", "Connect detect and build containers to network")
   250  	cmd.Flags().StringArrayVar(&buildFlags.PreBuildpacks, "pre-buildpack", []string{}, "Buildpacks to prepend to the groups in the builder's order")
   251  	cmd.Flags().StringArrayVar(&buildFlags.PostBuildpacks, "post-buildpack", []string{}, "Buildpacks to append to the groups in the builder's order")
   252  	cmd.Flags().BoolVar(&buildFlags.Publish, "publish", false, "Publish the application image directly to the container registry specified in <image-name>, instead of the daemon. The run image must also reside in the registry.")
   253  	cmd.Flags().StringVar(&buildFlags.DockerHost, "docker-host", "",
   254  		`Address to docker daemon that will be exposed to the build container.
   255  If not set (or set to empty string) the standard socket location will be used.
   256  Special value 'inherit' may be used in which case DOCKER_HOST environment variable will be used.
   257  This option may set DOCKER_HOST environment variable for the build container if needed.
   258  `)
   259  	cmd.Flags().StringVar(&buildFlags.LifecycleImage, "lifecycle-image", cfg.LifecycleImage, `Custom lifecycle image to use for analysis, restore, and export when builder is untrusted.`)
   260  	cmd.Flags().StringVar(&buildFlags.Policy, "pull-policy", "", `Pull policy to use. Accepted values are always, never, and if-not-present. (default "always")`)
   261  	cmd.Flags().StringVarP(&buildFlags.Registry, "buildpack-registry", "r", cfg.DefaultRegistryName, "Buildpack Registry by name")
   262  	cmd.Flags().StringVar(&buildFlags.RunImage, "run-image", "", "Run image (defaults to default stack's run image)")
   263  	cmd.Flags().StringSliceVarP(&buildFlags.AdditionalTags, "tag", "t", nil, "Additional tags to push the output image to.\nTags should be in the format 'image:tag' or 'repository/image:tag'."+stringSliceHelp("tag"))
   264  	cmd.Flags().BoolVar(&buildFlags.TrustBuilder, "trust-builder", false, "Trust the provided builder.\nAll lifecycle phases will be run in a single container.\nFor more on trusted builders, and when to trust or untrust a builder, check out our docs here: https://buildpacks.io/docs/tools/pack/concepts/trusted_builders")
   265  	cmd.Flags().StringArrayVar(&buildFlags.Volumes, "volume", nil, "Mount host volume into the build container, in the form '<host path>:<target path>[:<options>]'.\n- 'host path': Name of the volume or absolute directory path to mount.\n- 'target path': The path where the file or directory is available in the container.\n- 'options' (default \"ro\"): An optional comma separated list of mount options.\n    - \"ro\", volume contents are read-only.\n    - \"rw\", volume contents are readable and writeable.\n    - \"volume-opt=<key>=<value>\", can be specified more than once, takes a key-value pair consisting of the option name and its value."+stringArrayHelp("volume"))
   266  	cmd.Flags().StringVar(&buildFlags.Workspace, "workspace", "", "Location at which to mount the app dir in the build image")
   267  	cmd.Flags().IntVar(&buildFlags.GID, "gid", 0, `Override GID of user's group in the stack's build and run images. The provided value must be a positive number`)
   268  	cmd.Flags().IntVar(&buildFlags.UID, "uid", 0, `Override UID of user in the stack's build and run images. The provided value must be a positive number`)
   269  	cmd.Flags().StringVar(&buildFlags.PreviousImage, "previous-image", "", "Set previous image to a particular tag reference, digest reference, or (when performing a daemon build) image ID")
   270  	cmd.Flags().StringVar(&buildFlags.SBOMDestinationDir, "sbom-output-dir", "", "Path to export SBoM contents.\nOmitting the flag will yield no SBoM content.")
   271  	cmd.Flags().StringVar(&buildFlags.ReportDestinationDir, "report-output-dir", "", "Path to export build report.toml.\nOmitting the flag yield no report file.")
   272  	cmd.Flags().BoolVar(&buildFlags.Interactive, "interactive", false, "Launch a terminal UI to depict the build process")
   273  	cmd.Flags().BoolVar(&buildFlags.Sparse, "sparse", false, "Use this flag to avoid saving on disk the run-image layers when the application image is exported to OCI layout format")
   274  	if !cfg.Experimental {
   275  		cmd.Flags().MarkHidden("interactive")
   276  		cmd.Flags().MarkHidden("sparse")
   277  	}
   278  }
   279  
   280  func validateBuildFlags(flags *BuildFlags, cfg config.Config, inputImageRef client.InputImageReference, logger logging.Logger) error {
   281  	if flags.Registry != "" && !cfg.Experimental {
   282  		return client.NewExperimentError("Support for buildpack registries is currently experimental.")
   283  	}
   284  
   285  	if flags.Cache.Launch.Format == cache.CacheImage {
   286  		logger.Warn("cache definition: 'launch' cache in format 'image' is not supported.")
   287  	}
   288  
   289  	if flags.Cache.Build.Format == cache.CacheImage && flags.CacheImage != "" {
   290  		return errors.New("'cache' flag with 'image' format cannot be used with 'cache-image' flag.")
   291  	}
   292  
   293  	if flags.Cache.Build.Format == cache.CacheImage && !flags.Publish {
   294  		return errors.New("image cache format requires the 'publish' flag")
   295  	}
   296  
   297  	if flags.CacheImage != "" && !flags.Publish {
   298  		return errors.New("cache-image flag requires the publish flag")
   299  	}
   300  
   301  	if flags.GID < 0 {
   302  		return errors.New("gid flag must be in the range of 0-2147483647")
   303  	}
   304  
   305  	if flags.UID < 0 {
   306  		return errors.New("uid flag must be in the range of 0-2147483647")
   307  	}
   308  
   309  	if flags.Interactive && !cfg.Experimental {
   310  		return client.NewExperimentError("Interactive mode is currently experimental.")
   311  	}
   312  
   313  	if inputImageRef.Layout() && !cfg.Experimental {
   314  		return client.NewExperimentError("Exporting to OCI layout is currently experimental.")
   315  	}
   316  
   317  	return nil
   318  }
   319  
   320  func parseEnv(envFiles []string, envVars []string) (map[string]string, error) {
   321  	env := map[string]string{}
   322  
   323  	for _, envFile := range envFiles {
   324  		envFileVars, err := parseEnvFile(envFile)
   325  		if err != nil {
   326  			return nil, errors.Wrapf(err, "failed to parse env file '%s'", envFile)
   327  		}
   328  
   329  		for k, v := range envFileVars {
   330  			env[k] = v
   331  		}
   332  	}
   333  	for _, envVar := range envVars {
   334  		env = addEnvVar(env, envVar)
   335  	}
   336  	return env, nil
   337  }
   338  
   339  func parseEnvFile(filename string) (map[string]string, error) {
   340  	out := make(map[string]string)
   341  	f, err := os.ReadFile(filepath.Clean(filename))
   342  	if err != nil {
   343  		return nil, errors.Wrapf(err, "open %s", filename)
   344  	}
   345  	for _, line := range strings.Split(string(f), "\n") {
   346  		line = strings.TrimSpace(line)
   347  		if line == "" {
   348  			continue
   349  		}
   350  		out = addEnvVar(out, line)
   351  	}
   352  	return out, nil
   353  }
   354  
   355  func addEnvVar(env map[string]string, item string) map[string]string {
   356  	arr := strings.SplitN(item, "=", 2)
   357  	if len(arr) > 1 {
   358  		env[arr[0]] = arr[1]
   359  	} else {
   360  		env[arr[0]] = os.Getenv(arr[0])
   361  	}
   362  	return env
   363  }
   364  
   365  func parseProjectToml(appPath, descriptorPath string, logger logging.Logger) (projectTypes.Descriptor, string, error) {
   366  	actualPath := descriptorPath
   367  	computePath := descriptorPath == ""
   368  
   369  	if computePath {
   370  		actualPath = filepath.Join(appPath, "project.toml")
   371  	}
   372  
   373  	if _, err := os.Stat(actualPath); err != nil {
   374  		if computePath {
   375  			return projectTypes.Descriptor{}, "", nil
   376  		}
   377  		return projectTypes.Descriptor{}, "", errors.Wrap(err, "stat project descriptor")
   378  	}
   379  
   380  	descriptor, err := project.ReadProjectDescriptor(actualPath, logger)
   381  	return descriptor, actualPath, err
   382  }