get.porter.sh/porter@v1.3.0/pkg/porter/dependencies.go (about)

     1  package porter
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"strings"
     8  
     9  	"get.porter.sh/porter/pkg/cnab"
    10  	cnabprovider "get.porter.sh/porter/pkg/cnab/provider"
    11  	"get.porter.sh/porter/pkg/config"
    12  	"get.porter.sh/porter/pkg/manifest"
    13  	"get.porter.sh/porter/pkg/runtime"
    14  	"get.porter.sh/porter/pkg/storage"
    15  	"get.porter.sh/porter/pkg/tracing"
    16  	"github.com/hashicorp/go-multierror"
    17  )
    18  
    19  type dependencyExecutioner struct {
    20  	*config.Config
    21  	porter *Porter
    22  
    23  	Resolver      BundleResolver
    24  	CNAB          cnabprovider.CNABProvider
    25  	Installations storage.InstallationProvider
    26  
    27  	parentInstallation storage.Installation
    28  	parentAction       BundleAction
    29  	parentOpts         *BundleExecutionOptions
    30  
    31  	// These are populated by Prepare, call it or perish in inevitable errors
    32  	parentArgs cnabprovider.ActionArguments
    33  	deps       []*queuedDependency
    34  
    35  	// this should maybe go somewhere else
    36  	depArgs cnabprovider.ActionArguments
    37  }
    38  
    39  func newDependencyExecutioner(p *Porter, installation storage.Installation, action BundleAction) *dependencyExecutioner {
    40  	resolver := BundleResolver{
    41  		Cache:    p.Cache,
    42  		Registry: p.Registry,
    43  	}
    44  	return &dependencyExecutioner{
    45  		porter:             p,
    46  		parentInstallation: installation,
    47  		parentAction:       action,
    48  		parentOpts:         action.GetOptions(),
    49  		Config:             p.Config,
    50  		Resolver:           resolver,
    51  		CNAB:               p.CNAB,
    52  		Installations:      p.Installations,
    53  	}
    54  }
    55  
    56  type queuedDependency struct {
    57  	cnab.DependencyLock
    58  	BundleReference cnab.BundleReference
    59  	Parameters      map[string]string
    60  
    61  	// cache of the CNAB file contents
    62  	cnabFileContents []byte
    63  }
    64  
    65  func (e *dependencyExecutioner) Prepare(ctx context.Context) error {
    66  	ctx, span := tracing.StartSpan(ctx)
    67  	defer span.EndSpan()
    68  
    69  	parentActionArgs, err := e.porter.BuildActionArgs(ctx, e.parentInstallation, e.parentAction)
    70  	if err != nil {
    71  		return err
    72  	}
    73  	e.parentArgs = parentActionArgs
    74  
    75  	err = e.identifyDependencies(ctx)
    76  	if err != nil {
    77  		return err
    78  	}
    79  
    80  	for _, dep := range e.deps {
    81  		err := e.prepareDependency(ctx, dep)
    82  		if err != nil {
    83  			return err
    84  		}
    85  	}
    86  
    87  	return nil
    88  }
    89  
    90  func (e *dependencyExecutioner) Execute(ctx context.Context) error {
    91  	ctx, span := tracing.StartSpan(ctx)
    92  	defer span.EndSpan()
    93  
    94  	if e.deps == nil {
    95  		return span.Error(errors.New("Prepare must be called before Execute"))
    96  	}
    97  
    98  	// executeDependency the requested action against all the dependencies
    99  	for _, dep := range e.deps {
   100  		if !e.sharedActionResolver(ctx, dep) {
   101  			return nil
   102  		}
   103  		err := e.executeDependency(ctx, dep)
   104  		if err != nil {
   105  			return err
   106  		}
   107  	}
   108  
   109  	return nil
   110  }
   111  
   112  // PrepareRootActionArguments uses information about the dependencies of a bundle to prepare
   113  // the execution of the root operation.
   114  func (e *dependencyExecutioner) PrepareRootActionArguments(ctx context.Context) (cnabprovider.ActionArguments, error) {
   115  	args, err := e.porter.BuildActionArgs(ctx, e.parentInstallation, e.parentAction)
   116  	if err != nil {
   117  		return cnabprovider.ActionArguments{}, err
   118  	}
   119  
   120  	if args.Files == nil {
   121  		args.Files = make(map[string]string, 2*len(e.deps))
   122  	}
   123  
   124  	// Define files necessary for dependencies that need to be copied into the bundle
   125  	// args.Files is a map of target path to file contents
   126  	// This creates what goes in /cnab/app/dependencies/DEP.NAME
   127  	for _, dep := range e.deps {
   128  		// Copy the dependency bundle.json
   129  		err = e.checkSharedOutputs(ctx, dep)
   130  		if err != nil {
   131  			return cnabprovider.ActionArguments{}, err
   132  		}
   133  		target := runtime.GetDependencyDefinitionPath(dep.DependencyLock.Alias)
   134  		args.Files[target] = string(dep.cnabFileContents)
   135  	}
   136  	return args, nil
   137  }
   138  
   139  func (e *dependencyExecutioner) checkSharedOutputs(ctx context.Context, dep *queuedDependency) error {
   140  	if !e.sharedActionResolver(ctx, dep) && e.parentAction.GetAction() == "install" {
   141  		return e.getActionArgs(ctx, dep)
   142  	}
   143  	return nil
   144  }
   145  
   146  // sharedActionResolver tries to localize if v2, and shared deps
   147  // then what actions should we take based off labels/action type/state
   148  // true means continue, false means stop
   149  func (e *dependencyExecutioner) sharedActionResolver(ctx context.Context, dep *queuedDependency) bool {
   150  	depInstallation, err := e.Installations.GetInstallation(ctx, e.parentOpts.Namespace, dep.Alias)
   151  	if err != nil {
   152  		if errors.Is(err, storage.ErrNotFound{}) {
   153  			return true
   154  		}
   155  	}
   156  	e.depArgs.Installation = depInstallation
   157  
   158  	//We're real, let's check if this is in the installation the parent
   159  	// is referencing
   160  	if dep.SharingGroup == depInstallation.Labels["sh.porter.SharingGroup"] {
   161  		if e.parentAction.GetAction() == "install" {
   162  			return false
   163  		}
   164  		if e.parentAction.GetAction() == "upgrade" {
   165  			return true
   166  		}
   167  		if e.parentAction.GetAction() == "uninstall" {
   168  			return false
   169  		}
   170  	}
   171  	return true
   172  }
   173  
   174  func (e *dependencyExecutioner) identifyDependencies(ctx context.Context) error {
   175  	ctx, span := tracing.StartSpan(ctx)
   176  	defer span.EndSpan()
   177  
   178  	// Load parent CNAB bundle definition
   179  	var bun cnab.ExtendedBundle
   180  	if e.parentOpts.CNABFile != "" {
   181  		bundle, err := e.CNAB.LoadBundle(e.parentOpts.CNABFile)
   182  		if err != nil {
   183  			return span.Error(err)
   184  		}
   185  		bun = bundle
   186  	} else if e.parentOpts.Reference != "" {
   187  		cachedBundle, err := e.Resolver.Resolve(ctx, e.parentOpts.BundlePullOptions)
   188  		if err != nil {
   189  			return span.Error(fmt.Errorf("could not resolve bundle: %w", err))
   190  		}
   191  
   192  		bun = cachedBundle.Definition
   193  
   194  	} else if e.parentOpts.Name != "" {
   195  		c, err := e.Installations.GetLastRun(ctx, e.parentOpts.Namespace, e.parentOpts.Name)
   196  		if err != nil {
   197  			return err
   198  		}
   199  
   200  		bun = cnab.NewBundle(c.Bundle)
   201  	} else {
   202  		// If we hit here, there is a bug somewhere
   203  		return span.Error(errors.New("identifyDependencies failed to load the bundle because no bundle was specified. Please report this bug to https://github.com/getporter/porter/issues/new/choose"))
   204  	}
   205  	locks, err := bun.ResolveDependencies(bun)
   206  	if err != nil {
   207  		return span.Error(err)
   208  	}
   209  
   210  	e.deps = make([]*queuedDependency, len(locks))
   211  	for i, lock := range locks {
   212  		span.Debugf("Resolved dependency %s to %s", lock.Alias, lock.Reference)
   213  		e.deps[i] = &queuedDependency{
   214  			DependencyLock: lock,
   215  		}
   216  	}
   217  
   218  	return nil
   219  }
   220  
   221  func (e *dependencyExecutioner) prepareDependency(ctx context.Context, dep *queuedDependency) error {
   222  	ctx, span := tracing.StartSpan(ctx)
   223  	defer span.EndSpan()
   224  	// Pull the dependency
   225  	var err error
   226  	pullOpts := BundlePullOptions{
   227  		Reference:        dep.Reference,
   228  		InsecureRegistry: e.parentOpts.InsecureRegistry,
   229  		Force:            e.parentOpts.Force,
   230  	}
   231  	if err := pullOpts.Validate(); err != nil {
   232  		return span.Error(fmt.Errorf("error preparing dependency %s: %w", dep.Alias, err))
   233  	}
   234  	cachedDep, err := e.Resolver.Resolve(ctx, pullOpts)
   235  	if err != nil {
   236  		return span.Error(fmt.Errorf("error pulling dependency %s: %w", dep.Alias, err))
   237  	}
   238  	dep.BundleReference = cachedDep.BundleReference
   239  
   240  	strategy := e.GetSchemaCheckStrategy(ctx)
   241  	err = cachedDep.Definition.Validate(e.Context, strategy)
   242  	if err != nil {
   243  		return span.Error(fmt.Errorf("invalid bundle %s: %w", dep.Alias, err))
   244  	}
   245  
   246  	// Cache the bundle.json for later
   247  	dep.cnabFileContents, err = e.FileSystem.ReadFile(cachedDep.BundlePath)
   248  	if err != nil {
   249  		return span.Error(fmt.Errorf("error reading %s: %w", cachedDep.BundlePath, err))
   250  	}
   251  
   252  	// Make a lookup of which parameters are defined in the dependent bundle
   253  	depParams := map[string]struct{}{}
   254  	for paramName := range cachedDep.Definition.Parameters {
   255  		depParams[paramName] = struct{}{}
   256  	}
   257  
   258  	// Handle any parameter overrides for the dependency defined in porter.yaml
   259  	// dependencies:
   260  	//  requires:
   261  	//   - name: DEP
   262  	//     parameters:
   263  	//       PARAM: VALUE
   264  	// TODO: When we redo dependencies, we need to remove this dependency on the bundle being a porter bundle with a manifest
   265  	// Yes, right now the way this works means this feature is Porter only
   266  	m := &manifest.Manifest{}
   267  	if e.parentOpts.File != "" {
   268  		var err error
   269  		m, err = manifest.LoadManifestFrom(ctx, e.Config, e.parentOpts.File)
   270  		if err != nil {
   271  			return err
   272  		}
   273  	}
   274  
   275  	for _, manifestDep := range m.Dependencies.Requires {
   276  		if manifestDep.Name == dep.Alias {
   277  			for paramName, value := range manifestDep.Parameters {
   278  				// Make sure the parameter is defined in the bundle
   279  				if _, ok := depParams[paramName]; !ok {
   280  					return fmt.Errorf("invalid dependencies.%s.parameters entry, %s is not a parameter defined in that bundle", dep.Alias, paramName)
   281  				}
   282  
   283  				if dep.Parameters == nil {
   284  					dep.Parameters = make(map[string]string, 1)
   285  				}
   286  				dep.Parameters[paramName] = value
   287  			}
   288  		}
   289  	}
   290  
   291  	// Handle any parameter overrides for the dependency defined on the command line
   292  	// --param DEP#PARAM=VALUE
   293  	for key, value := range e.parentOpts.depParams {
   294  		parts := strings.Split(key, "#")
   295  		if len(parts) > 1 && parts[0] == dep.Alias {
   296  			paramName := parts[1]
   297  
   298  			// Make sure the parameter is defined in the bundle
   299  			if _, ok := depParams[paramName]; !ok {
   300  				return fmt.Errorf("invalid --param %s, %s is not a parameter defined in the bundle %s", key, paramName, dep.Alias)
   301  			}
   302  
   303  			if dep.Parameters == nil {
   304  				dep.Parameters = make(map[string]string, 1)
   305  			}
   306  			dep.Parameters[paramName] = value
   307  		}
   308  	}
   309  
   310  	return nil
   311  }
   312  
   313  func (e *dependencyExecutioner) executeDependency(ctx context.Context, dep *queuedDependency) error {
   314  	// TODO(carolynvs): We should really switch up how the deperator works so that
   315  	// even the root bundle uses the execution engine here. This would set up how
   316  	// we want dependencies and mixins as bundles to work in the future.
   317  
   318  	ctx, span := tracing.StartSpan(ctx)
   319  	defer span.EndSpan()
   320  
   321  	if dep.SharingMode {
   322  		err := e.runDependencyv2(ctx, dep)
   323  		return err
   324  	}
   325  
   326  	eb := cnab.ExtendedBundle{}
   327  	//this expects depv1 style dependency to be installed as parentName+depName
   328  	depName := eb.BuildPrerequisiteInstallationName(e.parentOpts.Name, dep.Alias)
   329  	depInstallation, err := e.Installations.GetInstallation(ctx, e.parentOpts.Namespace, depName)
   330  
   331  	if err != nil {
   332  		if errors.Is(err, storage.ErrNotFound{}) {
   333  			depInstallation = storage.NewInstallation(e.parentOpts.Namespace, depName)
   334  			depInstallation.SetLabel("sh.porter.parentInstallation", e.parentArgs.Installation.String())
   335  
   336  			// For now, assume it's okay to give the dependency the same credentials as the parent
   337  			depInstallation.CredentialSets = e.parentInstallation.CredentialSets
   338  			if err = e.Installations.InsertInstallation(ctx, depInstallation); err != nil {
   339  				return err
   340  			}
   341  		} else {
   342  			return err
   343  		}
   344  	}
   345  
   346  	e.depArgs.Installation = depInstallation
   347  
   348  	if err = e.getActionArgs(ctx, dep); err != nil {
   349  		return err
   350  	}
   351  
   352  	if err = e.finalizeExecute(ctx, dep); err != nil {
   353  		return err
   354  	}
   355  
   356  	return nil
   357  }
   358  
   359  // runDependencyv2 will see if the child dependency is already installed
   360  // and if so, use sharingmode && group to resolve what to do
   361  func (e *dependencyExecutioner) runDependencyv2(ctx context.Context, dep *queuedDependency) error {
   362  	depInstallation, err := e.Installations.GetInstallation(ctx, e.parentOpts.Namespace, dep.Alias)
   363  	if err != nil {
   364  		if errors.Is(err, storage.ErrNotFound{}) {
   365  			depInstallation = storage.NewInstallation(e.parentOpts.Namespace, dep.Alias)
   366  			depInstallation.SetLabel("sh.porter.parentInstallation", e.parentArgs.Installation.String())
   367  			depInstallation.SetLabel("sh.porter.SharingGroup", dep.SharingGroup)
   368  
   369  			// For now, assume it's okay to give the dependency the same credentials as the parent
   370  			depInstallation.CredentialSets = e.parentInstallation.CredentialSets
   371  			if err = e.Installations.InsertInstallation(ctx, depInstallation); err != nil {
   372  				return err
   373  			}
   374  
   375  			return err
   376  		}
   377  	}
   378  	//We save the installation
   379  	e.depArgs.Installation = depInstallation
   380  
   381  	// Installed: Return
   382  	// Uninstalled: Error (delete or else)
   383  	// Upgrade: Unsupported
   384  	// Invoke: At your own risk
   385  	//todo(schristoff): this is kind of icky, can be it less so?
   386  	if dep.SharingGroup == depInstallation.Labels["sh.porter.SharingGroup"] {
   387  		if depInstallation.IsInstalled() {
   388  
   389  			action := e.parentAction.GetAction()
   390  			if action == "upgrade" || action == "uninstall" {
   391  				return nil
   392  			}
   393  		}
   394  		if depInstallation.Uninstalled {
   395  			return fmt.Errorf("error executing dependency, dependency must be in installed status or deleted, %s is in  status %s", dep.Alias, depInstallation.Status)
   396  		}
   397  
   398  	}
   399  
   400  	if err = e.getActionArgs(ctx, dep); err != nil {
   401  		return err
   402  	}
   403  
   404  	if err = e.finalizeExecute(ctx, dep); err != nil {
   405  		return err
   406  	}
   407  
   408  	return nil
   409  }
   410  
   411  func (e *dependencyExecutioner) getActionArgs(ctx context.Context,
   412  	dep *queuedDependency) error {
   413  	actionName := e.parentArgs.Run.Action
   414  	finalParams, err := e.porter.finalizeParameters(ctx, e.depArgs.Installation, dep.BundleReference.Definition, actionName, dep.Parameters)
   415  	if err != nil {
   416  		return fmt.Errorf("error resolving parameters for dependency %s: %w", dep.Alias, err)
   417  	}
   418  	depRun, err := e.porter.createRun(ctx, dep.BundleReference, e.depArgs.Installation, actionName, finalParams)
   419  	if err != nil {
   420  		return fmt.Errorf("error creating run for dependency %s: %w", dep.Alias, err)
   421  	}
   422  	e.depArgs = cnabprovider.ActionArguments{
   423  		BundleReference:       dep.BundleReference,
   424  		Installation:          e.depArgs.Installation,
   425  		Run:                   depRun,
   426  		Driver:                e.parentArgs.Driver,
   427  		AllowDockerHostAccess: e.parentOpts.AllowDockerHostAccess,
   428  		PersistLogs:           e.parentArgs.PersistLogs,
   429  	}
   430  	return nil
   431  }
   432  
   433  // finalizeExecute handles some Uninstall logic that is carried out
   434  // right before calling CNAB execute.
   435  func (e *dependencyExecutioner) finalizeExecute(ctx context.Context, dep *queuedDependency) error {
   436  	ctx, span := tracing.StartSpan(ctx)
   437  	// Determine if we're working with UninstallOptions, to inform deletion and
   438  	// error handling, etc.
   439  	var uninstallOpts UninstallOptions
   440  	if opts, ok := e.parentAction.(UninstallOptions); ok {
   441  		uninstallOpts = opts
   442  	}
   443  
   444  	var executeErrs error
   445  	span.Infof("Executing dependency %s...", dep.Alias)
   446  	err := e.CNAB.Execute(ctx, e.depArgs)
   447  	if err != nil {
   448  		executeErrs = multierror.Append(executeErrs, fmt.Errorf("error executing dependency %s: %w", dep.Alias, err))
   449  
   450  		// Handle errors when/if the action is uninstall
   451  		// If uninstallOpts is an empty struct, executeErrs will pass through
   452  		executeErrs = uninstallOpts.handleUninstallErrs(e.Err, executeErrs)
   453  		if executeErrs != nil {
   454  			return span.Error(executeErrs)
   455  		}
   456  	}
   457  
   458  	// If uninstallOpts is an empty struct (i.e., action not Uninstall), this
   459  	// will resolve to false and thus be a no-op
   460  	if uninstallOpts.shouldDelete() {
   461  		span.Infof(installationDeleteTmpl, e.depArgs.Installation)
   462  		return e.Installations.RemoveInstallation(ctx, e.depArgs.Installation.Namespace, e.depArgs.Installation.Name)
   463  	}
   464  	return nil
   465  }