get.porter.sh/porter@v1.3.0/pkg/porter/explain.go (about) 1 package porter 2 3 import ( 4 "context" 5 "fmt" 6 "sort" 7 "strconv" 8 "strings" 9 10 "get.porter.sh/porter/pkg/cnab" 11 configadapter "get.porter.sh/porter/pkg/cnab/config-adapter" 12 "get.porter.sh/porter/pkg/portercontext" 13 "get.porter.sh/porter/pkg/printer" 14 "github.com/cnabio/cnab-go/bundle" 15 ) 16 17 type ExplainOpts struct { 18 BundleReferenceOptions 19 printer.PrintOptions 20 21 Action string 22 } 23 24 // PrintableBundle holds a subset of pertinent values to be explained from a bundle 25 type PrintableBundle struct { 26 Name string `json:"name" yaml:"name"` 27 Description string `json:"description,omitempty" yaml:"description,omitempty"` 28 Version string `json:"version" yaml:"version"` 29 PorterVersion string `json:"porterVersion,omitempty" yaml:"porterVersion,omitempty"` 30 Parameters []PrintableParameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` 31 Credentials []PrintableCredential `json:"credentials,omitempty" yaml:"credentials,omitempty"` 32 Outputs []PrintableOutput `json:"outputs,omitempty" yaml:"outputs,omitempty"` 33 Actions []PrintableAction `json:"customActions,omitempty" yaml:"customActions,omitempty"` 34 Dependencies []PrintableDependency `json:"dependencies,omitempty" yaml:"dependencies,omitempty"` 35 Mixins []string `json:"mixins" yaml:"mixins"` 36 Custom map[string]interface{} `json:"custom,omitempty" yaml:"custom,omitempty"` 37 } 38 39 type PrintableCredential struct { 40 Name string `json:"name" yaml:"name"` 41 Description string `json:"description" yaml:"description"` 42 Required bool `json:"required" yaml:"required"` 43 ApplyTo string `json:"applyTo" yaml:"applyTo"` 44 } 45 46 type SortPrintableCredential []PrintableCredential 47 48 func (s SortPrintableCredential) Len() int { 49 return len(s) 50 } 51 52 func (s SortPrintableCredential) Less(i, j int) bool { 53 return s[i].Name < s[j].Name 54 } 55 56 func (s SortPrintableCredential) Swap(i, j int) { 57 s[i], s[j] = s[j], s[i] 58 } 59 60 type PrintableOutput struct { 61 Name string `json:"name" yaml:"name"` 62 Type interface{} `json:"type" yaml:"type"` 63 ApplyTo string `json:"applyTo" yaml:"applyTo"` 64 Description string `json:"description" yaml:"description"` 65 } 66 67 type SortPrintableOutput []PrintableOutput 68 69 func (s SortPrintableOutput) Len() int { 70 return len(s) 71 } 72 73 func (s SortPrintableOutput) Less(i, j int) bool { 74 return s[i].Name < s[j].Name 75 } 76 77 func (s SortPrintableOutput) Swap(i, j int) { 78 s[i], s[j] = s[j], s[i] 79 } 80 81 type PrintableDependency struct { 82 Alias string `json:"alias" yaml:"alias"` 83 Reference string `json:"reference" yaml:"reference"` 84 } 85 86 type PrintableParameter struct { 87 param *bundle.Parameter 88 Name string `json:"name" yaml:"name"` 89 Type interface{} `json:"type" yaml:"type"` 90 Default interface{} `json:"default" yaml:"default"` 91 ApplyTo string `json:"applyTo" yaml:"applyTo"` 92 Description string `json:"description" yaml:"description"` 93 Required bool `json:"required" yaml:"required"` 94 Sensitive bool `json:"sensitive" yaml:"sensitive"` 95 } 96 97 type SortPrintableParameter []PrintableParameter 98 99 func (s SortPrintableParameter) Len() int { 100 return len(s) 101 } 102 103 func (s SortPrintableParameter) Less(i, j int) bool { 104 return s[i].Name < s[j].Name 105 } 106 107 func (s SortPrintableParameter) Swap(i, j int) { 108 s[i], s[j] = s[j], s[i] 109 } 110 111 type PrintableAction struct { 112 Name string `json:"name" yaml:"name"` 113 Modifies bool `json:"modifies" yaml:"modifies"` 114 // Stateless indicates that the action is purely informational, that credentials are not required, and that the runtime should not keep track of its invocation 115 Stateless bool `json:"stateless" yaml:"stateless"` 116 // Description describes the action as a user-readable string 117 Description string `json:"description" yaml:"description"` 118 } 119 120 type SortPrintableAction []PrintableAction 121 122 func (s SortPrintableAction) Len() int { 123 return len(s) 124 } 125 126 func (s SortPrintableAction) Less(i, j int) bool { 127 return s[i].Name < s[j].Name 128 } 129 130 func (s SortPrintableAction) Swap(i, j int) { 131 s[i], s[j] = s[j], s[i] 132 } 133 134 func (o *ExplainOpts) Validate(args []string, pctx *portercontext.Context) error { 135 // Allow reference to be specified as a positional argument, or using --reference 136 if len(args) == 1 { 137 o.Reference = args[0] 138 } else if len(args) > 1 { 139 return fmt.Errorf("only one positional argument may be specified, the bundle reference, but multiple were received: %s", args) 140 } 141 142 err := o.BundleDefinitionOptions.Validate(pctx) 143 if err != nil { 144 return err 145 } 146 147 err = o.ParseFormat() 148 if err != nil { 149 return err 150 } 151 if o.Reference != "" { 152 o.File = "" 153 o.CNABFile = "" 154 155 return o.validateReference() 156 } 157 return nil 158 } 159 160 func (p *Porter) Explain(ctx context.Context, o ExplainOpts) error { 161 bundleRef, err := o.GetBundleReference(ctx, p) 162 if err != nil { 163 return err 164 } 165 166 pb, err := generatePrintable(bundleRef.Definition, o.Action) 167 if err != nil { 168 return fmt.Errorf("unable to print bundle: %w", err) 169 } 170 return p.printBundleExplain(o, pb, bundleRef.Definition) 171 } 172 173 func (p *Porter) printBundleExplain(o ExplainOpts, pb *PrintableBundle, bun cnab.ExtendedBundle) error { 174 switch o.Format { 175 case printer.FormatJson: 176 return printer.PrintJson(p.Out, pb) 177 case printer.FormatYaml: 178 return printer.PrintYaml(p.Out, pb) 179 case printer.FormatPlaintext: 180 return p.printBundleExplainTable(pb, o.Reference, bun) 181 default: 182 return fmt.Errorf("invalid format: %s", o.Format) 183 } 184 } 185 186 func generatePrintable(bun cnab.ExtendedBundle, action string) (*PrintableBundle, error) { 187 var stamp configadapter.Stamp 188 189 stamp, err := configadapter.LoadStamp(bun) 190 if err != nil { 191 stamp = configadapter.Stamp{} 192 } 193 194 deps, err := bun.ResolveDependencies(bun) 195 if err != nil { 196 return nil, fmt.Errorf("error resolving bundle dependencies: %w", err) 197 } 198 199 pb := PrintableBundle{ 200 Name: bun.Name, 201 Description: bun.Description, 202 Version: bun.Version, 203 PorterVersion: stamp.Version, 204 Actions: make([]PrintableAction, 0, len(bun.Actions)), 205 Credentials: make([]PrintableCredential, 0, len(bun.Credentials)), 206 Parameters: make([]PrintableParameter, 0, len(bun.Parameters)), 207 Outputs: make([]PrintableOutput, 0, len(bun.Outputs)), 208 Dependencies: make([]PrintableDependency, 0, len(deps)), 209 Mixins: make([]string, 0, len(stamp.Mixins)), 210 Custom: make(map[string]interface{}), 211 } 212 213 for a, v := range bun.Actions { 214 pa := PrintableAction{} 215 pa.Name = a 216 pa.Description = v.Description 217 pa.Modifies = v.Modifies 218 pa.Stateless = v.Stateless 219 pb.Actions = append(pb.Actions, pa) 220 } 221 sort.Sort(SortPrintableAction(pb.Actions)) 222 223 for c, v := range bun.Credentials { 224 pc := PrintableCredential{} 225 pc.Name = c 226 pc.Description = v.Description 227 pc.Required = v.Required 228 pc.ApplyTo = generateApplyToString(v.ApplyTo) 229 230 if shouldIncludeInExplainOutput(&v, action) { 231 pb.Credentials = append(pb.Credentials, pc) 232 } 233 } 234 sort.Sort(SortPrintableCredential(pb.Credentials)) 235 236 for p, v := range bun.Parameters { 237 v := v // Go closures are funny like that 238 if bun.IsInternalParameter(p) || bun.ParameterHasSource(p) { 239 continue 240 } 241 242 def, ok := bun.Definitions[v.Definition] 243 if !ok { 244 return nil, fmt.Errorf("unable to find definition %s", v.Definition) 245 } 246 if def == nil { 247 return nil, fmt.Errorf("empty definition for %s", v.Definition) 248 } 249 pp := PrintableParameter{param: &v} 250 pp.Name = p 251 pp.Type = bun.GetParameterType(def) 252 pp.Default = def.Default 253 pp.ApplyTo = generateApplyToString(v.ApplyTo) 254 pp.Required = v.Required 255 pp.Description = v.Description 256 pp.Sensitive = bun.IsSensitiveParameter((p)) 257 258 if shouldIncludeInExplainOutput(&v, action) { 259 pb.Parameters = append(pb.Parameters, pp) 260 } 261 } 262 sort.Sort(SortPrintableParameter(pb.Parameters)) 263 264 for o, v := range bun.Outputs { 265 if bun.IsInternalOutput(o) { 266 continue 267 } 268 269 def, ok := bun.Definitions[v.Definition] 270 if !ok { 271 return nil, fmt.Errorf("unable to find definition %s", v.Definition) 272 } 273 if def == nil { 274 return nil, fmt.Errorf("empty definition for %s", v.Definition) 275 } 276 po := PrintableOutput{} 277 po.Name = o 278 po.Type = def.Type 279 po.ApplyTo = generateApplyToString(v.ApplyTo) 280 po.Description = v.Description 281 282 if shouldIncludeInExplainOutput(&v, action) { 283 pb.Outputs = append(pb.Outputs, po) 284 } 285 } 286 sort.Sort(SortPrintableOutput(pb.Outputs)) 287 288 for _, dep := range deps { 289 pd := PrintableDependency{} 290 pd.Alias = dep.Alias 291 pd.Reference = dep.Reference 292 293 pb.Dependencies = append(pb.Dependencies, pd) 294 } 295 // dependencies are sorted by their dependency sequence already 296 297 for mixin := range stamp.Mixins { 298 pb.Mixins = append(pb.Mixins, mixin) 299 } 300 sort.Strings(pb.Mixins) 301 302 for key, value := range bun.Custom { 303 if isUserDefinedCustomSectionKey(key) { 304 pb.Custom[key] = value 305 } 306 } 307 308 return &pb, nil 309 } 310 311 // shouldIncludeInExplainOutput determine if a scoped item such as a credential, parameter or output 312 // should be included in the explain output. 313 func shouldIncludeInExplainOutput(scoped bundle.Scoped, action string) bool { 314 if action == "" { 315 return true 316 } 317 318 return bundle.AppliesTo(scoped, action) 319 } 320 321 // isUserDefinedCustomSectionKey returns true if the given key in the custom section data is 322 // user-defined and not one that Porter uses for its own purposes. 323 func isUserDefinedCustomSectionKey(key string) bool { 324 porterKeyPrefixes := []string{ 325 "io.cnab", 326 "sh.porter", 327 } 328 329 for _, keyPrefix := range porterKeyPrefixes { 330 if strings.HasPrefix(key, keyPrefix) { 331 return false 332 } 333 } 334 335 return true 336 } 337 338 func generateApplyToString(appliesTo []string) string { 339 if len(appliesTo) == 0 { 340 return "All Actions" 341 } 342 return strings.Join(appliesTo, ",") 343 344 } 345 346 func (p *Porter) printBundleExplainTable(bun *PrintableBundle, bundleReference string, extendedBundle cnab.ExtendedBundle) error { 347 fmt.Fprintf(p.Out, "Name: %s\n", bun.Name) 348 fmt.Fprintf(p.Out, "Description: %s\n", bun.Description) 349 fmt.Fprintf(p.Out, "Version: %s\n", bun.Version) 350 if bun.PorterVersion != "" { 351 fmt.Fprintf(p.Out, "Porter Version: %s\n", bun.PorterVersion) 352 } 353 fmt.Fprintln(p.Out, "") 354 355 err := p.printCredentialsExplainBlock(bun) 356 if err != nil { 357 return err 358 } 359 err = p.printParametersExplainBlock(bun) 360 if err != nil { 361 return err 362 } 363 err = p.printOutputsExplainBlock(bun) 364 if err != nil { 365 return err 366 } 367 err = p.printActionsExplainBlock(bun) 368 if err != nil { 369 return err 370 } 371 err = p.printDependenciesExplainBlock(bun) 372 if err != nil { 373 return err 374 } 375 376 if extendedBundle.IsPorterBundle() && len(bun.Mixins) > 0 { 377 fmt.Fprintf(p.Out, "This bundle uses the following tools: %s.\n", strings.Join(bun.Mixins, ", ")) 378 } 379 380 if extendedBundle.SupportsDocker() { 381 fmt.Fprintln(p.Out, "") // force a blank line before this block 382 fmt.Fprintf(p.Out, "🚨 This bundle will grant docker access to the host, make sure the publisher of this bundle is trusted.") 383 fmt.Fprintln(p.Out, "") // force a blank line after this block 384 } 385 386 err = p.printInstallationInstructionBlock(bun, bundleReference, extendedBundle) 387 if err != nil { 388 return err 389 } 390 return nil 391 } 392 393 func (p *Porter) printCredentialsExplainBlock(bun *PrintableBundle) error { 394 if len(bun.Credentials) == 0 { 395 return nil 396 } 397 398 fmt.Fprintln(p.Out, "Credentials:") 399 err := p.printCredentialsExplainTable(bun) 400 if err != nil { 401 return fmt.Errorf("unable to print credentials table: %w", err) 402 } 403 404 fmt.Fprintln(p.Out, "") // force a blank line after this block 405 return nil 406 } 407 func (p *Porter) printCredentialsExplainTable(bun *PrintableBundle) error { 408 printCredRow := 409 func(v interface{}) []string { 410 c, ok := v.(PrintableCredential) 411 if !ok { 412 return nil 413 } 414 return []string{c.Name, c.Description, strconv.FormatBool(c.Required), c.ApplyTo} 415 } 416 return printer.PrintTable(p.Out, bun.Credentials, printCredRow, "Name", "Description", "Required", "Applies To") 417 } 418 419 func (p *Porter) printParametersExplainBlock(bun *PrintableBundle) error { 420 if len(bun.Parameters) == 0 { 421 return nil 422 } 423 424 fmt.Fprintln(p.Out, "Parameters:") 425 err := p.printParametersExplainTable(bun) 426 if err != nil { 427 return fmt.Errorf("unable to print parameters table: %w", err) 428 } 429 430 fmt.Fprintln(p.Out, "") // force a blank line after this block 431 return nil 432 } 433 func (p *Porter) printParametersExplainTable(bun *PrintableBundle) error { 434 printParamRow := 435 func(v interface{}) []string { 436 p, ok := v.(PrintableParameter) 437 if !ok { 438 return nil 439 } 440 return []string{p.Name, p.Description, fmt.Sprintf("%v", p.Type), fmt.Sprintf("%v", p.Default), strconv.FormatBool(p.Required), p.ApplyTo} 441 } 442 return printer.PrintTable(p.Out, bun.Parameters, printParamRow, "Name", "Description", "Type", "Default", "Required", "Applies To") 443 } 444 445 func (p *Porter) printOutputsExplainBlock(bun *PrintableBundle) error { 446 if len(bun.Outputs) == 0 { 447 return nil 448 } 449 450 fmt.Fprintln(p.Out, "Outputs:") 451 err := p.printOutputsExplainTable(bun) 452 if err != nil { 453 return fmt.Errorf("unable to print outputs table: %w", err) 454 } 455 456 fmt.Fprintln(p.Out, "") // force a blank line after this block 457 return nil 458 } 459 460 func (p *Porter) printOutputsExplainTable(bun *PrintableBundle) error { 461 printOutputRow := 462 func(v interface{}) []string { 463 o, ok := v.(PrintableOutput) 464 if !ok { 465 return nil 466 } 467 return []string{o.Name, o.Description, fmt.Sprintf("%v", o.Type), o.ApplyTo} 468 } 469 return printer.PrintTable(p.Out, bun.Outputs, printOutputRow, "Name", "Description", "Type", "Applies To") 470 } 471 472 func (p *Porter) printActionsExplainBlock(bun *PrintableBundle) error { 473 if len(bun.Actions) == 0 { 474 return nil 475 } 476 477 fmt.Fprintln(p.Out, "Actions:") 478 err := p.printActionsExplainTable(bun) 479 if err != nil { 480 return fmt.Errorf("unable to print actions block: %w", err) 481 } 482 483 fmt.Fprintln(p.Out, "") // force a blank line after this block 484 return nil 485 } 486 487 func (p *Porter) printActionsExplainTable(bun *PrintableBundle) error { 488 printActionRow := 489 func(v interface{}) []string { 490 a, ok := v.(PrintableAction) 491 if !ok { 492 return nil 493 } 494 return []string{a.Name, a.Description, strconv.FormatBool(a.Modifies), strconv.FormatBool(a.Stateless)} 495 } 496 return printer.PrintTable(p.Out, bun.Actions, printActionRow, "Name", "Description", "Modifies Installation", "Stateless") 497 } 498 499 // Dependencies 500 func (p *Porter) printDependenciesExplainBlock(bun *PrintableBundle) error { 501 if len(bun.Dependencies) == 0 { 502 return nil 503 } 504 505 fmt.Fprintln(p.Out, "Dependencies:") 506 err := p.printDependenciesExplainTable(bun) 507 if err != nil { 508 return fmt.Errorf("unable to print dependencies table: %w", err) 509 } 510 511 fmt.Fprintln(p.Out, "") // force a blank line after this block 512 return nil 513 } 514 515 func (p *Porter) printDependenciesExplainTable(bun *PrintableBundle) error { 516 printDependencyRow := 517 func(v interface{}) []string { 518 o, ok := v.(PrintableDependency) 519 if !ok { 520 return nil 521 } 522 return []string{o.Alias, o.Reference} 523 } 524 return printer.PrintTable(p.Out, bun.Dependencies, printDependencyRow, "Alias", "Reference") 525 } 526 527 func (p *Porter) printInstallationInstructionBlock(bun *PrintableBundle, bundleReference string, extendedBundle cnab.ExtendedBundle) error { 528 fmt.Fprintln(p.Out) 529 fmt.Fprint(p.Out, "To install this bundle run the following command, passing --param KEY=VALUE for any parameters you want to customize:\n") 530 531 var bundleReferenceFlag string 532 if bundleReference != "" { 533 bundleReferenceFlag += " --reference " + bundleReference 534 } 535 536 // Generate predefined credential set first. 537 if len(bun.Credentials) > 0 { 538 fmt.Fprintf(p.Out, "porter credentials generate mycreds%s\n", bundleReferenceFlag) 539 } 540 541 // Bundle installation instruction 542 var requiredParameterFlags string 543 for _, parameter := range bun.Parameters { 544 // Only include parameters required for install 545 if parameter.Required && shouldIncludeInExplainOutput(parameter.param, cnab.ActionInstall) { 546 requiredParameterFlags += parameter.Name + "=TODO " 547 } 548 } 549 550 if requiredParameterFlags != "" { 551 requiredParameterFlags = " --param " + requiredParameterFlags 552 } 553 554 var credentialFlags string 555 if len(bun.Credentials) > 0 { 556 credentialFlags += " --credential-set mycreds" 557 } 558 559 porterInstallCommand := fmt.Sprintf("porter install%s%s%s", bundleReferenceFlag, requiredParameterFlags, credentialFlags) 560 561 // Check whether the bundle requires docker socket to be mounted into the bundle. 562 // Add flag for docker host access for install command if it requires to do so. 563 if extendedBundle.SupportsDocker() { 564 porterInstallCommand += " --allow-docker-host-access" 565 } 566 567 fmt.Fprint(p.Out, porterInstallCommand) 568 fmt.Fprintln(p.Out, "") // force a blank line after this block 569 570 return nil 571 }