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  }