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

     1  package porter
     2  
     3  import (
     4  	"context"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"runtime"
     9  	"strings"
    10  	"unicode"
    11  
    12  	"get.porter.sh/porter/pkg/cache"
    13  	"get.porter.sh/porter/pkg/cnab"
    14  	"get.porter.sh/porter/pkg/cnab/drivers"
    15  	cnabprovider "get.porter.sh/porter/pkg/cnab/provider"
    16  	"get.porter.sh/porter/pkg/encoding"
    17  	"get.porter.sh/porter/pkg/portercontext"
    18  	"get.porter.sh/porter/pkg/secrets"
    19  	"get.porter.sh/porter/pkg/storage"
    20  	"get.porter.sh/porter/pkg/tracing"
    21  	"github.com/opencontainers/go-digest"
    22  	"go.mongodb.org/mongo-driver/bson"
    23  )
    24  
    25  // BundleAction is an interface that defines a method for supplying
    26  // BundleLifecycleOptions.  This is useful when implementations contain
    27  // action-specific options beyond the stock BundleLifecycleOptions.
    28  type BundleAction interface {
    29  	// GetAction returns the type of action: install, upgrade, invoke, uninstall
    30  	GetAction() string
    31  
    32  	// GetActionVerb returns the appropriate verb (present participle, e.g. -ing)
    33  	// for the action.
    34  	GetActionVerb() string
    35  
    36  	// GetOptions returns the common bundle action options used to execute the bundle.
    37  	GetOptions() *BundleExecutionOptions
    38  
    39  	// Validate the action before it is executed.
    40  	Validate(ctx context.Context, args []string, p *Porter) error
    41  }
    42  
    43  // BundleExecutionOptions are common options for commands that run a bundle (install/upgrade/invoke/uninstall)
    44  type BundleExecutionOptions struct {
    45  	*BundleReferenceOptions
    46  
    47  	// AllowDockerHostAccess grants the bundle access to the Docker socket.
    48  	AllowDockerHostAccess bool
    49  
    50  	// MountHostVolume mounts provides the bundle access to a host volume.
    51  	// This is the unparsed list of HOST_PATH:TARGET_PATH:OPTION
    52  	// OPTION can be ro (read-only) or rw (read-write). Defaults to ro.
    53  	HostVolumeMounts []string
    54  
    55  	// DebugMode indicates if the bundle should be run in debug mode.
    56  	DebugMode bool
    57  
    58  	// NoLogs runs the bundle without persisting any logs.
    59  	NoLogs bool
    60  
    61  	// Params is the unparsed list of NAME=VALUE parameters set on the command line.
    62  	Params []string
    63  
    64  	// ParameterSets is a list of parameter sets containing parameter sources
    65  	ParameterSets []string
    66  
    67  	// CredentialIdentifiers is a list of credential names or paths to make available to the bundle.
    68  	CredentialIdentifiers []string
    69  
    70  	// Driver is the CNAB-compliant driver used to run bundle actions.
    71  	Driver string
    72  
    73  	// parameters that are intended for dependencies
    74  	// This is legacy support for v1 of dependencies where you could pass a parameter to a dependency directly using special formatting
    75  	// Example: --param mysql#username=admin
    76  	// This is not used anymore in dependencies v2
    77  	depParams map[string]string
    78  
    79  	// A cache of the final resolved set of parameters that are passed to the bundle
    80  	// Do not use directly, use GetParameters instead.
    81  	finalParams map[string]interface{}
    82  
    83  	VerifyBundleBeforeExecution bool
    84  }
    85  
    86  func NewBundleExecutionOptions() *BundleExecutionOptions {
    87  	return &BundleExecutionOptions{
    88  		BundleReferenceOptions: &BundleReferenceOptions{},
    89  	}
    90  }
    91  
    92  func (o *BundleExecutionOptions) GetOptions() *BundleExecutionOptions {
    93  	return o
    94  }
    95  
    96  // GetParameters returns the final resolved set of a parameters to pass to the bundle.
    97  // You must have already called Porter.applyActionOptionsToInstallation to populate this value as
    98  // this just returns the cached set of parameters
    99  func (o *BundleExecutionOptions) GetParameters() map[string]interface{} {
   100  	if o.finalParams == nil {
   101  		panic("BundleExecutionOptions.GetParameters was called before the final set of parameters were resolved with Porter.applyActionOptionsToInstallation")
   102  	}
   103  	return o.finalParams
   104  }
   105  
   106  // Sets the final resolved set of host volumes to be made available to the bundle
   107  func (o *BundleExecutionOptions) GetHostVolumeMounts() []cnabprovider.HostVolumeMountSpec {
   108  	var hostVolumeMounts []cnabprovider.HostVolumeMountSpec
   109  	for _, mount := range o.HostVolumeMounts {
   110  		var isReadOnlyMount bool
   111  		parts := strings.Split(mount, ":") // HOST_PATH:TARGET_PATH:OPTION
   112  
   113  		// if parts[0] is a single character, it's a drive letter on Windows
   114  		// so we need to join it with the next part
   115  		if runtime.GOOS == "windows" && len(parts) > 1 && len(parts[0]) == 1 && unicode.IsLetter(rune(parts[0][0])) {
   116  			parts[1] = fmt.Sprintf("%s:%s", parts[0], parts[1])
   117  			parts = parts[1:]
   118  		}
   119  
   120  		l := len(parts)
   121  		if l < 2 || l > 3 {
   122  			continue
   123  		}
   124  
   125  		switch {
   126  		case l == 2:
   127  			isReadOnlyMount = true
   128  			// next cases are l == 3
   129  		case parts[2] == "ro":
   130  			isReadOnlyMount = true
   131  		case parts[2] == "rw":
   132  			isReadOnlyMount = false
   133  		default:
   134  			isReadOnlyMount = true
   135  		}
   136  
   137  		hostVolumeMounts = append(hostVolumeMounts, cnabprovider.HostVolumeMountSpec{
   138  			Source:   parts[0],
   139  			Target:   parts[1],
   140  			ReadOnly: isReadOnlyMount,
   141  		})
   142  	}
   143  
   144  	return hostVolumeMounts
   145  }
   146  
   147  func (o *BundleExecutionOptions) Validate(ctx context.Context, args []string, p *Porter) error {
   148  	if err := o.BundleReferenceOptions.Validate(ctx, args, p); err != nil {
   149  		return err
   150  	}
   151  
   152  	o.defaultDriver(p)
   153  	if err := o.validateDriver(p.Context); err != nil {
   154  		return err
   155  	}
   156  
   157  	return nil
   158  }
   159  
   160  // defaultDriver supplies the default driver if none is specified
   161  func (o *BundleExecutionOptions) defaultDriver(p *Porter) {
   162  	//
   163  	// When you run porter installation apply, there are some settings from porter install
   164  	// that aren't exposed as flags (like driver and allow-docker-host-access).
   165  	// This allows the user to set them in the config file, and we will use them before running the bundle.
   166  	//
   167  
   168  	// Apply global config to the --driver flag
   169  	if o.Driver == "" {
   170  		// We have both porter build --driver, and porter install --driver
   171  		// So in the config file it's named build-driver and runtime-driver
   172  		// This is why we check first before applying the value. Only apply the config
   173  		// file setting if they didn't specify a flag.
   174  		o.Driver = p.Data.RuntimeDriver
   175  	}
   176  
   177  	// Apply global config to the --allow-docker-host-access flag
   178  	if !o.AllowDockerHostAccess {
   179  		// Only apply the config setting if they didn't specify the flag (i.e. it's porter installation apply which doesn't have that flag)
   180  		o.AllowDockerHostAccess = p.Config.Data.AllowDockerHostAccess
   181  	}
   182  }
   183  
   184  // validateDriver validates that the provided driver is supported by Porter
   185  func (o *BundleExecutionOptions) validateDriver(cxt *portercontext.Context) error {
   186  	_, err := drivers.LookupDriver(cxt, o.Driver)
   187  	return err
   188  }
   189  
   190  // BundleReferenceOptions are the set of options available for commands that accept a bundle reference
   191  type BundleReferenceOptions struct {
   192  	installationOptions
   193  	BundlePullOptions
   194  
   195  	// DO NOT ACCESS DIRECTLY, use GetBundleReference to retrieve and cache the value
   196  	bundleRef *cnab.BundleReference
   197  }
   198  
   199  // GetBundleReference resolves the bundle reference if needed and caches the result so that this is safe to call multiple times in a row.
   200  func (o *BundleReferenceOptions) GetBundleReference(ctx context.Context, p *Porter) (cnab.BundleReference, error) {
   201  	if o.bundleRef == nil {
   202  		ref, err := p.resolveBundleReference(ctx, o)
   203  		if err != nil {
   204  			return cnab.BundleReference{}, err
   205  		}
   206  
   207  		o.bundleRef = &ref
   208  	}
   209  
   210  	return *o.bundleRef, nil
   211  }
   212  
   213  // UnsetBundleReference clears the cached bundle reference so that it may be re-resolved the next time GetBundleReference is called.
   214  func (o *BundleReferenceOptions) UnsetBundleReference() {
   215  	o.bundleRef = nil
   216  }
   217  
   218  func (o *BundleReferenceOptions) Validate(ctx context.Context, args []string, porter *Porter) error {
   219  	var err error
   220  
   221  	if o.Reference != "" {
   222  		// Ignore anything set based on the bundle directory we are in, go off of the tag
   223  		o.File = ""
   224  		o.CNABFile = ""
   225  		o.ReferenceSet = true
   226  
   227  		if err := o.BundlePullOptions.Validate(); err != nil {
   228  			return err
   229  		}
   230  	}
   231  
   232  	err = o.installationOptions.Validate(ctx, args, porter)
   233  	if err != nil {
   234  		return err
   235  	}
   236  
   237  	if o.Name == "" && o.File == "" && o.CNABFile == "" && o.Reference == "" {
   238  		return errors.New("no bundle specified. Either an installation name, --reference, --file or --cnab-file must be specified or the current directory must contain a porter.yaml file")
   239  	}
   240  
   241  	return nil
   242  }
   243  
   244  // resolveBundleReference uses the bundle options from the CLI flags to determine which bundle is being referenced.
   245  // Takes into account the --reference, --file and --cnab-file flags, and also uses the NAME argument and looks up the bundle definition from the installation.
   246  // Do not call this directly. Call BundleReferenceOptions.GetBundleReference() instead so that it's safe to call multiple times in a row and returns a cached results after being resolved.
   247  func (p *Porter) resolveBundleReference(ctx context.Context, opts *BundleReferenceOptions) (cnab.BundleReference, error) {
   248  	var bundleRef cnab.BundleReference
   249  
   250  	useReference := func(ref cnab.OCIReference) error {
   251  		pullOpts := *opts // make a copy just to do the pull
   252  		pullOpts.Reference = ref.String()
   253  
   254  		err := ensureVPrefix(&pullOpts, p.Err)
   255  		if err != nil {
   256  			return err
   257  		}
   258  
   259  		cachedBundle, err := p.prepullBundleByReference(ctx, &pullOpts)
   260  		if err != nil {
   261  			return err
   262  		}
   263  
   264  		bundleRef = cachedBundle.BundleReference
   265  		return nil
   266  	}
   267  
   268  	// load the referenced bundle
   269  	if opts.Reference != "" {
   270  		if err := useReference(opts.GetReference()); err != nil {
   271  			return cnab.BundleReference{}, err
   272  		}
   273  	} else if opts.File != "" { // load the local bundle source
   274  		buildOpts := BuildOptions{
   275  			BundleDefinitionOptions: opts.BundleDefinitionOptions,
   276  			InsecureRegistry:        opts.InsecureRegistry,
   277  		}
   278  		localBundle, err := p.ensureLocalBundleIsUpToDate(ctx, buildOpts)
   279  		if err != nil {
   280  			return cnab.BundleReference{}, err
   281  		}
   282  		bundleRef = localBundle
   283  	} else if opts.CNABFile != "" { // load the cnab bundle definition
   284  		bun, err := p.CNAB.LoadBundle(opts.CNABFile)
   285  		if err != nil {
   286  			return cnab.BundleReference{}, err
   287  		}
   288  		bundleRef = cnab.BundleReference{Definition: bun}
   289  	} else if opts.Name != "" { // Return the bundle associated with the installation
   290  		i, err := p.Installations.GetInstallation(ctx, opts.Namespace, opts.Name)
   291  		if err != nil {
   292  			return cnab.BundleReference{}, fmt.Errorf("installation %s/%s not found: %w", opts.Namespace, opts.Name, err)
   293  		}
   294  		if i.Status.BundleReference != "" {
   295  			ref, err := cnab.ParseOCIReference(i.Status.BundleReference)
   296  			if err != nil {
   297  				return cnab.BundleReference{}, fmt.Errorf("installation.Status.BundleReference is invalid: %w", err)
   298  			}
   299  			if err := useReference(ref); err != nil {
   300  				return cnab.BundleReference{}, err
   301  			}
   302  		} else { // The bundle was installed from source
   303  			lastRun, err := p.Installations.GetLastRun(ctx, opts.Namespace, opts.Name)
   304  			if err != nil {
   305  				return cnab.BundleReference{}, fmt.Errorf("could not load the bundle definition from the installation's last run: %w", err)
   306  			}
   307  
   308  			bundleRef = cnab.BundleReference{
   309  				Definition: cnab.NewBundle(lastRun.Bundle),
   310  				Digest:     digest.Digest(lastRun.BundleDigest)}
   311  
   312  			if lastRun.BundleReference != "" {
   313  				bundleRef.Reference, err = cnab.ParseOCIReference(lastRun.BundleReference)
   314  				if err != nil {
   315  					return cnab.BundleReference{}, fmt.Errorf("invalid bundle reference, %s, found on the last bundle run record %s: %w", lastRun.BundleReference, lastRun.ID, err)
   316  				}
   317  			}
   318  		}
   319  	} else { // Nothing was referenced
   320  		return cnab.BundleReference{}, errors.New("no bundle specified")
   321  	}
   322  
   323  	if opts.Name == "" {
   324  		opts.Name = bundleRef.Definition.Name
   325  	}
   326  
   327  	return bundleRef, nil
   328  }
   329  
   330  // BuildActionArgs converts an instance of user-provided action options into prepared arguments
   331  // that can be used to execute the action.
   332  func (p *Porter) BuildActionArgs(ctx context.Context, installation storage.Installation, action BundleAction) (cnabprovider.ActionArguments, error) {
   333  	log := tracing.LoggerFromContext(ctx)
   334  
   335  	opts := action.GetOptions()
   336  	bundleRef, err := opts.GetBundleReference(ctx, p)
   337  	if err != nil {
   338  		return cnabprovider.ActionArguments{}, err
   339  	}
   340  
   341  	if opts.RelocationMapping != "" {
   342  		err := encoding.UnmarshalFile(p.FileSystem, opts.RelocationMapping, &bundleRef.RelocationMap)
   343  		if err != nil {
   344  			return cnabprovider.ActionArguments{}, log.Error(fmt.Errorf("could not parse the relocation mapping file at %s: %w", opts.RelocationMapping, err))
   345  		}
   346  	}
   347  
   348  	run, err := p.createRun(ctx, bundleRef, installation, action.GetAction(), opts.GetParameters())
   349  	if err != nil {
   350  		return cnabprovider.ActionArguments{}, err
   351  	}
   352  
   353  	args := cnabprovider.ActionArguments{
   354  		Run:                   run,
   355  		Installation:          installation,
   356  		BundleReference:       bundleRef,
   357  		Driver:                opts.Driver,
   358  		AllowDockerHostAccess: opts.AllowDockerHostAccess,
   359  		HostVolumeMounts:      opts.GetHostVolumeMounts(),
   360  		PersistLogs:           !opts.NoLogs,
   361  	}
   362  	return args, nil
   363  }
   364  
   365  // ensureVPrefix adds a "v" prefix to the version tag if it's not already there.
   366  // Semver version tags tag should always be prefixed with a "v", see https://github.com/getporter/porter/issues/2886.
   367  // This is safe because "porter publish" adds a "v", see
   368  // https://github.com/getporter/porter/blob/17bd7816ef6bde856793f6122e32274aa9d01d1b/pkg/storage/installation.go#L350
   369  func ensureVPrefix(opts *BundleReferenceOptions, out io.Writer) error {
   370  	var ociRef *cnab.OCIReference
   371  	if opts._ref != nil {
   372  		ociRef = opts._ref
   373  	} else {
   374  		ref, err := cnab.ParseOCIReference(opts.Reference)
   375  		if err != nil {
   376  			return fmt.Errorf("unable to parse OCI reference from '%s': %w", opts.Reference, err)
   377  		}
   378  		ociRef = &ref
   379  	}
   380  
   381  	// Do nothing for empty tags, tags that do not start with a number and non-semver tags
   382  	if !tagStartsWithNumber(ociRef) || !ociRef.HasVersion() {
   383  		return nil
   384  	}
   385  
   386  	vRef, err := ociRef.WithTag("v" + ociRef.Tag())
   387  	if err != nil {
   388  		return fmt.Errorf("unable to prefix reference tag '%s' with 'v': %w", ociRef.Tag(), err)
   389  	}
   390  
   391  	// always update the .Reference string, but don't add the _ref field unless it was already there (non-nil)
   392  	fmt.Fprintf(out, "WARNING: using reference %q instead of %q because missing v-prefix on tag\n", vRef.String(), ociRef.String())
   393  	opts.Reference = vRef.String()
   394  	if opts._ref != nil {
   395  		opts._ref = &vRef
   396  	}
   397  	return nil
   398  }
   399  
   400  func tagStartsWithNumber(ociRef *cnab.OCIReference) bool {
   401  	return ociRef.HasTag() && ociRef.Tag()[0] >= '0' && ociRef.Tag()[0] <= '9'
   402  }
   403  
   404  // prepullBundleByReference handles calling the bundle pull operation and updating
   405  // the shared options like name and bundle file path. This is used by install, upgrade
   406  // and uninstall
   407  func (p *Porter) prepullBundleByReference(ctx context.Context, opts *BundleReferenceOptions) (cache.CachedBundle, error) {
   408  	if opts.Reference == "" {
   409  		return cache.CachedBundle{}, nil
   410  	}
   411  
   412  	cachedBundle, err := p.PullBundle(ctx, opts.BundlePullOptions)
   413  	if err != nil {
   414  		return cache.CachedBundle{}, err
   415  	}
   416  
   417  	opts.RelocationMapping = cachedBundle.RelocationFilePath
   418  
   419  	if opts.Name == "" {
   420  		opts.Name = cachedBundle.Definition.Name
   421  	}
   422  
   423  	return cachedBundle, nil
   424  }
   425  
   426  // createRun generates a Run record instructing porter exactly how to run the bundle
   427  // and includes audit/status fields as well.
   428  func (p *Porter) createRun(ctx context.Context, bundleRef cnab.BundleReference, inst storage.Installation, action string, params map[string]interface{}) (storage.Run, error) {
   429  	ctx, span := tracing.StartSpan(ctx)
   430  	defer span.EndSpan()
   431  
   432  	// Create a record for the run we are about to execute
   433  	var currentRun = inst.NewRun(action, bundleRef.Definition)
   434  	currentRun.Bundle = bundleRef.Definition.Bundle
   435  	currentRun.BundleReference = bundleRef.Reference.String()
   436  	currentRun.BundleDigest = bundleRef.Digest.String()
   437  
   438  	var err error
   439  	cleanParams, err := p.Sanitizer.CleanRawParameters(ctx, params, bundleRef.Definition, currentRun.ID)
   440  	if err != nil {
   441  		return storage.Run{}, span.Error(err)
   442  	}
   443  	currentRun.Parameters.Parameters = cleanParams
   444  
   445  	// TODO: Do not save secrets when the run isn't recorded
   446  	currentRun.ParameterOverrides = storage.LinkSensitiveParametersToSecrets(currentRun.ParameterOverrides, bundleRef.Definition, currentRun.ID)
   447  	currentRun.ParameterSets = inst.ParameterSets
   448  
   449  	// Persist an audit record of the credential sets used to determine the final
   450  	// credentials injected into the bundle.
   451  	//
   452  	// These should remain in the order specified on the installation, and not
   453  	// sorted, so that the last specified set overrides the one before it when a
   454  	// value is specified in more than one set.
   455  	currentRun.CredentialSets = inst.CredentialSets
   456  
   457  	// Combine the credential sets above into a single credential set we can resolve just-in-time (JIT) before running the bundle.
   458  	finalCreds := make(map[string]secrets.SourceMap, len(currentRun.Bundle.Credentials))
   459  	for _, csName := range currentRun.CredentialSets {
   460  		var cs storage.CredentialSet
   461  		// Try to get the creds in the local namespace first, fallback to the global creds
   462  		query := storage.FindOptions{
   463  			Sort: []string{"-namespace"},
   464  			Filter: bson.M{
   465  				"name": csName,
   466  				"$or": []bson.M{
   467  					{"namespace": ""},
   468  					{"namespace": currentRun.Namespace},
   469  				},
   470  			},
   471  		}
   472  		store := p.Credentials.GetDataStore()
   473  		err := store.FindOne(ctx, storage.CollectionCredentials, query, &cs)
   474  		if err != nil {
   475  			return storage.Run{}, span.Errorf("could not find credential set named %s in the %s namespace or global namespace: %w", csName, inst.Namespace, err)
   476  		}
   477  
   478  		for _, cred := range cs.Credentials {
   479  			credDef, ok := currentRun.Bundle.Credentials[cred.Name]
   480  			if !ok || !credDef.AppliesTo(currentRun.Action) {
   481  				// ignore extra credential mappings in the set that are not defined by the bundle or used by the current action
   482  				// it's okay to over specify so that people can reuse sets better
   483  				continue
   484  			}
   485  
   486  			// If a credential is mapped in multiple credential sets, the strategy associated with the last set specified "wins"
   487  			finalCreds[cred.Name] = cred
   488  		}
   489  	}
   490  
   491  	if len(finalCreds) > 0 {
   492  		// Store the composite credential set on the run, so that the runtime can later resolve them in a single step
   493  		currentRun.Credentials = storage.NewInternalCredentialSet()
   494  		for _, cred := range finalCreds {
   495  			currentRun.Credentials.Credentials = append(currentRun.Credentials.Credentials, cred)
   496  		}
   497  	}
   498  
   499  	return currentRun, nil
   500  }