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, ¤tRun); 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, ¤tRun); 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 }