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

     1  package action
     2  
     3  import (
     4  	"encoding/json"
     5  	"errors"
     6  	"fmt"
     7  	"math"
     8  	"strings"
     9  
    10  	"github.com/deislabs/cnab-go/bundle"
    11  	"github.com/deislabs/cnab-go/bundle/definition"
    12  	"github.com/deislabs/cnab-go/claim"
    13  	"github.com/deislabs/cnab-go/credentials"
    14  	"github.com/deislabs/cnab-go/driver"
    15  )
    16  
    17  // stateful is there just to make callers of opFromClaims more readable
    18  const stateful = false
    19  
    20  // Action describes one of the primary actions that can be executed in CNAB.
    21  //
    22  // The actions are:
    23  // - install
    24  // - upgrade
    25  // - uninstall
    26  // - downgrade
    27  // - status
    28  type Action interface {
    29  	// Run an action, and record the status in the given claim
    30  	Run(*claim.Claim, credentials.Set, ...OperationConfigFunc) error
    31  }
    32  
    33  func golangTypeToJSONType(value interface{}) (string, error) {
    34  	switch v := value.(type) {
    35  	case nil:
    36  		return "null", nil
    37  	case bool:
    38  		return "boolean", nil
    39  	case float64:
    40  		// All numeric values are parsed by JSON into float64s. When a value could be an integer, it could also be a number, so give the more specific answer.
    41  		if math.Trunc(v) == v {
    42  			return "integer", nil
    43  		}
    44  		return "number", nil
    45  	case string:
    46  		return "string", nil
    47  	case map[string]interface{}:
    48  		return "object", nil
    49  	case []interface{}:
    50  		return "array", nil
    51  	default:
    52  		return fmt.Sprintf("%T", value), fmt.Errorf("unsupported type: %T", value)
    53  	}
    54  }
    55  
    56  // allowedTypes takes an output Schema and returns a map of the allowed types (to true)
    57  // or an error (if reading the allowed types from the schema failed).
    58  func allowedTypes(outputSchema definition.Schema) (map[string]bool, error) {
    59  	var outputTypes []string
    60  	mapOutputTypes := map[string]bool{}
    61  
    62  	// Get list of one or more allowed types for this output
    63  	outputType, ok, err1 := outputSchema.GetType()
    64  	if !ok { // there are multiple types
    65  		var err2 error
    66  		outputTypes, ok, err2 = outputSchema.GetTypes()
    67  		if !ok {
    68  			return mapOutputTypes, fmt.Errorf("Getting a single type errored with %q and getting multiple types errored with %q", err1, err2)
    69  		}
    70  	} else {
    71  		outputTypes = []string{outputType}
    72  	}
    73  
    74  	// Turn allowed outputs into map for easier membership checking
    75  	for _, v := range outputTypes {
    76  		mapOutputTypes[v] = true
    77  	}
    78  
    79  	// All integers make acceptable numbers, and our helper function provides the most specific type.
    80  	if mapOutputTypes["number"] {
    81  		mapOutputTypes["integer"] = true
    82  	}
    83  
    84  	return mapOutputTypes, nil
    85  }
    86  
    87  // keys takes a map and returns the keys joined into a comma-separate string.
    88  func keys(stringMap map[string]bool) string {
    89  	var keys []string
    90  	for k := range stringMap {
    91  		keys = append(keys, k)
    92  	}
    93  	return strings.Join(keys, ",")
    94  }
    95  
    96  // isTypeOK uses the content and allowedTypes arguments to make sure the content of an output file matches one of the allowed types.
    97  // The other arguments (name and allowedTypesList) are used when assembling error messages.
    98  func isTypeOk(name, content string, allowedTypes map[string]bool) error {
    99  	if !allowedTypes["string"] { // String output types are always passed through as the escape hatch for non-JSON bundle outputs.
   100  		var value interface{}
   101  		if err := json.Unmarshal([]byte(content), &value); err != nil {
   102  			return fmt.Errorf("failed to parse %q: %s", name, err)
   103  		}
   104  
   105  		v, err := golangTypeToJSONType(value)
   106  		if err != nil {
   107  			return fmt.Errorf("%q is not a known JSON type. Expected %q to be one of: %s", name, v, keys(allowedTypes))
   108  		}
   109  		if !allowedTypes[v] {
   110  			return fmt.Errorf("%q is not any of the expected types (%s) because it is %q", name, keys(allowedTypes), v)
   111  		}
   112  	}
   113  	return nil
   114  }
   115  
   116  func setOutputsOnClaim(claim *claim.Claim, outputs map[string]string) error {
   117  	var outputErrors []error
   118  	claim.Outputs = map[string]interface{}{}
   119  
   120  	if claim.Bundle.Outputs == nil {
   121  		return nil
   122  	}
   123  
   124  	for outputName, v := range claim.Bundle.Outputs {
   125  		name := v.Definition
   126  		if name == "" {
   127  			return fmt.Errorf("invalid bundle: no definition set for output %q", outputName)
   128  		}
   129  
   130  		outputSchema := claim.Bundle.Definitions[name]
   131  		if outputSchema == nil {
   132  			return fmt.Errorf("invalid bundle: output %q references definition %q, which was not found", outputName, name)
   133  		}
   134  		outputTypes, err := allowedTypes(*outputSchema)
   135  		if err != nil {
   136  			return err
   137  		}
   138  
   139  		content := outputs[v.Path]
   140  		if content != "" {
   141  			err := isTypeOk(outputName, content, outputTypes)
   142  			if err != nil {
   143  				outputErrors = append(outputErrors, err)
   144  			}
   145  			claim.Outputs[outputName] = outputs[v.Path]
   146  		}
   147  	}
   148  
   149  	if len(outputErrors) > 0 {
   150  		return fmt.Errorf("error: %s", outputErrors)
   151  	}
   152  
   153  	return nil
   154  }
   155  
   156  func selectInvocationImage(d driver.Driver, c *claim.Claim) (bundle.InvocationImage, error) {
   157  	if len(c.Bundle.InvocationImages) == 0 {
   158  		return bundle.InvocationImage{}, errors.New("no invocationImages are defined in the bundle")
   159  	}
   160  
   161  	for _, ii := range c.Bundle.InvocationImages {
   162  		if d.Handles(ii.ImageType) {
   163  			return ii, nil
   164  		}
   165  	}
   166  
   167  	return bundle.InvocationImage{}, errors.New("driver is not compatible with any of the invocation images in the bundle")
   168  }
   169  
   170  func getImageMap(b *bundle.Bundle) ([]byte, error) {
   171  	imgs := b.Images
   172  	if imgs == nil {
   173  		imgs = make(map[string]bundle.Image)
   174  	}
   175  	return json.Marshal(imgs)
   176  }
   177  
   178  func opFromClaim(action string, stateless bool, c *claim.Claim, ii bundle.InvocationImage, creds credentials.Set) (*driver.Operation, error) {
   179  	env, files, err := creds.Expand(c.Bundle, stateless)
   180  	if err != nil {
   181  		return nil, err
   182  	}
   183  
   184  	// Quick verification that no params were passed that are not actual legit params.
   185  	for key := range c.Parameters {
   186  		if _, ok := c.Bundle.Parameters[key]; !ok {
   187  			return nil, fmt.Errorf("undefined parameter %q", key)
   188  		}
   189  	}
   190  
   191  	if err := injectParameters(action, c, env, files); err != nil {
   192  		return nil, err
   193  	}
   194  
   195  	bundleBytes, err := json.Marshal(c.Bundle)
   196  	if err != nil {
   197  		return nil, fmt.Errorf("failed to marshal bundle contents: %s", err)
   198  	}
   199  
   200  	files["/cnab/bundle.json"] = string(bundleBytes)
   201  
   202  	imgMap, err := getImageMap(c.Bundle)
   203  	if err != nil {
   204  		return nil, fmt.Errorf("unable to generate image map: %s", err)
   205  	}
   206  	files["/cnab/app/image-map.json"] = string(imgMap)
   207  
   208  	env["CNAB_INSTALLATION_NAME"] = c.Name
   209  	env["CNAB_ACTION"] = action
   210  	env["CNAB_BUNDLE_NAME"] = c.Bundle.Name
   211  	env["CNAB_BUNDLE_VERSION"] = c.Bundle.Version
   212  
   213  	var outputs []string
   214  	if c.Bundle.Outputs != nil {
   215  		for _, v := range c.Bundle.Outputs {
   216  			outputs = append(outputs, v.Path)
   217  		}
   218  	}
   219  
   220  	return &driver.Operation{
   221  		Action:       action,
   222  		Installation: c.Name,
   223  		Parameters:   c.Parameters,
   224  		Image:        ii,
   225  		Revision:     c.Revision,
   226  		Environment:  env,
   227  		Files:        files,
   228  		Outputs:      outputs,
   229  		Bundle:       c.Bundle,
   230  	}, nil
   231  }
   232  
   233  func injectParameters(action string, c *claim.Claim, env, files map[string]string) error {
   234  	for k, param := range c.Bundle.Parameters {
   235  		rawval, ok := c.Parameters[k]
   236  		if !ok {
   237  			if param.Required && param.AppliesTo(action) {
   238  				return fmt.Errorf("missing required parameter %q for action %q", k, action)
   239  			}
   240  			continue
   241  		}
   242  
   243  		contents, err := json.Marshal(rawval)
   244  		if err != nil {
   245  			return err
   246  		}
   247  
   248  		// In order to preserve the exact string value the user provided
   249  		// we don't marshal string parameters
   250  		value := string(contents)
   251  		if value[0] == '"' {
   252  			value, ok = rawval.(string)
   253  			if !ok {
   254  				return fmt.Errorf("failed to parse parameter %q as string", k)
   255  			}
   256  		}
   257  
   258  		if param.Destination == nil {
   259  			// env is a CNAB_P_
   260  			env[fmt.Sprintf("CNAB_P_%s", strings.ToUpper(k))] = value
   261  			continue
   262  		}
   263  		if param.Destination.Path != "" {
   264  			files[param.Destination.Path] = value
   265  		}
   266  		if param.Destination.EnvironmentVariable != "" {
   267  			env[param.Destination.EnvironmentVariable] = value
   268  		}
   269  	}
   270  	return nil
   271  }