github.com/YousefHaggyHeroku/pack@v1.5.5/internal/build/lifecycle_execution.go (about) 1 package build 2 3 import ( 4 "context" 5 "fmt" 6 "math/rand" 7 8 "github.com/buildpacks/lifecycle/api" 9 "github.com/buildpacks/lifecycle/auth" 10 "github.com/docker/docker/client" 11 "github.com/google/go-containerregistry/pkg/authn" 12 "github.com/pkg/errors" 13 14 "github.com/YousefHaggyHeroku/pack/internal/builder" 15 "github.com/YousefHaggyHeroku/pack/internal/cache" 16 "github.com/YousefHaggyHeroku/pack/internal/paths" 17 "github.com/YousefHaggyHeroku/pack/internal/style" 18 "github.com/YousefHaggyHeroku/pack/logging" 19 ) 20 21 const ( 22 defaultProcessType = "web" 23 ) 24 25 type LifecycleExecution struct { 26 logger logging.Logger 27 docker client.CommonAPIClient 28 platformAPI *api.Version 29 layersVolume string 30 appVolume string 31 os string 32 mountPaths mountPaths 33 opts LifecycleOptions 34 } 35 36 func NewLifecycleExecution(logger logging.Logger, docker client.CommonAPIClient, opts LifecycleOptions) (*LifecycleExecution, error) { 37 latestSupportedPlatformAPI, err := findLatestSupported(append( 38 opts.Builder.LifecycleDescriptor().APIs.Platform.Deprecated, 39 opts.Builder.LifecycleDescriptor().APIs.Platform.Supported..., 40 )) 41 if err != nil { 42 return nil, err 43 } 44 45 osType, err := opts.Builder.Image().OS() 46 if err != nil { 47 return nil, err 48 } 49 50 exec := &LifecycleExecution{ 51 logger: logger, 52 docker: docker, 53 layersVolume: paths.FilterReservedNames("pack-layers-" + randString(10)), 54 appVolume: paths.FilterReservedNames("pack-app-" + randString(10)), 55 platformAPI: latestSupportedPlatformAPI, 56 opts: opts, 57 os: osType, 58 mountPaths: mountPathsForOS(osType), 59 } 60 61 return exec, nil 62 } 63 64 func findLatestSupported(apis []*api.Version) (*api.Version, error) { 65 for i := len(SupportedPlatformAPIVersions) - 1; i >= 0; i-- { 66 for _, version := range apis { 67 if SupportedPlatformAPIVersions[i].Equal(version) { 68 return version, nil 69 } 70 } 71 } 72 73 return nil, errors.New("unable to find a supported Platform API version") 74 } 75 76 func randString(n int) string { 77 b := make([]byte, n) 78 for i := range b { 79 b[i] = 'a' + byte(rand.Intn(26)) 80 } 81 return string(b) 82 } 83 84 func (l *LifecycleExecution) Builder() Builder { 85 return l.opts.Builder 86 } 87 88 func (l *LifecycleExecution) AppPath() string { 89 return l.opts.AppPath 90 } 91 92 func (l *LifecycleExecution) AppVolume() string { 93 return l.appVolume 94 } 95 96 func (l *LifecycleExecution) LayersVolume() string { 97 return l.layersVolume 98 } 99 100 func (l *LifecycleExecution) PlatformAPI() *api.Version { 101 return l.platformAPI 102 } 103 104 func (l *LifecycleExecution) Run(ctx context.Context, phaseFactoryCreator PhaseFactoryCreator) error { 105 phaseFactory := phaseFactoryCreator(l) 106 107 buildCache := cache.NewVolumeCache(l.opts.Image, "build", l.docker) 108 109 l.logger.Debugf("Using build cache volume %s", style.Symbol(buildCache.Name())) 110 if l.opts.ClearCache { 111 if err := buildCache.Clear(ctx); err != nil { 112 return errors.Wrap(err, "clearing build cache") 113 } 114 l.logger.Debugf("Build cache %s cleared", style.Symbol(buildCache.Name())) 115 } 116 117 launchCache := cache.NewVolumeCache(l.opts.Image, "launch", l.docker) 118 119 if !l.opts.UseCreator { 120 l.logger.Info(style.Step("DETECTING")) 121 if err := l.Detect(ctx, l.opts.Network, l.opts.Volumes, phaseFactory); err != nil { 122 return err 123 } 124 125 l.logger.Info(style.Step("ANALYZING")) 126 if err := l.Analyze(ctx, l.opts.Image.String(), buildCache.Name(), l.opts.Network, l.opts.Publish, l.opts.ClearCache, phaseFactory); err != nil { 127 return err 128 } 129 130 l.logger.Info(style.Step("RESTORING")) 131 if l.opts.ClearCache { 132 l.logger.Info("Skipping 'restore' due to clearing cache") 133 } else if err := l.Restore(ctx, buildCache.Name(), l.opts.Network, phaseFactory); err != nil { 134 return err 135 } 136 137 l.logger.Info(style.Step("BUILDING")) 138 139 if err := l.Build(ctx, l.opts.Network, l.opts.Volumes, phaseFactory); err != nil { 140 return err 141 } 142 143 l.logger.Info(style.Step("EXPORTING")) 144 return l.Export(ctx, l.opts.Image.String(), l.opts.RunImage, l.opts.Publish, launchCache.Name(), buildCache.Name(), l.opts.Network, phaseFactory) 145 } 146 147 return l.Create( 148 ctx, 149 l.opts.Publish, 150 l.opts.ClearCache, 151 l.opts.RunImage, 152 launchCache.Name(), 153 buildCache.Name(), 154 l.opts.Image.String(), 155 l.opts.Network, 156 l.opts.Volumes, 157 phaseFactory, 158 ) 159 } 160 161 func (l *LifecycleExecution) Cleanup() error { 162 var reterr error 163 if err := l.docker.VolumeRemove(context.Background(), l.layersVolume, true); err != nil { 164 reterr = errors.Wrapf(err, "failed to clean up layers volume %s", l.layersVolume) 165 } 166 if err := l.docker.VolumeRemove(context.Background(), l.appVolume, true); err != nil { 167 reterr = errors.Wrapf(err, "failed to clean up app volume %s", l.appVolume) 168 } 169 return reterr 170 } 171 172 func (l *LifecycleExecution) Create( 173 ctx context.Context, 174 publish, clearCache bool, 175 runImage, launchCacheName, cacheName, repoName, networkMode string, 176 volumes []string, 177 phaseFactory PhaseFactory, 178 ) error { 179 flags := []string{ 180 "-cache-dir", l.mountPaths.cacheDir(), 181 "-run-image", runImage, 182 } 183 184 if clearCache { 185 flags = append(flags, "-skip-restore") 186 } 187 188 processType := determineDefaultProcessType(l.platformAPI, l.opts.DefaultProcessType) 189 if processType != "" { 190 flags = append(flags, "-process-type", processType) 191 } 192 193 opts := []PhaseConfigProviderOperation{ 194 WithFlags(l.withLogLevel(flags...)...), 195 WithArgs(repoName), 196 WithNetwork(networkMode), 197 WithBinds(append(volumes, fmt.Sprintf("%s:%s", cacheName, l.mountPaths.cacheDir()))...), 198 WithContainerOperations(CopyDir(l.opts.AppPath, l.mountPaths.appDir(), l.opts.Builder.UID(), l.opts.Builder.GID(), l.os, l.opts.FileFilter)), 199 } 200 201 if publish { 202 authConfig, err := auth.BuildEnvVar(authn.DefaultKeychain, repoName) 203 if err != nil { 204 return err 205 } 206 207 opts = append(opts, WithRoot(), WithRegistryAccess(authConfig)) 208 } else { 209 opts = append(opts, 210 WithDaemonAccess(), 211 WithFlags("-daemon", "-launch-cache", l.mountPaths.launchCacheDir()), 212 WithBinds(fmt.Sprintf("%s:%s", launchCacheName, l.mountPaths.launchCacheDir())), 213 ) 214 } 215 216 create := phaseFactory.New(NewPhaseConfigProvider("creator", l, opts...)) 217 defer create.Cleanup() 218 return create.Run(ctx) 219 } 220 221 func (l *LifecycleExecution) Detect(ctx context.Context, networkMode string, volumes []string, phaseFactory PhaseFactory) error { 222 configProvider := NewPhaseConfigProvider( 223 "detector", 224 l, 225 WithLogPrefix("detector"), 226 WithArgs( 227 l.withLogLevel( 228 "-app", l.mountPaths.appDir(), 229 "-platform", l.mountPaths.platformDir(), 230 )..., 231 ), 232 WithNetwork(networkMode), 233 WithBinds(volumes...), 234 WithContainerOperations( 235 EnsureVolumeAccess(l.opts.Builder.UID(), l.opts.Builder.GID(), l.os, l.layersVolume, l.appVolume), 236 CopyDir(l.opts.AppPath, l.mountPaths.appDir(), l.opts.Builder.UID(), l.opts.Builder.GID(), l.os, l.opts.FileFilter), 237 ), 238 ) 239 240 detect := phaseFactory.New(configProvider) 241 defer detect.Cleanup() 242 return detect.Run(ctx) 243 } 244 245 func (l *LifecycleExecution) Restore(ctx context.Context, cacheName, networkMode string, phaseFactory PhaseFactory) error { 246 configProvider := NewPhaseConfigProvider( 247 "restorer", 248 l, 249 WithLogPrefix("restorer"), 250 WithImage(l.opts.LifecycleImage), 251 WithEnv(fmt.Sprintf("%s=%d", builder.EnvUID, l.opts.Builder.UID()), fmt.Sprintf("%s=%d", builder.EnvGID, l.opts.Builder.GID())), 252 WithRoot(), // remove after platform API 0.2 is no longer supported 253 WithArgs( 254 l.withLogLevel( 255 "-cache-dir", l.mountPaths.cacheDir(), 256 "-layers", l.mountPaths.layersDir(), 257 )..., 258 ), 259 WithNetwork(networkMode), 260 WithBinds(fmt.Sprintf("%s:%s", cacheName, l.mountPaths.cacheDir())), 261 ) 262 263 restore := phaseFactory.New(configProvider) 264 defer restore.Cleanup() 265 return restore.Run(ctx) 266 } 267 268 func (l *LifecycleExecution) Analyze(ctx context.Context, repoName, cacheName, networkMode string, publish, clearCache bool, phaseFactory PhaseFactory) error { 269 analyze, err := l.newAnalyze(repoName, cacheName, networkMode, publish, clearCache, phaseFactory) 270 if err != nil { 271 return err 272 } 273 defer analyze.Cleanup() 274 return analyze.Run(ctx) 275 } 276 277 func (l *LifecycleExecution) newAnalyze(repoName, cacheName, networkMode string, publish, clearCache bool, phaseFactory PhaseFactory) (RunnerCleaner, error) { 278 args := []string{ 279 "-layers", l.mountPaths.layersDir(), 280 repoName, 281 } 282 if clearCache { 283 args = prependArg("-skip-layers", args) 284 } else { 285 args = append([]string{"-cache-dir", l.mountPaths.cacheDir()}, args...) 286 } 287 288 if publish { 289 authConfig, err := auth.BuildEnvVar(authn.DefaultKeychain, repoName) 290 if err != nil { 291 return nil, err 292 } 293 294 configProvider := NewPhaseConfigProvider( 295 "analyzer", 296 l, 297 WithLogPrefix("analyzer"), 298 WithImage(l.opts.LifecycleImage), 299 WithEnv(fmt.Sprintf("%s=%d", builder.EnvUID, l.opts.Builder.UID()), fmt.Sprintf("%s=%d", builder.EnvGID, l.opts.Builder.GID())), 300 WithRegistryAccess(authConfig), 301 WithRoot(), 302 WithArgs(l.withLogLevel(args...)...), 303 WithNetwork(networkMode), 304 WithBinds(fmt.Sprintf("%s:%s", cacheName, l.mountPaths.cacheDir())), 305 ) 306 307 return phaseFactory.New(configProvider), nil 308 } 309 310 // TODO: when platform API 0.2 is no longer supported we can delete this code: https://github.com/YousefHaggyHeroku/pack/issues/629. 311 configProvider := NewPhaseConfigProvider( 312 "analyzer", 313 l, 314 WithLogPrefix("analyzer"), 315 WithImage(l.opts.LifecycleImage), 316 WithEnv( 317 fmt.Sprintf("%s=%d", builder.EnvUID, l.opts.Builder.UID()), 318 fmt.Sprintf("%s=%d", builder.EnvGID, l.opts.Builder.GID()), 319 ), 320 WithDaemonAccess(), 321 WithArgs( 322 l.withLogLevel( 323 prependArg( 324 "-daemon", 325 args, 326 )..., 327 )..., 328 ), 329 WithNetwork(networkMode), 330 WithBinds(fmt.Sprintf("%s:%s", cacheName, l.mountPaths.cacheDir())), 331 ) 332 333 return phaseFactory.New(configProvider), nil 334 } 335 336 func (l *LifecycleExecution) Build(ctx context.Context, networkMode string, volumes []string, phaseFactory PhaseFactory) error { 337 args := []string{ 338 "-layers", l.mountPaths.layersDir(), 339 "-app", l.mountPaths.appDir(), 340 "-platform", l.mountPaths.platformDir(), 341 } 342 343 configProvider := NewPhaseConfigProvider( 344 "builder", 345 l, 346 WithLogPrefix("builder"), 347 WithArgs(l.withLogLevel(args...)...), 348 WithNetwork(networkMode), 349 WithBinds(volumes...), 350 ) 351 352 build := phaseFactory.New(configProvider) 353 defer build.Cleanup() 354 return build.Run(ctx) 355 } 356 357 func determineDefaultProcessType(platformAPI *api.Version, providedValue string) string { 358 shouldSetForceDefault := platformAPI.Compare(api.MustParse("0.4")) >= 0 359 if providedValue == "" && shouldSetForceDefault { 360 return defaultProcessType 361 } 362 363 return providedValue 364 } 365 366 func (l *LifecycleExecution) newExport(repoName, runImage string, publish bool, launchCacheName, cacheName, networkMode string, phaseFactory PhaseFactory) (RunnerCleaner, error) { 367 flags := []string{ 368 "-cache-dir", l.mountPaths.cacheDir(), 369 "-layers", l.mountPaths.layersDir(), 370 "-stack", l.mountPaths.stackPath(), 371 "-app", l.mountPaths.appDir(), 372 "-run-image", runImage, 373 } 374 375 processType := determineDefaultProcessType(l.platformAPI, l.opts.DefaultProcessType) 376 if processType != "" { 377 flags = append(flags, "-process-type", processType) 378 } 379 380 opts := []PhaseConfigProviderOperation{ 381 WithLogPrefix("exporter"), 382 WithImage(l.opts.LifecycleImage), 383 WithEnv( 384 fmt.Sprintf("%s=%d", builder.EnvUID, l.opts.Builder.UID()), 385 fmt.Sprintf("%s=%d", builder.EnvGID, l.opts.Builder.GID()), 386 ), 387 WithFlags( 388 l.withLogLevel(flags...)..., 389 ), 390 WithArgs(repoName), 391 WithRoot(), 392 WithNetwork(networkMode), 393 WithBinds(fmt.Sprintf("%s:%s", cacheName, l.mountPaths.cacheDir())), 394 WithContainerOperations(WriteStackToml(l.mountPaths.stackPath(), l.opts.Builder.Stack(), l.os)), 395 } 396 397 if publish { 398 authConfig, err := auth.BuildEnvVar(authn.DefaultKeychain, repoName, runImage) 399 if err != nil { 400 return nil, err 401 } 402 403 opts = append( 404 opts, 405 WithRegistryAccess(authConfig), 406 WithRoot(), 407 ) 408 } else { 409 opts = append( 410 opts, 411 WithDaemonAccess(), 412 WithFlags("-daemon", "-launch-cache", l.mountPaths.launchCacheDir()), 413 WithBinds(fmt.Sprintf("%s:%s", launchCacheName, l.mountPaths.launchCacheDir())), 414 ) 415 } 416 417 return phaseFactory.New(NewPhaseConfigProvider("exporter", l, opts...)), nil 418 } 419 420 func (l *LifecycleExecution) Export(ctx context.Context, repoName string, runImage string, publish bool, launchCacheName, cacheName, networkMode string, phaseFactory PhaseFactory) error { 421 export, err := l.newExport(repoName, runImage, publish, launchCacheName, cacheName, networkMode, phaseFactory) 422 if err != nil { 423 return err 424 } 425 defer export.Cleanup() 426 return export.Run(ctx) 427 } 428 429 func (l *LifecycleExecution) withLogLevel(args ...string) []string { 430 if l.logger.IsVerbose() { 431 return append([]string{"-log-level", "debug"}, args...) 432 } 433 return args 434 } 435 436 func prependArg(arg string, args []string) []string { 437 return append([]string{arg}, args...) 438 }