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 }