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 }