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 }