get.porter.sh/porter@v1.3.0/pkg/porter/credentials.go (about) 1 package porter 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "path/filepath" 8 "strings" 9 "time" 10 11 "get.porter.sh/porter/pkg/editor" 12 "get.porter.sh/porter/pkg/encoding" 13 "get.porter.sh/porter/pkg/generator" 14 "get.porter.sh/porter/pkg/printer" 15 "get.porter.sh/porter/pkg/storage" 16 "get.porter.sh/porter/pkg/tracing" 17 dtprinter "github.com/carolynvs/datetime-printer" 18 "github.com/olekukonko/tablewriter" 19 "go.opentelemetry.io/otel/attribute" 20 ) 21 22 // CredentialShowOptions represent options for Porter's credential show command 23 type CredentialShowOptions struct { 24 printer.PrintOptions 25 Name string 26 Namespace string 27 } 28 29 type CredentialEditOptions struct { 30 Name string 31 Namespace string 32 } 33 34 // ListCredentials lists saved credential sets. 35 func (p *Porter) ListCredentials(ctx context.Context, opts ListOptions) ([]DisplayCredentialSet, error) { 36 listOpts := storage.ListOptions{ 37 Namespace: opts.GetNamespace(), 38 Name: opts.Name, 39 Labels: opts.ParseLabels(), 40 Skip: opts.Skip, 41 Limit: opts.Limit, 42 } 43 results, err := p.Credentials.ListCredentialSets(ctx, listOpts) 44 if err != nil { 45 return nil, err 46 } 47 48 displayResults := make([]DisplayCredentialSet, len(results)) 49 for i, cs := range results { 50 displayResults[i] = NewDisplayCredentialSet(cs) 51 } 52 53 return displayResults, nil 54 } 55 56 // PrintCredentials prints saved credential sets. 57 func (p *Porter) PrintCredentials(ctx context.Context, opts ListOptions) error { 58 ctx, span := tracing.StartSpan(ctx) 59 defer span.EndSpan() 60 61 creds, err := p.ListCredentials(ctx, opts) 62 if err != nil { 63 return err 64 } 65 66 switch opts.Format { 67 case printer.FormatJson: 68 return printer.PrintJson(p.Out, creds) 69 case printer.FormatYaml: 70 return printer.PrintYaml(p.Out, creds) 71 case printer.FormatPlaintext: 72 // have every row use the same "now" starting ... NOW! 73 now := time.Now() 74 tp := dtprinter.DateTimePrinter{ 75 Now: func() time.Time { return now }, 76 } 77 78 printCredRow := 79 func(v interface{}) []string { 80 cr, ok := v.(DisplayCredentialSet) 81 if !ok { 82 return nil 83 } 84 return []string{cr.Namespace, cr.Name, tp.Format(cr.Status.Modified)} 85 } 86 return printer.PrintTable(p.Out, creds, printCredRow, 87 "NAMESPACE", "NAME", "MODIFIED") 88 default: 89 return span.Error(fmt.Errorf("invalid format: %s", opts.Format)) 90 } 91 } 92 93 // CredentialsOptions are the set of options available to Porter.GenerateCredentials 94 type CredentialOptions struct { 95 BundleReferenceOptions 96 Silent bool 97 Labels []string 98 } 99 100 func (o CredentialOptions) ParseLabels() map[string]string { 101 return parseLabels(o.Labels) 102 } 103 104 // Validate prepares for an action and validates the options. 105 // For example, relative paths are converted to full paths and then checked that 106 // they exist and are accessible. 107 func (o *CredentialOptions) Validate(ctx context.Context, args []string, p *Porter) error { 108 err := o.validateCredName(args) 109 if err != nil { 110 return err 111 } 112 113 return o.BundleReferenceOptions.Validate(ctx, args, p) 114 } 115 116 func (o *CredentialOptions) validateCredName(args []string) error { 117 if len(args) == 1 { 118 o.Name = args[0] 119 } else if len(args) > 1 { 120 return fmt.Errorf("only one positional argument may be specified, the credential name, but multiple were received: %s", args) 121 } 122 return nil 123 } 124 125 // GenerateCredentials builds a new credential set based on the given options. This can be either 126 // a silent build, based on the opts.Silent flag, or interactive using a survey. Returns an 127 // error if unable to generate credentials 128 func (p *Porter) GenerateCredentials(ctx context.Context, opts CredentialOptions) error { 129 ctx, span := tracing.StartSpan(ctx, attribute.String("reference", opts.Reference)) 130 defer span.EndSpan() 131 132 bundleRef, err := opts.GetBundleReference(ctx, p) 133 if err != nil { 134 return err 135 } 136 137 name := opts.Name 138 if name == "" { 139 name = bundleRef.Definition.Name 140 } 141 genOpts := generator.GenerateCredentialsOptions{ 142 GenerateOptions: generator.GenerateOptions{ 143 Name: name, 144 Namespace: opts.Namespace, 145 Labels: opts.ParseLabels(), 146 Silent: opts.Silent, 147 }, 148 Credentials: bundleRef.Definition.Credentials, 149 } 150 span.Infof("Generating new credential %s from bundle %s\n", genOpts.Name, bundleRef.Definition.Name) 151 span.Infof("==> %d credentials required for bundle %s\n", len(genOpts.Credentials), bundleRef.Definition.Name) 152 153 cs, err := generator.GenerateCredentials(genOpts) 154 if err != nil { 155 return span.Error(fmt.Errorf("unable to generate credentials: %w", err)) 156 } 157 158 if len(cs.Credentials) == 0 { 159 return nil 160 } 161 162 cs.Status.Created = time.Now() 163 cs.Status.Modified = cs.Status.Created 164 165 err = p.Credentials.UpsertCredentialSet(ctx, cs) 166 if err != nil { 167 return span.Error(fmt.Errorf("unable to save credentials: %w", err)) 168 } 169 170 return nil 171 } 172 173 // Validate validates the args provided to Porter's credential show command 174 func (o *CredentialShowOptions) Validate(args []string) error { 175 if err := validateCredentialName(args); err != nil { 176 return err 177 } 178 o.Name = args[0] 179 return o.ParseFormat() 180 } 181 182 // Validate validates the args provided to Porter's credential edit command 183 func (o *CredentialEditOptions) Validate(args []string) error { 184 if err := validateCredentialName(args); err != nil { 185 return err 186 } 187 o.Name = args[0] 188 return nil 189 } 190 191 // EditCredential edits the credentials of the provided name. 192 func (p *Porter) EditCredential(ctx context.Context, opts CredentialEditOptions) error { 193 ctx, span := tracing.StartSpan(ctx) 194 defer span.EndSpan() 195 196 credSet, err := p.Credentials.GetCredentialSet(ctx, opts.Namespace, opts.Name) 197 if err != nil { 198 return err 199 } 200 201 // TODO(carolynvs): support editing in yaml, json or toml 202 contents, err := encoding.MarshalYaml(credSet) 203 if err != nil { 204 return span.Error(fmt.Errorf("unable to load credentials: %w", err)) 205 } 206 207 editor := editor.New(p.Context, fmt.Sprintf("porter-%s.yaml", credSet.Name), contents) 208 output, err := editor.Run(ctx) 209 if err != nil { 210 return span.Error(fmt.Errorf("unable to open editor to edit credentials: %w", err)) 211 } 212 213 err = encoding.UnmarshalYaml(output, &credSet) 214 if err != nil { 215 return span.Error(fmt.Errorf("unable to process credentials: %w", err)) 216 } 217 218 err = p.Credentials.Validate(ctx, credSet) 219 if err != nil { 220 return span.Error(fmt.Errorf("credentials are invalid: %w", err)) 221 } 222 223 credSet.Status.Modified = time.Now() 224 err = p.Credentials.UpdateCredentialSet(ctx, credSet) 225 if err != nil { 226 return span.Error(fmt.Errorf("unable to save credentials: %w", err)) 227 } 228 229 return nil 230 } 231 232 type DisplayCredentialSet struct { 233 storage.CredentialSet `yaml:",inline"` 234 } 235 236 func NewDisplayCredentialSet(cs storage.CredentialSet) DisplayCredentialSet { 237 ds := DisplayCredentialSet{CredentialSet: cs} 238 ds.SchemaType = storage.SchemaTypeCredentialSet 239 return ds 240 } 241 242 // ShowCredential shows the credential set corresponding to the provided name, using 243 // the provided printer.PrintOptions for display. 244 func (p *Porter) ShowCredential(ctx context.Context, opts CredentialShowOptions) error { 245 ctx, span := tracing.StartSpan(ctx) 246 defer span.EndSpan() 247 248 cs, err := p.Credentials.GetCredentialSet(ctx, opts.Namespace, opts.Name) 249 if err != nil { 250 return err 251 } 252 253 credSet := NewDisplayCredentialSet(cs) 254 255 switch opts.Format { 256 case printer.FormatJson, printer.FormatYaml: 257 result, err := encoding.Marshal(string(opts.Format), credSet) 258 if err != nil { 259 return err 260 } 261 262 // Note that we are not using span.Info because the command's output must go to standard out 263 fmt.Fprintln(p.Out, string(result)) 264 return nil 265 case printer.FormatPlaintext: 266 // Set up human friendly time formatter 267 now := time.Now() 268 tp := dtprinter.DateTimePrinter{ 269 Now: func() time.Time { return now }, 270 } 271 272 // Here we use an instance of olekukonko/tablewriter as our table, 273 // rather than using the printer pkg variant, as we wish to decorate 274 // the table a bit differently from the default 275 var rows [][]string 276 277 // Iterate through all CredentialStrategies and add to rows 278 for _, cs := range credSet.Credentials { 279 rows = append(rows, []string{cs.Name, cs.Source.Hint, cs.Source.Strategy}) 280 } 281 282 // Build and configure our tablewriter 283 table := tablewriter.NewWriter(p.Out) 284 table.SetCenterSeparator("") 285 table.SetColumnSeparator("") 286 table.SetAlignment(tablewriter.ALIGN_LEFT) 287 table.SetHeaderAlignment(tablewriter.ALIGN_LEFT) 288 table.SetBorders(tablewriter.Border{Left: false, Right: false, Bottom: false, Top: true}) 289 table.SetAutoFormatHeaders(false) 290 291 // First, print the CredentialSet metadata 292 // Note that we are not using span.Info because the command's output must go to standard out 293 fmt.Fprintf(p.Out, "Name: %s\n", credSet.Name) 294 fmt.Fprintf(p.Out, "Namespace: %s\n", credSet.Namespace) 295 fmt.Fprintf(p.Out, "Created: %s\n", tp.Format(credSet.Status.Created)) 296 fmt.Fprintf(p.Out, "Modified: %s\n\n", tp.Format(credSet.Status.Modified)) 297 298 // Print labels, if any 299 if len(credSet.Labels) > 0 { 300 fmt.Fprintln(p.Out, "Labels:") 301 302 for k, v := range credSet.Labels { 303 fmt.Fprintf(p.Out, " %s: %s\n", k, v) 304 } 305 fmt.Fprintln(p.Out) 306 } 307 308 // Now print the table 309 table.SetHeader([]string{"Name", "Local Source", "Source Type"}) 310 for _, row := range rows { 311 table.Append(row) 312 } 313 table.Render() 314 return nil 315 default: 316 return span.Error(fmt.Errorf("invalid format: %s", opts.Format)) 317 } 318 } 319 320 // CredentialDeleteOptions represent options for Porter's credential delete command 321 type CredentialDeleteOptions struct { 322 Name string 323 Namespace string 324 } 325 326 // DeleteCredential deletes the credential set corresponding to the provided 327 // names. 328 func (p *Porter) DeleteCredential(ctx context.Context, opts CredentialDeleteOptions) error { 329 ctx, span := tracing.StartSpan(ctx, 330 attribute.String("namespace", opts.Namespace), 331 attribute.String("name", opts.Name), 332 ) 333 defer span.EndSpan() 334 335 err := p.Credentials.RemoveCredentialSet(ctx, opts.Namespace, opts.Name) 336 if errors.Is(err, storage.ErrNotFound{}) { 337 span.Debug("nothing to remove, credential already does not exist") 338 return nil 339 } 340 if err != nil { 341 return span.Error(fmt.Errorf("unable to delete credential set: %w", err)) 342 } 343 344 return nil 345 } 346 347 // Validate validates the args provided Porter's credential delete command 348 func (o *CredentialDeleteOptions) Validate(args []string) error { 349 if err := validateCredentialName(args); err != nil { 350 return err 351 } 352 o.Name = args[0] 353 return nil 354 } 355 356 func validateCredentialName(args []string) error { 357 switch len(args) { 358 case 0: 359 return fmt.Errorf("no credential name was specified") 360 case 1: 361 return nil 362 default: 363 return fmt.Errorf("only one positional argument may be specified, the credential name, but multiple were received: %s", args) 364 } 365 } 366 367 func (p *Porter) CredentialsApply(ctx context.Context, o ApplyOptions) error { 368 ctx, span := tracing.StartSpan(ctx) 369 defer span.EndSpan() 370 371 span.Debugf("Reading input file %s...\n", o.File) 372 namespace, err := p.getNamespaceFromFile(o) 373 if err != nil { 374 return span.Error(err) 375 } 376 377 var creds DisplayCredentialSet 378 err = encoding.UnmarshalFile(p.FileSystem, o.File, &creds) 379 if err != nil { 380 return span.Error(fmt.Errorf("could not load %s as a credential set: %w", o.File, err)) 381 } 382 383 if err = creds.Validate(ctx, p.GetSchemaCheckStrategy(ctx)); err != nil { 384 return span.Error(fmt.Errorf("invalid credential set: %w", err)) 385 } 386 387 creds.Namespace = namespace 388 creds.Status.Modified = time.Now() 389 390 err = p.Credentials.Validate(ctx, creds.CredentialSet) 391 if err != nil { 392 return span.Error(fmt.Errorf("credential set is invalid: %w", err)) 393 } 394 395 err = p.Credentials.UpsertCredentialSet(ctx, creds.CredentialSet) 396 if err != nil { 397 return err 398 } 399 400 fmt.Fprintf(p.Out, "Applied %s credential set\n", creds) 401 return nil 402 } 403 404 func (p *Porter) getNamespaceFromFile(o ApplyOptions) (string, error) { 405 // Check if the namespace was set in the file, if not, use the namespace set on the command 406 var raw map[string]interface{} 407 err := encoding.UnmarshalFile(p.FileSystem, o.File, &raw) 408 if err != nil { 409 return "", fmt.Errorf("invalid file '%s': %w", o.File, err) 410 } 411 412 if rawNamespace, ok := raw["namespace"]; ok { 413 if ns, ok := rawNamespace.(string); ok { 414 return ns, nil 415 } else { 416 return "", errors.New("invalid namespace specified in file, must be a string") 417 } 418 } 419 420 return o.Namespace, nil 421 } 422 423 // CredentialCreateOptions represent options for Porter's credential create command 424 type CredentialCreateOptions struct { 425 FileName string 426 OutputType string 427 } 428 429 func (o *CredentialCreateOptions) Validate(args []string) error { 430 if len(args) > 1 { 431 return fmt.Errorf("only one positional argument may be specified, fileName, but multiple were received: %s", args) 432 } 433 434 if len(args) > 0 { 435 o.FileName = args[0] 436 } 437 438 if o.OutputType == "" && o.FileName != "" && strings.Trim(filepath.Ext(o.FileName), ".") == "" { 439 return errors.New("could not detect the file format from the file extension (.txt). Specify the format with --output") 440 } 441 442 return nil 443 } 444 445 func (p *Porter) CreateCredential(ctx context.Context, opts CredentialCreateOptions) error { 446 _, span := tracing.StartSpan(ctx) 447 defer span.EndSpan() 448 449 if opts.OutputType == "" { 450 opts.OutputType = strings.Trim(filepath.Ext(opts.FileName), ".") 451 } 452 453 if opts.FileName == "" { 454 if opts.OutputType == "" { 455 opts.OutputType = "yaml" 456 } 457 458 switch opts.OutputType { 459 case "json": 460 credentialSet, err := p.Templates.GetCredentialSetJSON() 461 if err != nil { 462 return err 463 } 464 465 // Note that we are not using span.Info because this must be printed to stdout 466 fmt.Fprintln(p.Out, string(credentialSet)) 467 468 return nil 469 case "yaml", "yml": 470 credentialSet, err := p.Templates.GetCredentialSetYAML() 471 if err != nil { 472 return err 473 } 474 475 // Note that we are not using span.Info because this must be printed to stdout 476 fmt.Fprintln(p.Out, string(credentialSet)) 477 478 return nil 479 default: 480 return span.Error(newUnsupportedFormatError(opts.OutputType)) 481 } 482 483 } 484 485 span.Info("creating porter credential set in the current directory") 486 487 switch opts.OutputType { 488 case "json": 489 err := p.CopyTemplate(p.Templates.GetCredentialSetJSON, opts.FileName) 490 return span.Error(err) 491 case "yaml", "yml": 492 err := p.CopyTemplate(p.Templates.GetCredentialSetYAML, opts.FileName) 493 return span.Error(err) 494 default: 495 return span.Error(newUnsupportedFormatError(opts.OutputType)) 496 } 497 } 498 499 func newUnsupportedFormatError(format string) error { 500 return fmt.Errorf("unsupported format %s. Supported formats are: yaml and json", format) 501 }