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 }