get.porter.sh/porter@v1.3.0/pkg/cnab/provider/action.go (about)

     1  package cnabprovider
     2  
     3  import (
     4  	"context"
     5  	"encoding/json"
     6  	"errors"
     7  	"fmt"
     8  
     9  	"get.porter.sh/porter/pkg/cnab"
    10  	"get.porter.sh/porter/pkg/config"
    11  	"get.porter.sh/porter/pkg/storage"
    12  	"get.porter.sh/porter/pkg/tracing"
    13  	cnabaction "github.com/cnabio/cnab-go/action"
    14  	"github.com/cnabio/cnab-go/driver"
    15  	"github.com/hashicorp/go-multierror"
    16  	"go.opentelemetry.io/otel/attribute"
    17  )
    18  
    19  type HostVolumeMountSpec struct {
    20  	Source   string
    21  	Target   string
    22  	ReadOnly bool
    23  }
    24  
    25  // ActionArguments are the shared arguments for all bundle runs.
    26  type ActionArguments struct {
    27  	// Name of the installation.
    28  	Installation storage.Installation
    29  
    30  	// Run defines how to execute the bundle.
    31  	Run storage.Run
    32  
    33  	// BundleReference is the set of information necessary to execute a bundle.
    34  	BundleReference cnab.BundleReference
    35  
    36  	// Additional files to copy into the bundle
    37  	// Target Path => File Contents
    38  	Files map[string]string
    39  
    40  	// Driver is the CNAB-compliant driver used to run bundle actions.
    41  	Driver string
    42  
    43  	// Give the bundle privileged access to the docker daemon.
    44  	AllowDockerHostAccess bool
    45  
    46  	// MountHostVolumes is a map of host paths to container paths to mount.
    47  	HostVolumeMounts []HostVolumeMountSpec
    48  
    49  	// PersistLogs specifies if the bundle image output should be saved as an output.
    50  	PersistLogs bool
    51  }
    52  
    53  func (r *Runtime) ApplyConfig(ctx context.Context, args ActionArguments) cnabaction.OperationConfigs {
    54  	return cnabaction.OperationConfigs{
    55  		r.SetOutput(),
    56  		r.AddFiles(ctx, args),
    57  		r.AddEnvironment(args),
    58  		r.AddRelocation(args),
    59  	}
    60  }
    61  
    62  func (r *Runtime) SetOutput() cnabaction.OperationConfigFunc {
    63  	return func(op *driver.Operation) error {
    64  		op.Out = r.Out
    65  		op.Err = r.Err
    66  		return nil
    67  	}
    68  }
    69  
    70  func (r *Runtime) AddFiles(ctx context.Context, args ActionArguments) cnabaction.OperationConfigFunc {
    71  	return func(op *driver.Operation) error {
    72  		if op.Files == nil {
    73  			op.Files = make(map[string]string, 1)
    74  		}
    75  
    76  		for k, v := range args.Files {
    77  			op.Files[k] = v
    78  		}
    79  
    80  		// Add claim.json to file list as well, if exists
    81  		run, err := r.installations.GetLastRun(ctx, args.Installation.Namespace, args.Installation.Name)
    82  		if err == nil {
    83  			claim := run.ToCNAB()
    84  			claimBytes, err := json.Marshal(claim)
    85  			if err != nil {
    86  				return fmt.Errorf("could not marshal run %s for installation %s: %w", run.ID, args.Installation, err)
    87  			}
    88  			op.Files[config.ClaimFilepath] = string(claimBytes)
    89  		}
    90  
    91  		return nil
    92  	}
    93  }
    94  
    95  func (r *Runtime) AddEnvironment(args ActionArguments) cnabaction.OperationConfigFunc {
    96  	const verbosityEnv = "PORTER_VERBOSITY"
    97  
    98  	return func(op *driver.Operation) error {
    99  		op.Environment[config.EnvPorterInstallationNamespace] = args.Installation.Namespace
   100  		op.Environment[config.EnvPorterInstallationName] = args.Installation.Name
   101  
   102  		// Pass the verbosity from porter's local config into the bundle
   103  		op.Environment[verbosityEnv] = r.Config.GetVerbosity().Level().String()
   104  
   105  		return nil
   106  	}
   107  }
   108  
   109  // AddRelocation operates on an ActionArguments and adds any provided relocation mapping
   110  // to the operation's files.
   111  func (r *Runtime) AddRelocation(args ActionArguments) cnabaction.OperationConfigFunc {
   112  	return func(op *driver.Operation) error {
   113  		if len(args.BundleReference.RelocationMap) > 0 {
   114  			b, err := json.MarshalIndent(args.BundleReference.RelocationMap, "", "    ")
   115  			if err != nil {
   116  				return fmt.Errorf("error marshaling relocation mapping file: %w", err)
   117  			}
   118  
   119  			op.Files["/cnab/app/relocation-mapping.json"] = string(b)
   120  
   121  			// If the bundle image is present in the relocation mapping, we need
   122  			// to update the operation and set the new image reference. Unfortunately,
   123  			// the relocation mapping is just reference => reference, so there isn't a
   124  			// great way to check for the bundle image.
   125  			if mappedInvo, ok := args.BundleReference.RelocationMap[op.Image.Image]; ok {
   126  				op.Image.Image = mappedInvo
   127  			}
   128  		}
   129  		return nil
   130  	}
   131  }
   132  
   133  func (r *Runtime) Execute(ctx context.Context, args ActionArguments) error {
   134  	// Check if we've been asked to stop before executing long blocking calls
   135  	select {
   136  	case <-ctx.Done():
   137  		return ctx.Err()
   138  	default:
   139  		currentRun := args.Run
   140  		ctx, log := tracing.StartSpan(ctx,
   141  			attribute.String("action", currentRun.Action),
   142  			attribute.Bool("allowDockerHostAccess", args.AllowDockerHostAccess),
   143  			attribute.String("driver", args.Driver))
   144  		defer log.EndSpan()
   145  		args.BundleReference.AddToTrace(ctx)
   146  		args.Installation.AddToTrace(ctx)
   147  
   148  		if currentRun.Action == "" {
   149  			return log.Error(errors.New("action is required"))
   150  		}
   151  
   152  		b, err := r.ProcessBundle(ctx, args.BundleReference.Definition)
   153  		if err != nil {
   154  			return err
   155  		}
   156  
   157  		// Validate the action
   158  		if _, err := b.GetAction(currentRun.Action); err != nil {
   159  			return log.Errorf("invalid action '%s' specified for bundle %s: %w", currentRun.Action, b.Name, err)
   160  		}
   161  
   162  		log.Debugf("Using runtime driver %s\n", args.Driver)
   163  		driver, err := r.newDriver(args.Driver, args)
   164  		if err != nil {
   165  			return log.Errorf("unable to instantiate driver: %w", err)
   166  		}
   167  
   168  		a := cnabaction.New(driver)
   169  		a.SaveLogs = args.PersistLogs
   170  
   171  		// Resolve parameters and credentials just-in-time (JIT) before running the bundle, do this at the *LAST* possible moment
   172  		log.Info("Just-in-time resolving credentials...")
   173  		if err = r.loadCredentials(ctx, b, &currentRun); err != nil {
   174  			return log.Errorf("could not resolve credentials before running the bundle: %w", err)
   175  		}
   176  		log.Info("Just-in-time resolving parameters...")
   177  		if err = r.loadParameters(ctx, b, &currentRun); err != nil {
   178  			return log.Errorf("could not resolve parameters before running the bundle: %w", err)
   179  		}
   180  
   181  		if currentRun.ShouldRecord() {
   182  			err = r.SaveRun(ctx, args.Installation, currentRun, cnab.StatusRunning)
   183  			if err != nil {
   184  				return log.Errorf("could not save the pending action's status, the bundle was not executed: %w", err)
   185  			}
   186  		}
   187  
   188  		cnabClaim := currentRun.ToCNAB()
   189  		cnabCreds := currentRun.Credentials.ToCNAB()
   190  		// The claim and credentials contain sensitive values. Only trace it in special dev builds (nothing is traced for release builds)
   191  		log.SetSensitiveAttributes(
   192  			tracing.ObjectAttribute("cnab-claim", cnabClaim),
   193  			tracing.ObjectAttribute("cnab-credentials", cnabCreds))
   194  		opResult, result, err := a.Run(cnabClaim, cnabCreds, r.ApplyConfig(ctx, args)...)
   195  
   196  		if currentRun.ShouldRecord() {
   197  			if err != nil {
   198  				err = r.appendFailedResult(ctx, err, currentRun)
   199  				return log.Errorf("failed to record that %s for installation %s failed: %w", currentRun.Action, args.Installation.Name, err)
   200  			}
   201  			return r.SaveOperationResult(ctx, opResult, args.Installation, currentRun, currentRun.NewResultFrom(result))
   202  		}
   203  
   204  		if err != nil {
   205  			return log.Errorf("execution of %s for installation %s failed: %w", currentRun.Action, args.Installation.Name, err)
   206  		}
   207  
   208  		return nil
   209  	}
   210  }
   211  
   212  // SaveRun with the specified status.
   213  func (r *Runtime) SaveRun(ctx context.Context, installation storage.Installation, run storage.Run, status string) error {
   214  	ctx, span := tracing.StartSpan(ctx)
   215  	defer span.EndSpan()
   216  
   217  	span.Debugf("saving action %s for %s installation with status %s", run.Action, installation, status)
   218  
   219  	// update installation record to use run id encoded parameters instead of
   220  	// installation id
   221  	installation.Parameters.Parameters = run.ParameterOverrides.Parameters
   222  	err := r.installations.UpsertInstallation(ctx, installation)
   223  	if err != nil {
   224  		return span.Error(fmt.Errorf("error saving the installation record before executing the bundle: %w", err))
   225  	}
   226  
   227  	err = r.installations.UpsertRun(ctx, run)
   228  	if err != nil {
   229  		return span.Error(fmt.Errorf("error saving the installation run record before executing the bundle: %w", err))
   230  	}
   231  
   232  	result := run.NewResult(status)
   233  	err = r.installations.InsertResult(ctx, result)
   234  	if err != nil {
   235  		return span.Error(fmt.Errorf("error saving the installation status record before executing the bundle: %w", err))
   236  	}
   237  
   238  	return nil
   239  }
   240  
   241  // SaveOperationResult saves the ClaimResult and Outputs. The caller is
   242  // responsible for having already persisted the claim itself, for example using
   243  // SaveRun.
   244  func (r *Runtime) SaveOperationResult(ctx context.Context, opResult driver.OperationResult, installation storage.Installation, run storage.Run, result storage.Result) error {
   245  	ctx, span := tracing.StartSpan(ctx)
   246  	defer span.EndSpan()
   247  
   248  	// TODO(carolynvs): optimistic locking on updates
   249  
   250  	// Keep accumulating errors from any error returned from the operation
   251  	// We must save the claim even when the op failed, but we want to report
   252  	// ALL errors back.
   253  	var bigerr *multierror.Error
   254  	bigerr = multierror.Append(bigerr, opResult.Error)
   255  
   256  	err := r.installations.InsertResult(ctx, result)
   257  	if err != nil {
   258  		bigerr = multierror.Append(bigerr, fmt.Errorf("error adding %s result for %s run of installation %s\n%#v: %w", result.Status, run.Action, installation, result, err))
   259  	}
   260  
   261  	installation.ApplyResult(run, result)
   262  	err = r.installations.UpdateInstallation(ctx, installation)
   263  	if err != nil {
   264  		bigerr = multierror.Append(bigerr, fmt.Errorf("error updating installation record for %s\n%#v: %w", installation, installation, err))
   265  	}
   266  
   267  	for outputName, outputValue := range opResult.Outputs {
   268  		output := result.NewOutput(outputName, []byte(outputValue))
   269  		output, err = r.sanitizer.CleanOutput(ctx, output, cnab.ExtendedBundle{Bundle: run.Bundle})
   270  		if err != nil {
   271  			bigerr = multierror.Append(bigerr, fmt.Errorf("error sanitizing sensitive %s output for %s run of installation %s\n%#v: %w", output.Name, run.Action, installation, output, err))
   272  		}
   273  		err = r.installations.InsertOutput(ctx, output)
   274  		if err != nil {
   275  			bigerr = multierror.Append(bigerr, fmt.Errorf("error adding %s output for %s run of installation %s\n%#v: %w", output.Name, run.Action, installation, output, err))
   276  		}
   277  	}
   278  
   279  	return bigerr.ErrorOrNil()
   280  }
   281  
   282  // appendFailedResult creates a failed result from the operation error and accumulates
   283  // the error(s).
   284  func (r *Runtime) appendFailedResult(ctx context.Context, opErr error, run storage.Run) error {
   285  	saveResult := func() error {
   286  		result := run.NewResult(cnab.StatusFailed)
   287  		return r.installations.InsertResult(ctx, result)
   288  	}
   289  
   290  	resultErr := saveResult()
   291  
   292  	// Accumulate any errors from the operation with the persistence errors
   293  	return multierror.Append(opErr, resultErr).ErrorOrNil()
   294  }