github.com/jdolitsky/cnab-go@v0.7.1-beta1/credentials/credentialset.go (about)

     1  package credentials
     2  
     3  import (
     4  	"fmt"
     5  	"io/ioutil"
     6  	"os"
     7  	"os/exec"
     8  	"strings"
     9  
    10  	"github.com/deislabs/cnab-go/bundle"
    11  
    12  	yaml "gopkg.in/yaml.v2"
    13  )
    14  
    15  // Set is an actual set of resolved credentials.
    16  // This is the output of resolving a credentialset file.
    17  type Set map[string]string
    18  
    19  // Expand expands the set into env vars and paths per the spec in the bundle.
    20  //
    21  // This matches the credentials required by the bundle to the credentials present
    22  // in the credentialset, and then expands them per the definition in the Bundle.
    23  func (s Set) Expand(b *bundle.Bundle, stateless bool) (env, files map[string]string, err error) {
    24  	env, files = map[string]string{}, map[string]string{}
    25  	for name, val := range b.Credentials {
    26  		src, ok := s[name]
    27  		if !ok {
    28  			if stateless || !val.Required {
    29  				continue
    30  			}
    31  			err = fmt.Errorf("credential %q is missing from the user-supplied credentials", name)
    32  			return
    33  		}
    34  		if val.EnvironmentVariable != "" {
    35  			env[val.EnvironmentVariable] = src
    36  		}
    37  		if val.Path != "" {
    38  			files[val.Path] = src
    39  		}
    40  	}
    41  	return
    42  }
    43  
    44  // Merge merges a second Set into the base.
    45  //
    46  // Duplicate credential names are not allow and will result in an
    47  // error, this is the case even if the values are identical.
    48  func (s Set) Merge(s2 Set) error {
    49  	for k, v := range s2 {
    50  		if _, ok := s[k]; ok {
    51  			return fmt.Errorf("ambiguous credential resolution: %q is already present in base credential sets, cannot merge", k)
    52  		}
    53  		s[k] = v
    54  	}
    55  	return nil
    56  }
    57  
    58  // CredentialSet represents a collection of credentials
    59  type CredentialSet struct {
    60  	// Name is the name of the credentialset.
    61  	Name string `json:"name" yaml:"name"`
    62  	// Creadentials is a list of credential specs.
    63  	Credentials []CredentialStrategy `json:"credentials" yaml:"credentials"`
    64  }
    65  
    66  // Load a CredentialSet from a file at a given path.
    67  //
    68  // It does not load the individual credentials.
    69  func Load(path string) (*CredentialSet, error) {
    70  	cset := &CredentialSet{}
    71  	data, err := ioutil.ReadFile(path)
    72  	if err != nil {
    73  		return cset, err
    74  	}
    75  	return cset, yaml.Unmarshal(data, cset)
    76  }
    77  
    78  // Validate compares the given credentials with the spec.
    79  //
    80  // This will result in an error only when the following conditions are true:
    81  // - a credential in the spec is not present in the given set
    82  // - the credential is required
    83  //
    84  // It is allowed for spec to specify both an env var and a file. In such case, if
    85  // the given set provides either, it will be considered valid.
    86  func Validate(given Set, spec map[string]bundle.Credential) error {
    87  	for name, cred := range spec {
    88  		if !isValidCred(given, name) && cred.Required {
    89  			return fmt.Errorf("bundle requires credential for %s", name)
    90  		}
    91  	}
    92  	return nil
    93  }
    94  
    95  func isValidCred(haystack Set, needle string) bool {
    96  	for name := range haystack {
    97  		if name == needle {
    98  			return true
    99  		}
   100  	}
   101  	return false
   102  }
   103  
   104  // Resolve looks up the credentials as described in Source, then copies
   105  // the resulting value into the Value field of each credential strategy.
   106  //
   107  // The typical workflow for working with a credential set is:
   108  //
   109  //	- Load the set
   110  //	- Validate the credentials against a spec
   111  //	- Resolve the credentials
   112  //	- Expand them into bundle values
   113  func (c *CredentialSet) Resolve() (Set, error) {
   114  	l := len(c.Credentials)
   115  	res := make(map[string]string, l)
   116  	for i := 0; i < l; i++ {
   117  		cred := c.Credentials[i]
   118  		src := cred.Source
   119  		// Precedence is Command, Path, EnvVar, Value
   120  		switch {
   121  		case src.Command != "":
   122  			data, err := execCmd(src.Command)
   123  			if err != nil {
   124  				return res, err
   125  			}
   126  			cred.Value = string(data)
   127  		case src.Path != "":
   128  			data, err := ioutil.ReadFile(os.ExpandEnv(src.Path))
   129  			if err != nil {
   130  				return res, fmt.Errorf("credential %q: %s", c.Credentials[i].Name, err)
   131  			}
   132  			cred.Value = string(data)
   133  		case src.EnvVar != "":
   134  			var ok bool
   135  			cred.Value, ok = os.LookupEnv(src.EnvVar)
   136  			if ok {
   137  				break
   138  			}
   139  			fallthrough
   140  		default:
   141  			cred.Value = src.Value
   142  		}
   143  		res[c.Credentials[i].Name] = cred.Value
   144  	}
   145  	return res, nil
   146  }
   147  
   148  func execCmd(cmd string) ([]byte, error) {
   149  	parts := strings.Split(cmd, " ")
   150  	c := parts[0]
   151  	args := parts[1:]
   152  	run := exec.Command(c, args...)
   153  
   154  	return run.CombinedOutput()
   155  }
   156  
   157  // CredentialStrategy represents a source credential and the destination to which it should be sent.
   158  type CredentialStrategy struct {
   159  	// Name is the name of the credential.
   160  	// Name is used to match a credential strategy to a bundle's credential.
   161  	Name string `json:"name" yaml:"name"`
   162  	// Source is the location of the credential.
   163  	// During resolution, the source will be loaded, and the result temporarily placed
   164  	// into Value.
   165  	Source Source `json:"source,omitempty" yaml:"source,omitempty"`
   166  	// Value holds the credential value.
   167  	// When a credential is loaded, it is loaded into this field. In all
   168  	// other cases, it is empty. This field is omitted during serialization.
   169  	Value string `json:"-" yaml:"-"`
   170  }
   171  
   172  // Source represents a strategy for loading a credential from local host.
   173  type Source struct {
   174  	Path    string `json:"path,omitempty" yaml:"path,omitempty"`
   175  	Command string `json:"command,omitempty" yaml:"command,omitempty"`
   176  	Value   string `json:"value,omitempty" yaml:"value,omitempty"`
   177  	EnvVar  string `json:"env,omitempty" yaml:"env,omitempty"`
   178  }
   179  
   180  // Destination reprents a strategy for injecting a credential into an image.
   181  type Destination struct {
   182  	Value string `json:"value,omitempty" yaml:"value,omitempty"`
   183  }