diff --git a/Gopkg.lock b/Gopkg.lock index 57d1467b..bb951427 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -200,7 +200,7 @@ version = "v1.1.1" [[projects]] - digest = "1:0033eca884c6e2a28b9754a881220244942748bb9b0408b2550aecb39297711d" + digest = "1:e6b1ef832f13f503f6496d923b9f918d5475b004424ca589558afae541dc0a17" name = "github.com/deislabs/cnab-go" packages = [ "action", @@ -215,7 +215,8 @@ "utils/crud", ] pruneopts = "NUT" - revision = "b58e5b38096273e9fbbea9691111dda27c59d836" + revision = "c992a169ba13fb2f36857dbddfb45e6790400758" + version = "v0.3.0-beta1" [[projects]] digest = "1:7a6852b35eb5bbc184561443762d225116ae630c26a7c4d90546619f1e7d2ad2" diff --git a/Gopkg.toml b/Gopkg.toml index 1b58c997..3e077884 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -57,9 +57,7 @@ [[constraint]] name = "github.com/deislabs/cnab-go" - # version = "v0.2.1-beta1" - # FIX ME - revision = "b58e5b38096273e9fbbea9691111dda27c59d836" + version = "v0.3.0-beta1" [[override]] name = "github.com/google/go-containerregistry" diff --git a/cmd/duffle/main.go b/cmd/duffle/main.go index ba6f527d..6f4a1344 100644 --- a/cmd/duffle/main.go +++ b/cmd/duffle/main.go @@ -143,7 +143,7 @@ type driverWithRelocationMapping struct { relMapping string } -func (d *driverWithRelocationMapping) Run(op *driver.Operation) error { +func (d *driverWithRelocationMapping) Run(op *driver.Operation) (driver.OperationResult, error) { // if there is a relocation mapping, ensure it is mounted and relocate the invocation image if d.relMapping != "" { op.Files["/cnab/app/relocation-mapping.json"] = d.relMapping @@ -151,7 +151,7 @@ func (d *driverWithRelocationMapping) Run(op *driver.Operation) error { var err error op.Image, err = d.relocateImage(op.Image) if err != nil { - return err + return driver.OperationResult{}, err } } diff --git a/pkg/builder/builder_test.go b/pkg/builder/builder_test.go index eb7f7fed..1b27aa66 100644 --- a/pkg/builder/builder_test.go +++ b/pkg/builder/builder_test.go @@ -52,9 +52,7 @@ func (tc testImage) Build(ctx context.Context, log io.WriteCloser) error { } func TestPrepareBuild(t *testing.T) { - outputs := &bundle.OutputsDefinition{ - Fields: map[string]bundle.OutputDefinition{"output1": {}}, - } + outputs := map[string]bundle.Output{"output1": {}} params := map[string]bundle.Parameter{"param1": {}} mfst := &manifest.Manifest{ diff --git a/pkg/duffle/manifest/manifest.go b/pkg/duffle/manifest/manifest.go index d763dd3e..9eba0ceb 100644 --- a/pkg/duffle/manifest/manifest.go +++ b/pkg/duffle/manifest/manifest.go @@ -24,7 +24,7 @@ type Manifest struct { Parameters map[string]bundle.Parameter `json:"parameters,omitempty"` Credentials map[string]bundle.Credential `json:"credentials,omitempty"` Definitions definition.Definitions `json:"definitions,omitempty"` - Outputs *bundle.OutputsDefinition `json:"outputs,omitempty"` + Outputs map[string]bundle.Output `json:"outputs,omitempty"` Custom map[string]interface{} `json:"custom,omitempty"` License string `json:"license,omitempty"` RequiredExtensions []string `json:"requiredExtensions,omitempty"` diff --git a/vendor/github.com/deislabs/cnab-go/action/action.go b/vendor/github.com/deislabs/cnab-go/action/action.go index 9b9957bf..25da3f34 100644 --- a/vendor/github.com/deislabs/cnab-go/action/action.go +++ b/vendor/github.com/deislabs/cnab-go/action/action.go @@ -5,9 +5,11 @@ import ( "errors" "fmt" "io" + "math" "strings" "github.com/deislabs/cnab-go/bundle" + "github.com/deislabs/cnab-go/bundle/definition" "github.com/deislabs/cnab-go/claim" "github.com/deislabs/cnab-go/credentials" "github.com/deislabs/cnab-go/driver" @@ -29,6 +31,129 @@ type Action interface { Run(*claim.Claim, credentials.Set, io.Writer) error } +func golangTypeToJSONType(value interface{}) (string, error) { + switch v := value.(type) { + case nil: + return "null", nil + case bool: + return "boolean", nil + case float64: + // 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. + if math.Trunc(v) == v { + return "integer", nil + } + return "number", nil + case string: + return "string", nil + case map[string]interface{}: + return "object", nil + case []interface{}: + return "array", nil + default: + return fmt.Sprintf("%T", value), fmt.Errorf("unsupported type: %T", value) + } +} + +// allowedTypes takes an output Schema and returns a map of the allowed types (to true) +// or an error (if reading the allowed types from the schema failed). +func allowedTypes(outputSchema definition.Schema) (map[string]bool, error) { + var outputTypes []string + mapOutputTypes := map[string]bool{} + + // Get list of one or more allowed types for this output + outputType, ok, err1 := outputSchema.GetType() + if !ok { // there are multiple types + var err2 error + outputTypes, ok, err2 = outputSchema.GetTypes() + if !ok { + return mapOutputTypes, fmt.Errorf("Getting a single type errored with %q and getting multiple types errored with %q", err1, err2) + } + } else { + outputTypes = []string{outputType} + } + + // Turn allowed outputs into map for easier membership checking + for _, v := range outputTypes { + mapOutputTypes[v] = true + } + + // All integers make acceptable numbers, and our helper function provides the most specific type. + if mapOutputTypes["number"] { + mapOutputTypes["integer"] = true + } + + return mapOutputTypes, nil +} + +// keys takes a map and returns the keys joined into a comma-separate string. +func keys(stringMap map[string]bool) string { + var keys []string + for k := range stringMap { + keys = append(keys, k) + } + return strings.Join(keys, ",") +} + +// isTypeOK uses the content and allowedTypes arguments to make sure the content of an output file matches one of the allowed types. +// The other arguments (name and allowedTypesList) are used when assembling error messages. +func isTypeOk(name, content string, allowedTypes map[string]bool) error { + if !allowedTypes["string"] { // String output types are always passed through as the escape hatch for non-JSON bundle outputs. + var value interface{} + if err := json.Unmarshal([]byte(content), &value); err != nil { + return fmt.Errorf("failed to parse %q: %s", name, err) + } + + v, err := golangTypeToJSONType(value) + if err != nil { + return fmt.Errorf("%q is not a known JSON type. Expected %q to be one of: %s", name, v, keys(allowedTypes)) + } + if !allowedTypes[v] { + return fmt.Errorf("%q is not any of the expected types (%s) because it is %q", name, keys(allowedTypes), v) + } + } + return nil +} + +func setOutputsOnClaim(claim *claim.Claim, outputs map[string]string) error { + var outputErrors []error + claim.Outputs = map[string]interface{}{} + + if claim.Bundle.Outputs == nil { + return nil + } + + for outputName, v := range claim.Bundle.Outputs { + name := v.Definition + if name == "" { + return fmt.Errorf("invalid bundle: no definition set for output %q", outputName) + } + + outputSchema := claim.Bundle.Definitions[name] + if outputSchema == nil { + return fmt.Errorf("invalid bundle: output %q references definition %q, which was not found", outputName, name) + } + outputTypes, err := allowedTypes(*outputSchema) + if err != nil { + return err + } + + content := outputs[v.Path] + if content != "" { + err := isTypeOk(outputName, content, outputTypes) + if err != nil { + outputErrors = append(outputErrors, err) + } + claim.Outputs[outputName] = outputs[v.Path] + } + } + + if len(outputErrors) > 0 { + return fmt.Errorf("error: %s", outputErrors) + } + + return nil +} + func selectInvocationImage(d driver.Driver, c *claim.Claim) (bundle.InvocationImage, error) { if len(c.Bundle.InvocationImages) == 0 { return bundle.InvocationImage{}, errors.New("no invocationImages are defined in the bundle") @@ -96,6 +221,13 @@ func opFromClaim(action string, stateless bool, c *claim.Claim, ii bundle.Invoca env["CNAB_BUNDLE_NAME"] = c.Bundle.Name env["CNAB_BUNDLE_VERSION"] = c.Bundle.Version + var outputs []string + if c.Bundle.Outputs != nil { + for _, v := range c.Bundle.Outputs { + outputs = append(outputs, v.Path) + } + } + return &driver.Operation{ Action: action, Installation: c.Name, @@ -105,6 +237,7 @@ func opFromClaim(action string, stateless bool, c *claim.Claim, ii bundle.Invoca Revision: c.Revision, Environment: env, Files: files, + Outputs: outputs, Out: w, }, nil } diff --git a/vendor/github.com/deislabs/cnab-go/action/install.go b/vendor/github.com/deislabs/cnab-go/action/install.go index e6c84594..ddee71fe 100644 --- a/vendor/github.com/deislabs/cnab-go/action/install.go +++ b/vendor/github.com/deislabs/cnab-go/action/install.go @@ -24,13 +24,18 @@ func (i *Install) Run(c *claim.Claim, creds credentials.Set, w io.Writer) error if err != nil { return err } - if err := i.Driver.Run(op); err != nil { + + opResult, err := i.Driver.Run(op) + + // update outputs in claim even if there were errors so users can see the output files. + outputErrors := setOutputsOnClaim(c, opResult.Outputs) + + if err != nil { c.Update(claim.ActionInstall, claim.StatusFailure) c.Result.Message = err.Error() return err } - - // Update claim: c.Update(claim.ActionInstall, claim.StatusSuccess) - return nil + + return outputErrors } diff --git a/vendor/github.com/deislabs/cnab-go/action/run_custom.go b/vendor/github.com/deislabs/cnab-go/action/run_custom.go index 3a0d641a..b55f7df0 100644 --- a/vendor/github.com/deislabs/cnab-go/action/run_custom.go +++ b/vendor/github.com/deislabs/cnab-go/action/run_custom.go @@ -48,7 +48,7 @@ func (i *RunCustom) Run(c *claim.Claim, creds credentials.Set, w io.Writer) erro return err } - err = i.Driver.Run(op) + opResult, err := i.Driver.Run(op) // If this action says it does not modify the release, then we don't track // it in the claim. Otherwise, we do. @@ -56,12 +56,15 @@ func (i *RunCustom) Run(c *claim.Claim, creds credentials.Set, w io.Writer) erro return err } - status := claim.StatusSuccess + // update outputs in claim even if there were errors so users can see the output files. + outputErrors := setOutputsOnClaim(c, opResult.Outputs) + if err != nil { + c.Update(i.Action, claim.StatusFailure) c.Result.Message = err.Error() - status = claim.StatusFailure + return err } + c.Update(i.Action, claim.StatusSuccess) - c.Update(i.Action, status) - return err + return outputErrors } diff --git a/vendor/github.com/deislabs/cnab-go/action/status.go b/vendor/github.com/deislabs/cnab-go/action/status.go index 5e839476..ba420cc0 100644 --- a/vendor/github.com/deislabs/cnab-go/action/status.go +++ b/vendor/github.com/deislabs/cnab-go/action/status.go @@ -24,5 +24,8 @@ func (i *Status) Run(c *claim.Claim, creds credentials.Set, w io.Writer) error { if err != nil { return err } - return i.Driver.Run(op) + + // Ignore OperationResult because non-modifying actions don't have outputs to save. + _, err = i.Driver.Run(op) + return err } diff --git a/vendor/github.com/deislabs/cnab-go/action/uninstall.go b/vendor/github.com/deislabs/cnab-go/action/uninstall.go index a78c59e4..db96d690 100644 --- a/vendor/github.com/deislabs/cnab-go/action/uninstall.go +++ b/vendor/github.com/deislabs/cnab-go/action/uninstall.go @@ -24,12 +24,16 @@ func (u *Uninstall) Run(c *claim.Claim, creds credentials.Set, w io.Writer) erro if err != nil { return err } - if err := u.Driver.Run(op); err != nil { + + opResult, err := u.Driver.Run(op) + outputErrors := setOutputsOnClaim(c, opResult.Outputs) + + if err != nil { c.Update(claim.ActionUninstall, claim.StatusFailure) c.Result.Message = err.Error() return err } - c.Update(claim.ActionUninstall, claim.StatusSuccess) - return nil + + return outputErrors } diff --git a/vendor/github.com/deislabs/cnab-go/action/upgrade.go b/vendor/github.com/deislabs/cnab-go/action/upgrade.go index 50bc7568..0655d4d9 100644 --- a/vendor/github.com/deislabs/cnab-go/action/upgrade.go +++ b/vendor/github.com/deislabs/cnab-go/action/upgrade.go @@ -24,12 +24,16 @@ func (u *Upgrade) Run(c *claim.Claim, creds credentials.Set, w io.Writer) error if err != nil { return err } - if err := u.Driver.Run(op); err != nil { + + opResult, err := u.Driver.Run(op) + outputErrors := setOutputsOnClaim(c, opResult.Outputs) + + if err != nil { c.Update(claim.ActionUpgrade, claim.StatusFailure) c.Result.Message = err.Error() return err } - c.Update(claim.ActionUpgrade, claim.StatusSuccess) - return nil + + return outputErrors } diff --git a/vendor/github.com/deislabs/cnab-go/bundle/bundle.go b/vendor/github.com/deislabs/cnab-go/bundle/bundle.go index 189589cf..3c2568a7 100644 --- a/vendor/github.com/deislabs/cnab-go/bundle/bundle.go +++ b/vendor/github.com/deislabs/cnab-go/bundle/bundle.go @@ -16,25 +16,25 @@ import ( // Bundle is a CNAB metadata document type Bundle struct { - SchemaVersion string `json:"schemaVersion" mapstructure:"schemaVersion"` - Name string `json:"name" mapstructure:"name"` - Version string `json:"version" mapstructure:"version"` - Description string `json:"description" mapstructure:"description"` - Keywords []string `json:"keywords,omitempty" mapstructure:"keywords"` - Maintainers []Maintainer `json:"maintainers,omitempty" mapstructure:"maintainers"` - InvocationImages []InvocationImage `json:"invocationImages" mapstructure:"invocationImages"` - Images map[string]Image `json:"images,omitempty" mapstructure:"images"` - Actions map[string]Action `json:"actions,omitempty" mapstructure:"actions"` - Parameters map[string]Parameter `json:"parameters,omitempty" mapstructure:"parameters"` - Credentials map[string]Credential `json:"credentials,omitempty" mapstructure:"credentials"` - Outputs *OutputsDefinition `json:"outputs,omitempty" mapstructure:"outputs"` - Definitions definition.Definitions `json:"definitions,omitempty" mapstructure:"definitions"` - License string `json:"license,omitempty" mapstructure:"license"` - RequiredExtensions []string `json:"requiredExtensions,omitempty" mapstructure:"requiredExtensions"` + SchemaVersion string `json:"schemaVersion" yaml:"schemaVersion"` + Name string `json:"name" yaml:"name"` + Version string `json:"version" yaml:"version"` + Description string `json:"description" yaml:"description"` + Keywords []string `json:"keywords,omitempty" yaml:"keywords,omitempty"` + Maintainers []Maintainer `json:"maintainers,omitempty" yaml:"maintainers,omitempty"` + InvocationImages []InvocationImage `json:"invocationImages" yaml:"invocationImages"` + Images map[string]Image `json:"images,omitempty" yaml:"images,omitempty"` + Actions map[string]Action `json:"actions,omitempty" yaml:"actions,omitempty"` + Parameters map[string]Parameter `json:"parameters,omitempty" yaml:"parameters,omitempty"` + Credentials map[string]Credential `json:"credentials,omitempty" yaml:"credentials,omitempty"` + Outputs map[string]Output `json:"outputs,omitempty" yaml:"outputs,omitempty"` + Definitions definition.Definitions `json:"definitions,omitempty" yaml:"definitions,omitempty"` + License string `json:"license,omitempty" yaml:"license,omitempty"` + RequiredExtensions []string `json:"requiredExtensions,omitempty" yaml:"requiredExtensions,omitempty"` // Custom extension metadata is a named collection of auxiliary data whose // meaning is defined outside of the CNAB specification. - Custom map[string]interface{} `json:"custom,omitempty" mapstructure:"custom"` + Custom map[string]interface{} `json:"custom,omitempty" yaml:"custom,omitempty"` } //Unmarshal unmarshals a Bundle that was not signed. @@ -72,30 +72,30 @@ func (b Bundle) WriteTo(w io.Writer) (int64, error) { // LocationRef specifies a location within the invocation package type LocationRef struct { - Path string `json:"path" mapstructure:"path"` - Field string `json:"field" mapstructure:"field"` - MediaType string `json:"mediaType" mapstructure:"mediaType"` + Path string `json:"path" yaml:"path"` + Field string `json:"field" yaml:"field"` + MediaType string `json:"mediaType" yaml:"mediaType"` } // BaseImage contains fields shared across image types type BaseImage struct { - ImageType string `json:"imageType" mapstructure:"imageType"` - Image string `json:"image" mapstructure:"image"` - Digest string `json:"contentDigest,omitempty" mapstructure:"contentDigest"` - Size uint64 `json:"size,omitempty" mapstructure:"size"` - Labels map[string]string `json:"labels,omitempty" mapstructure:"labels"` - MediaType string `json:"mediaType,omitempty" mapstructure:"mediaType"` + ImageType string `json:"imageType" yaml:"imageType"` + Image string `json:"image" yaml:"image"` + Digest string `json:"contentDigest,omitempty" yaml:"contentDigest"` + Size uint64 `json:"size,omitempty" yaml:"size,omitempty"` + Labels map[string]string `json:"labels,omitempty" yaml:"labels,omitempty"` + MediaType string `json:"mediaType,omitempty" yaml:"mediaType,omitempty"` } // Image describes a container image in the bundle type Image struct { - BaseImage `mapstructure:",squash"` - Description string `json:"description" mapstructure:"description"` //TODO: change? see where it's being used? change to description? + BaseImage `yaml:",inline"` + Description string `json:"description" yaml:"description"` //TODO: change? see where it's being used? change to description? } // InvocationImage contains the image type and location for the installation of a bundle type InvocationImage struct { - BaseImage `mapstructure:",squash"` + BaseImage `yaml:",inline"` } // ImageRelocationMap stores the relocated images @@ -108,18 +108,18 @@ type ImageRelocationMap map[string]string // // A location may be either a file (by path) or an environment variable. type Location struct { - Path string `json:"path,omitempty" mapstructure:"path"` - EnvironmentVariable string `json:"env,omitempty" mapstructure:"env"` + Path string `json:"path,omitempty" yaml:"path,omitempty"` + EnvironmentVariable string `json:"env,omitempty" yaml:"env,omitempty"` } // Maintainer describes a code maintainer of a bundle type Maintainer struct { // Name is a user name or organization name - Name string `json:"name" mapstructure:"name"` + Name string `json:"name" yaml:"name"` // Email is an optional email address to contact the named maintainer - Email string `json:"email,omitempty" mapstructure:"email"` + Email string `json:"email,omitempty" yaml:"email,omitempty"` // Url is an optional URL to an address for the named maintainer - URL string `json:"url,omitempty" mapstructure:"url"` + URL string `json:"url,omitempty" yaml:"url,omitempty"` } // Action describes a custom (non-core) action. @@ -127,11 +127,11 @@ type Action struct { // Modifies indicates whether this action modifies the release. // // If it is possible that an action modify a release, this must be set to true. - Modifies bool `json:"modifies,omitempty" mapstructure:"modifies"` + Modifies bool `json:"modifies,omitempty" yaml:"modifies,omitempty"` // Stateless indicates that the action is purely informational, that credentials are not required, and that the runtime should not keep track of its invocation - Stateless bool `json:"stateless,omitempty" mapstructure:"stateless"` + Stateless bool `json:"stateless,omitempty" yaml:"stateless,omitempty"` // Description describes the action as a user-readable string - Description string `json:"description,omitempty" mapstructure:"description"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` } // ValuesOrDefaults returns parameter values or the default parameter values. An error is returned when the parameter value does not pass diff --git a/vendor/github.com/deislabs/cnab-go/bundle/credentials.go b/vendor/github.com/deislabs/cnab-go/bundle/credentials.go index 3ca2698a..d5c033cb 100644 --- a/vendor/github.com/deislabs/cnab-go/bundle/credentials.go +++ b/vendor/github.com/deislabs/cnab-go/bundle/credentials.go @@ -2,7 +2,7 @@ package bundle // Credential represents the definition of a CNAB credential type Credential struct { - Location `mapstructure:",squash"` - Description string `json:"description,omitempty" mapstructure:"description"` - Required bool `json:"required,omitempty" mapstructure:"required"` + Location `yaml:",inline"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` } diff --git a/vendor/github.com/deislabs/cnab-go/bundle/definition/schema.go b/vendor/github.com/deislabs/cnab-go/bundle/definition/schema.go index 19abbb06..6b3e7dfb 100644 --- a/vendor/github.com/deislabs/cnab-go/bundle/definition/schema.go +++ b/vendor/github.com/deislabs/cnab-go/bundle/definition/schema.go @@ -13,50 +13,51 @@ type Definitions map[string]*Schema // Schema represents a JSON Schema compatible CNAB Definition type Schema struct { - Comment string `json:"$comment,omitempty" mapstructure:"$comment,omitempty"` - ID string `json:"$id,omitempty" mapstructure:"$ref,omitempty"` - Ref string `json:"$ref,omitempty" mapstructure:"$ref,omitempty"` - AdditionalItems interface{} `json:"additionalItems,omitempty" mapstructure:"additionalProperties,omitempty"` - AdditionalProperties interface{} `json:"additionalProperties,omitempty" mapstructure:"additionalProperties,omitempty"` - AllOf []*Schema `json:"allOf,omitempty" mapstructure:"allOf,omitempty"` - Const interface{} `json:"const,omitempty" mapstructure:"const,omitempty"` - Contains *Schema `json:"contains,omitempty" mapstructure:"contains,omitempty"` - ContentEncoding string `json:"contentEncoding,omitempty" mapstructure:"contentEncoding,omitempty"` - ContentMediaType string `json:"contentMediaType,omitempty" mapstructure:"contentMediaType,omitempty"` - Default interface{} `json:"default,omitempty" mapstructure:"default,omitempty"` - Definitions Definitions `json:"definitions,omitempty" mapstructure:"definitions,omitempty"` - Dependencies map[string]interface{} `json:"dependencies,omitempty" mapstructure:"dependencies,omitempty"` - Description string `json:"description,omitempty" mapstructure:"description,omitempty"` - Else *Schema `json:"else,omitempty" mapstructure:"else,omitempty"` - Enum []interface{} `json:"enum,omitempty" mapstructure:"enum,omitempty"` - Examples []interface{} `json:"examples,omitempty" mapstructure:"examples,omitempty"` - ExclusiveMaximum *float64 `json:"exclusiveMaximum,omitempty" mapstructure:"exclusiveMaximum,omitempty"` - ExclusiveMinimum *float64 `json:"exclusiveMinimum,omitempty" mapstructure:"exclusiveMinimum,omitempty"` - Format string `json:"format,omitempty" mapstructure:"format,omitempty"` - If *Schema `json:"if,omitempty" mapstructure:"if,omitempty"` + Schema string `json:"$schema,omitempty" yaml:"$schema,omitempty"` + Comment string `json:"$comment,omitempty" yaml:"$comment,omitempty"` + ID string `json:"$id,omitempty" yaml:"$id,omitempty"` + Ref string `json:"$ref,omitempty" yaml:"$ref,omitempty"` + AdditionalItems interface{} `json:"additionalItems,omitempty" yaml:"additionalItems,omitempty"` + AdditionalProperties interface{} `json:"additionalProperties,omitempty" yaml:"additionalProperties,omitempty"` + AllOf []*Schema `json:"allOf,omitempty" yaml:"allOf,omitempty"` + Const interface{} `json:"const,omitempty" yaml:"const,omitempty"` + Contains *Schema `json:"contains,omitempty" yaml:"contains,omitempty"` + ContentEncoding string `json:"contentEncoding,omitempty" yaml:"contentEncoding,omitempty"` + ContentMediaType string `json:"contentMediaType,omitempty" yaml:"contentMediaType,omitempty"` + Default interface{} `json:"default,omitempty" yaml:"default,omitempty"` + Definitions Definitions `json:"definitions,omitempty" yaml:"definitions,omitempty"` + Dependencies map[string]interface{} `json:"dependencies,omitempty" yaml:"dependencies,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Else *Schema `json:"else,omitempty" yaml:"else,omitempty"` + Enum []interface{} `json:"enum,omitempty" yaml:"enum,omitempty"` + Examples []interface{} `json:"examples,omitempty" yaml:"examples,omitempty"` + ExclusiveMaximum *float64 `json:"exclusiveMaximum,omitempty" yaml:"exclusiveMaximum,omitempty"` + ExclusiveMinimum *float64 `json:"exclusiveMinimum,omitempty" yaml:"exclusiveMinimum,omitempty"` + Format string `json:"format,omitempty" yaml:"format,omitempty"` + If *Schema `json:"if,omitempty" yaml:"if,omitempty"` //Items can be a Schema or an Array of Schema :( - Items interface{} `json:"items,omitempty" mapstructure:"items,omitempty"` - Maximum *float64 `json:"maximum,omitempty" mapstructure:"maximum,omitempty"` - MaxLength *float64 `json:"maxLength,omitempty" mapstructure:"maxLength,omitempty"` - MinItems *float64 `json:"minItems,omitempty" mapstructure:"minItems,omitempty"` - MinLength *float64 `json:"minLength,omitempty" mapstructure:"minLength,omitempty"` - MinProperties *float64 `json:"minProperties,omitempty" mapstructure:"minProperties,omitempty"` - Minimum *float64 `json:"minimum,omitempty" mapstructure:"minimum,omitempty"` - MultipleOf *float64 `json:"multipleOf,omitempty" mapstructure:"multipleOf,omitempty"` - Not *Schema `json:"not,omitempty" mapstructure:"not,omitempty"` - OneOf *Schema `json:"oneOf,omitempty" mapstructure:"oneOf,omitempty"` + Items interface{} `json:"items,omitempty" yaml:"items,omitempty"` + Maximum *float64 `json:"maximum,omitempty" yaml:"maximum,omitempty"` + MaxLength *float64 `json:"maxLength,omitempty" yaml:"maxLength,omitempty"` + MinItems *float64 `json:"minItems,omitempty" yaml:"minItems,omitempty"` + MinLength *float64 `json:"minLength,omitempty" yaml:"minLength,omitempty"` + MinProperties *float64 `json:"minProperties,omitempty" yaml:"minProperties,omitempty"` + Minimum *float64 `json:"minimum,omitempty" yaml:"minimum,omitempty"` + MultipleOf *float64 `json:"multipleOf,omitempty" yaml:"multipleOf,omitempty"` + Not *Schema `json:"not,omitempty" yaml:"not,omitempty"` + OneOf *Schema `json:"oneOf,omitempty" yaml:"oneOf,omitempty"` - PatternProperties map[string]*Schema `json:"patternProperties,omitempty" mapstructure:"patternProperties,omitempty"` + PatternProperties map[string]*Schema `json:"patternProperties,omitempty" yaml:"patternProperties,omitempty"` - Properties map[string]*Schema `json:"properties,omitempty" mapstructure:"properties,omitempty"` - PropertyNames *Schema `json:"propertyNames,omitempty" mapstructure:"propertyNames,omitempty"` - ReadOnly *bool `json:"readOnly,omitempty" mapstructure:"readOnly,omitempty"` - Required []string `json:"required,omitempty" mapstructure:"required,omitempty"` - Then *Schema `json:"then,omitempty" mapstructure:"then,omitempty"` - Title string `json:"title,omitempty" mapstructure:"title,omitempty"` - Type interface{} `json:"type,omitempty" mapstructure:"type,omitempty"` - UniqueItems *bool `json:"uniqueItems,omitempty" mapstructure:"uniqueItems,omitempty"` - WriteOnly *bool `json:"writeOnly,omitempty" mapstructure:"writeOnly,omitempty"` + Properties map[string]*Schema `json:"properties,omitempty" yaml:"properties,omitempty"` + PropertyNames *Schema `json:"propertyNames,omitempty" yaml:"propertyNames,omitempty"` + ReadOnly *bool `json:"readOnly,omitempty" yaml:"readOnly,omitempty"` + Required []string `json:"required,omitempty" yaml:"required,omitempty"` + Then *Schema `json:"then,omitempty" yaml:"then,omitempty"` + Title string `json:"title,omitempty" yaml:"title,omitempty"` + Type interface{} `json:"type,omitempty" yaml:"type,omitempty"` + UniqueItems *bool `json:"uniqueItems,omitempty" yaml:"uniqueItems,omitempty"` + WriteOnly *bool `json:"writeOnly,omitempty" yaml:"writeOnly,omitempty"` } // GetType will return the singular type for a given schema and a success boolean. If the diff --git a/vendor/github.com/deislabs/cnab-go/bundle/outputs.go b/vendor/github.com/deislabs/cnab-go/bundle/outputs.go index ea4a0f0a..a17cfa80 100644 --- a/vendor/github.com/deislabs/cnab-go/bundle/outputs.go +++ b/vendor/github.com/deislabs/cnab-go/bundle/outputs.go @@ -1,12 +1,8 @@ package bundle -type OutputsDefinition struct { - Fields map[string]OutputDefinition `json:"fields" mapstructure:"fields"` -} - -type OutputDefinition struct { - Definition string `json:"definition" mapstructure:"definition"` - ApplyTo []string `json:"applyTo,omitempty" mapstructure:"applyTo,omitempty"` - Description string `json:"description,omitempty" mapstructure:"description"` - Path string `json:"path" mapstructure:"path"` +type Output struct { + Definition string `json:"definition" yaml:"definition"` + ApplyTo []string `json:"applyTo,omitempty" yaml:"applyTo,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Path string `json:"path" yaml:"path"` } diff --git a/vendor/github.com/deislabs/cnab-go/bundle/parameters.go b/vendor/github.com/deislabs/cnab-go/bundle/parameters.go index 2de9fe36..c461c577 100644 --- a/vendor/github.com/deislabs/cnab-go/bundle/parameters.go +++ b/vendor/github.com/deislabs/cnab-go/bundle/parameters.go @@ -2,9 +2,9 @@ package bundle // Parameter defines a single parameter for a CNAB bundle type Parameter struct { - Definition string `json:"definition" mapstructure:"definition"` - ApplyTo []string `json:"applyTo,omitempty" mapstructure:"applyTo,omitempty"` - Description string `json:"description,omitempty" mapstructure:"description"` - Destination *Location `json:"destination,omitemtpty" mapstructure:"destination"` - Required bool `json:"required,omitempty" mapstructure:"required,omitempty"` + Definition string `json:"definition" yaml:"definition"` + ApplyTo []string `json:"applyTo,omitempty" yaml:"applyTo,omitempty"` + Description string `json:"description,omitempty" yaml:"description,omitempty"` + Destination *Location `json:"destination,omitemtpty" yaml:"destination,omitempty"` + Required bool `json:"required,omitempty" yaml:"required,omitempty"` } diff --git a/vendor/github.com/deislabs/cnab-go/claim/claim.go b/vendor/github.com/deislabs/cnab-go/claim/claim.go index 989803b4..57fc709d 100644 --- a/vendor/github.com/deislabs/cnab-go/claim/claim.go +++ b/vendor/github.com/deislabs/cnab-go/claim/claim.go @@ -35,13 +35,14 @@ const ( // provide the necessary data to upgrade, uninstall, and downgrade // a CNAB package. type Claim struct { - Name string `json:"name"` - Revision string `json:"revision"` - Created time.Time `json:"created"` - Modified time.Time `json:"modified"` - Bundle *bundle.Bundle `json:"bundle"` - Result Result `json:"result"` - Parameters map[string]interface{} `json:"parameters"` + Name string `json:"name"` + Revision string `json:"revision"` + Created time.Time `json:"created"` + Modified time.Time `json:"modified"` + Bundle *bundle.Bundle `json:"bundle"` + Result Result `json:"result"` + Parameters map[string]interface{} `json:"parameters"` + // Outputs is a map from the names of outputs (defined in the bundle) to the contents of the files. Outputs map[string]interface{} `json:"outputs"` RelocationMap bundle.ImageRelocationMap `json:"relocationMap"` } diff --git a/vendor/github.com/deislabs/cnab-go/driver/command/command.go b/vendor/github.com/deislabs/cnab-go/driver/command/command.go index 59ef22ba..d2689004 100644 --- a/vendor/github.com/deislabs/cnab-go/driver/command/command.go +++ b/vendor/github.com/deislabs/cnab-go/driver/command/command.go @@ -18,7 +18,7 @@ type Driver struct { } // Run executes the command -func (d *Driver) Run(op *driver.Operation) error { +func (d *Driver) Run(op *driver.Operation) (driver.OperationResult, error) { return d.exec(op) } @@ -42,7 +42,7 @@ func (d *Driver) cliName() string { return "cnab-" + strings.ToLower(d.Name) } -func (d *Driver) exec(op *driver.Operation) error { +func (d *Driver) exec(op *driver.Operation) (driver.OperationResult, error) { // We need to do two things here: We need to make it easier for the // command to access data, and we need to make it easy for the command // to pass that data on to the image it invokes. So we do some data @@ -62,13 +62,13 @@ func (d *Driver) exec(op *driver.Operation) error { data, err := json.Marshal(op) if err != nil { - return err + return driver.OperationResult{}, err } args := []string{} cmd := exec.Command(d.cliName(), args...) cmd.Dir, err = os.Getwd() if err != nil { - return err + return driver.OperationResult{}, err } cmd.Env = pairs cmd.Stdin = bytes.NewBuffer(data) @@ -77,7 +77,7 @@ func (d *Driver) exec(op *driver.Operation) error { stdout, err := cmd.StdoutPipe() if err != nil { - return fmt.Errorf("Setting up output handling for driver (%s) failed: %v", d.Name, err) + return driver.OperationResult{}, fmt.Errorf("Setting up output handling for driver (%s) failed: %v", d.Name, err) } go func() { @@ -88,7 +88,7 @@ func (d *Driver) exec(op *driver.Operation) error { }() stderr, err := cmd.StderrPipe() if err != nil { - return fmt.Errorf("Setting up error output handling for driver (%s) failed: %v", d.Name, err) + return driver.OperationResult{}, fmt.Errorf("Setting up error output handling for driver (%s) failed: %v", d.Name, err) } go func() { @@ -98,8 +98,8 @@ func (d *Driver) exec(op *driver.Operation) error { }() if err = cmd.Start(); err != nil { - return fmt.Errorf("Start of driver (%s) failed: %v", d.Name, err) + return driver.OperationResult{}, fmt.Errorf("Start of driver (%s) failed: %v", d.Name, err) } - return cmd.Wait() + return driver.OperationResult{}, cmd.Wait() } diff --git a/vendor/github.com/deislabs/cnab-go/driver/docker/docker.go b/vendor/github.com/deislabs/cnab-go/driver/docker/docker.go index 83fc3d8b..134d75ea 100644 --- a/vendor/github.com/deislabs/cnab-go/driver/docker/docker.go +++ b/vendor/github.com/deislabs/cnab-go/driver/docker/docker.go @@ -34,7 +34,7 @@ type Driver struct { } // Run executes the Docker driver -func (d *Driver) Run(op *driver.Operation) error { +func (d *Driver) Run(op *driver.Operation) (driver.OperationResult, error) { return d.exec(op) } @@ -54,6 +54,7 @@ func (d *Driver) Config() map[string]string { "VERBOSE": "Increase verbosity. true, false are supported values", "PULL_ALWAYS": "Always pull image, even if locally available (0|1)", "DOCKER_DRIVER_QUIET": "Make the Docker driver quiet (only print container stdout/stderr)", + "OUTPUTS_MOUNT_PATH": "Absolute path to where Docker driver can create temporary directories to bundle outputs. Defaults to temp dir.", } } @@ -124,20 +125,20 @@ func (d *Driver) initializeDockerCli() (command.Cli, error) { return cli, nil } -func (d *Driver) exec(op *driver.Operation) error { +func (d *Driver) exec(op *driver.Operation) (driver.OperationResult, error) { ctx := context.Background() cli, err := d.initializeDockerCli() if err != nil { - return err + return driver.OperationResult{}, err } if d.Simulate { - return nil + return driver.OperationResult{}, nil } if d.config["PULL_ALWAYS"] == "1" { if err := pullImage(ctx, cli, op.Image); err != nil { - return err + return driver.OperationResult{}, err } } var env []string @@ -153,11 +154,10 @@ func (d *Driver) exec(op *driver.Operation) error { AttachStdout: true, } - hostCfg := &container.HostConfig{AutoRemove: true} - + hostCfg := &container.HostConfig{} for _, opt := range d.dockerConfigurationOptions { if err := opt(cfg, hostCfg); err != nil { - return err + return driver.OperationResult{}, err } } @@ -166,18 +166,20 @@ func (d *Driver) exec(op *driver.Operation) error { case client.IsErrNotFound(err): fmt.Fprintf(cli.Err(), "Unable to find image '%s' locally\n", op.Image) if err := pullImage(ctx, cli, op.Image); err != nil { - return err + return driver.OperationResult{}, err } if resp, err = cli.Client().ContainerCreate(ctx, cfg, hostCfg, nil, ""); err != nil { - return fmt.Errorf("cannot create container: %v", err) + return driver.OperationResult{}, fmt.Errorf("cannot create container: %v", err) } case err != nil: - return fmt.Errorf("cannot create container: %v", err) + return driver.OperationResult{}, fmt.Errorf("cannot create container: %v", err) } + defer cli.Client().ContainerRemove(ctx, resp.ID, types.ContainerRemoveOptions{}) + tarContent, err := generateTar(op.Files) if err != nil { - return fmt.Errorf("error staging files: %s", err) + return driver.OperationResult{}, fmt.Errorf("error staging files: %s", err) } options := types.CopyToContainerOptions{ AllowOverwriteDirWithFile: false, @@ -186,7 +188,7 @@ func (d *Driver) exec(op *driver.Operation) error { // path from the given file, starting at the /. err = cli.Client().CopyToContainer(ctx, resp.ID, "/", tarContent, options) if err != nil { - return fmt.Errorf("error copying to / in container: %s", err) + return driver.OperationResult{}, fmt.Errorf("error copying to / in container: %s", err) } attach, err := cli.Client().ContainerAttach(ctx, resp.ID, types.ContainerAttachOptions{ @@ -196,7 +198,7 @@ func (d *Driver) exec(op *driver.Operation) error { Logs: true, }) if err != nil { - return fmt.Errorf("unable to retrieve logs: %v", err) + return driver.OperationResult{}, fmt.Errorf("unable to retrieve logs: %v", err) } var ( stdout io.Writer = os.Stdout @@ -218,25 +220,82 @@ func (d *Driver) exec(op *driver.Operation) error { } }() - statusc, errc := cli.Client().ContainerWait(ctx, resp.ID, container.WaitConditionRemoved) + statusc, errc := cli.Client().ContainerWait(ctx, resp.ID, container.WaitConditionNextExit) if err = cli.Client().ContainerStart(ctx, resp.ID, types.ContainerStartOptions{}); err != nil { - return fmt.Errorf("cannot start container: %v", err) + return driver.OperationResult{}, fmt.Errorf("cannot start container: %v", err) } select { case err := <-errc: if err != nil { - return fmt.Errorf("error in container: %v", err) + opResult, fetchErr := d.fetchOutputs(ctx, resp.ID) + return opResult, containerError("error in container", err, fetchErr) } case s := <-statusc: if s.StatusCode == 0 { - return nil + return d.fetchOutputs(ctx, resp.ID) } if s.Error != nil { - return fmt.Errorf("container exit code: %d, message: %v", s.StatusCode, s.Error.Message) + opResult, fetchErr := d.fetchOutputs(ctx, resp.ID) + return opResult, containerError(fmt.Sprintf("container exit code: %d, message", s.StatusCode), err, fetchErr) + } + opResult, fetchErr := d.fetchOutputs(ctx, resp.ID) + return opResult, containerError(fmt.Sprintf("container exit code: %d, message", s.StatusCode), err, fetchErr) + } + opResult, fetchErr := d.fetchOutputs(ctx, resp.ID) + if fetchErr != nil { + return opResult, fmt.Errorf("fetching outputs failed: %s", fetchErr) + } + return opResult, err +} + +func containerError(containerMessage string, containerErr, fetchErr error) error { + if fetchErr != nil { + return fmt.Errorf("%s: %v. fetching outputs failed: %s", containerMessage, containerErr, fetchErr) + } + + return fmt.Errorf("%s: %v", containerMessage, containerErr) +} + +// fetchOutputs takes a context and a container ID; it copies the /cnab/app/outputs directory from that container. +// The goal is to collect all the files in the directory (recursively) and put them in a flat map of path to contents. +// This map will be inside the OperationResult. When fetchOutputs returns an error, it may also return partial results. +func (d *Driver) fetchOutputs(ctx context.Context, container string) (driver.OperationResult, error) { + opResult := driver.OperationResult{ + Outputs: map[string]string{}, + } + ioReader, _, err := d.dockerCli.Client().CopyFromContainer(ctx, container, "/cnab/app/outputs") + if err != nil { + return opResult, fmt.Errorf("error copying outputs from container: %s", err) + } + + tarReader := tar.NewReader(ioReader) + header, err := tarReader.Next() + + // io.EOF pops us out of loop on successful run. + for err == nil { + // skip directories because we're gathering file contents + if header.FileInfo().IsDir() { + header, err = tarReader.Next() + continue + } + + var contents []byte + // CopyFromContainer strips prefix above outputs directory. + pathInContainer := unix_path.Join("/cnab", "app", header.Name) + + contents, err = ioutil.ReadAll(tarReader) + if err != nil { + return opResult, fmt.Errorf("error while reading %q from outputs tar: %s", pathInContainer, err) } - return fmt.Errorf("container exit code: %d", s.StatusCode) + opResult.Outputs[pathInContainer] = string(contents) + header, err = tarReader.Next() } - return err + + if err != io.EOF { + return opResult, err + } + + return opResult, nil } func generateTar(files map[string]string) (io.Reader, error) { diff --git a/vendor/github.com/deislabs/cnab-go/driver/driver.go b/vendor/github.com/deislabs/cnab-go/driver/driver.go index 3aef2e84..3039b416 100644 --- a/vendor/github.com/deislabs/cnab-go/driver/driver.go +++ b/vendor/github.com/deislabs/cnab-go/driver/driver.go @@ -33,6 +33,8 @@ type Operation struct { Environment map[string]string `json:"environment"` // Files contains files that should be injected into the invocation image. Files map[string]string `json:"files"` + // Outputs is a list of paths starting with `/cnab/app/outputs` that the driver should return the contents of in the OperationResult. + Outputs []string `json:"outputs"` // Output stream for log messages from the driver Out io.Writer `json:"-"` } @@ -44,10 +46,16 @@ type ResolvedCred struct { Value string `json:"value"` } +// OperationResult is the output of the Driver running an Operation. +type OperationResult struct { + // Outputs is a map from the container path of an output file to its contents (i.e. /cnab/app/outputs/...). + Outputs map[string]string +} + // Driver is capable of running a invocation image type Driver interface { // Run executes the operation inside of the invocation image - Run(*Operation) error + Run(*Operation) (OperationResult, error) // Handles receives an ImageType* and answers whether this driver supports that type Handles(string) bool } @@ -69,13 +77,13 @@ type DebugDriver struct { } // Run executes the operation on the Debug driver -func (d *DebugDriver) Run(op *Operation) error { +func (d *DebugDriver) Run(op *Operation) (OperationResult, error) { data, err := json.MarshalIndent(op, "", " ") if err != nil { - return err + return OperationResult{}, err } fmt.Fprintln(op.Out, string(data)) - return nil + return OperationResult{}, nil } // Handles always returns true, effectively claiming to work for any image type diff --git a/vendor/github.com/deislabs/cnab-go/driver/kubernetes/kubernetes.go b/vendor/github.com/deislabs/cnab-go/driver/kubernetes/kubernetes.go index 66a2e5ff..ef18cd6c 100644 --- a/vendor/github.com/deislabs/cnab-go/driver/kubernetes/kubernetes.go +++ b/vendor/github.com/deislabs/cnab-go/driver/kubernetes/kubernetes.go @@ -120,9 +120,9 @@ func (k *Driver) setClient(conf *rest.Config) error { } // Run executes the operation inside of the invocation image. -func (k *Driver) Run(op *driver.Operation) error { +func (k *Driver) Run(op *driver.Operation) (driver.OperationResult, error) { if k.Namespace == "" { - return fmt.Errorf("KUBE_NAMESPACE is required") + return driver.OperationResult{}, fmt.Errorf("KUBE_NAMESPACE is required") } labelMap := generateLabels(op) meta := metav1.ObjectMeta{ @@ -171,7 +171,7 @@ func (k *Driver) Run(op *driver.Operation) error { secret.ObjectMeta.GenerateName += "env-" envsecret, err := k.secrets.Create(secret) if err != nil { - return err + return driver.OperationResult{}, err } if !k.SkipCleanup { defer k.deleteSecret(envsecret.ObjectMeta.Name) @@ -197,7 +197,7 @@ func (k *Driver) Run(op *driver.Operation) error { } secret, err := k.secrets.Create(secret) if err != nil { - return err + return driver.OperationResult{}, err } if !k.SkipCleanup { defer k.deleteSecret(secret.ObjectMeta.Name) @@ -217,7 +217,7 @@ func (k *Driver) Run(op *driver.Operation) error { job.Spec.Template.Spec.Containers = []v1.Container{container} job, err := k.jobs.Create(job) if err != nil { - return err + return driver.OperationResult{}, err } if !k.SkipCleanup { defer k.deleteJob(job.ObjectMeta.Name) @@ -226,14 +226,14 @@ func (k *Driver) Run(op *driver.Operation) error { // Return early for unit testing purposes (the fake k8s client implementation just // hangs during watch because no events are ever created on the Job) if k.skipJobStatusCheck { - return nil + return driver.OperationResult{}, nil } selector := metav1.ListOptions{ LabelSelector: labels.Set(job.ObjectMeta.Labels).String(), } - return k.watchJobStatusAndLogs(selector, op.Out) + return driver.OperationResult{}, k.watchJobStatusAndLogs(selector, op.Out) } func (k *Driver) watchJobStatusAndLogs(selector metav1.ListOptions, out io.Writer) error {