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  }