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 }