From 6aca7d23b1c627e2601d999f2be85a36130244a2 Mon Sep 17 00:00:00 2001 From: Radu M Date: Tue, 6 Aug 2019 18:52:58 +0300 Subject: [PATCH] Update cnab-go to v0.3.0-beta1 Signed-off-by: Radu M --- Gopkg.lock | 6 +- Gopkg.toml | 2 +- cmd/duffle/install.go | 11 +- cmd/duffle/main.go | 4 +- cmd/duffle/run.go | 2 +- cmd/duffle/uninstall.go | 2 +- cmd/duffle/upgrade.go | 2 +- pkg/builder/builder.go | 28 ++-- pkg/builder/builder_test.go | 31 ++-- pkg/duffle/manifest/manifest.go | 30 ++-- pkg/duffle/manifest/manifest_test.go | 6 +- pkg/duffle/manifest/testdata/duffle.json | 6 +- .../deislabs/cnab-go/action/action.go | 152 ++++++++++++++++-- .../deislabs/cnab-go/action/install.go | 13 +- .../deislabs/cnab-go/action/run_custom.go | 13 +- .../deislabs/cnab-go/action/status.go | 5 +- .../deislabs/cnab-go/action/uninstall.go | 10 +- .../deislabs/cnab-go/action/upgrade.go | 10 +- .../deislabs/cnab-go/bundle/bundle.go | 110 +++++++------ .../deislabs/cnab-go/bundle/credentials.go | 6 +- .../cnab-go/bundle/definition/schema.go | 83 +++++----- .../deislabs/cnab-go/bundle/outputs.go | 14 +- .../deislabs/cnab-go/bundle/parameters.go | 19 +-- .../deislabs/cnab-go/claim/claim.go | 15 +- .../cnab-go/driver/command/command.go | 16 +- .../deislabs/cnab-go/driver/docker/docker.go | 101 +++++++++--- .../deislabs/cnab-go/driver/driver.go | 16 +- .../cnab-go/driver/kubernetes/kubernetes.go | 14 +- 28 files changed, 473 insertions(+), 254 deletions(-) diff --git a/Gopkg.lock b/Gopkg.lock index 0870b4c5..bb951427 100644 --- a/Gopkg.lock +++ b/Gopkg.lock @@ -200,7 +200,7 @@ version = "v1.1.1" [[projects]] - digest = "1:d5a5e2bc2ecdebdb160b61cc7deda7116ff2358955b8c84582be5200c28979dd" + digest = "1:e6b1ef832f13f503f6496d923b9f918d5475b004424ca589558afae541dc0a17" name = "github.com/deislabs/cnab-go" packages = [ "action", @@ -215,8 +215,8 @@ "utils/crud", ] pruneopts = "NUT" - revision = "63abcd2f312747df714357dfab48fda5d6d1f903" - version = "v0.2.1-beta1" + revision = "c992a169ba13fb2f36857dbddfb45e6790400758" + version = "v0.3.0-beta1" [[projects]] digest = "1:7a6852b35eb5bbc184561443762d225116ae630c26a7c4d90546619f1e7d2ad2" diff --git a/Gopkg.toml b/Gopkg.toml index d80d11c6..3e077884 100644 --- a/Gopkg.toml +++ b/Gopkg.toml @@ -57,7 +57,7 @@ [[constraint]] name = "github.com/deislabs/cnab-go" - version = "v0.2.1-beta1" + version = "v0.3.0-beta1" [[override]] name = "github.com/google/go-containerregistry" diff --git a/cmd/duffle/install.go b/cmd/duffle/install.go index cadbdd62..802dfc43 100644 --- a/cmd/duffle/install.go +++ b/cmd/duffle/install.go @@ -142,7 +142,7 @@ func (i *installCmd) run() error { c.Bundle = bun // calculateParamValues determines if values can be changed in later actions, but we don't have // previous values so install passes nil. - c.Parameters, err = calculateParamValues(bun, i.valuesFile, i.setParams, i.setFiles, nil) + c.Parameters, err = calculateParamValues(bun, i.valuesFile, i.setParams, i.setFiles) if err != nil { return err } @@ -229,10 +229,7 @@ func parseValues(file string) (map[string]interface{}, error) { return vals, nil } -func calculateParamValues(bun *bundle.Bundle, valuesFile string, setParams, setFilePaths []string, prevVals map[string]interface{}) (map[string]interface{}, error) { - if prevVals == nil { - prevVals = map[string]interface{}{} - } +func calculateParamValues(bun *bundle.Bundle, valuesFile string, setParams, setFilePaths []string) (map[string]interface{}, error) { vals := map[string]interface{}{} if valuesFile != "" { var err error @@ -258,7 +255,7 @@ func calculateParamValues(bun *bundle.Bundle, valuesFile string, setParams, setF } // Check that this is a known param - if _, ok := bun.Parameters.Fields[parts[0]]; !ok { + if _, ok := bun.Parameters[parts[0]]; !ok { return vals, fmt.Errorf("bundle does not have a parameter named %q", parts[0]) } @@ -272,5 +269,5 @@ func calculateParamValues(bun *bundle.Bundle, valuesFile string, setParams, setF vals[parts[0]] = string(content) } - return bundle.ValuesOrDefaults(vals, prevVals, bun) + return bundle.ValuesOrDefaults(vals, bun) } 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/cmd/duffle/run.go b/cmd/duffle/run.go index fe60e589..745f5d0c 100644 --- a/cmd/duffle/run.go +++ b/cmd/duffle/run.go @@ -68,7 +68,7 @@ Credentials and parameters may be passed to the bundle during a target action. // Override parameters only if some are set. if valuesFile != "" || len(setParams) > 0 { - c.Parameters, err = calculateParamValues(c.Bundle, valuesFile, setParams, setFiles, c.Parameters) + c.Parameters, err = calculateParamValues(c.Bundle, valuesFile, setParams, setFiles) if err != nil { return err } diff --git a/cmd/duffle/uninstall.go b/cmd/duffle/uninstall.go index 6fd58837..71ff6a0f 100644 --- a/cmd/duffle/uninstall.go +++ b/cmd/duffle/uninstall.go @@ -89,7 +89,7 @@ func (un *uninstallCmd) run() error { if claim.Bundle == nil { return errors.New("parameters can only be set if a bundle is provided") } - params, err := calculateParamValues(claim.Bundle, un.valuesFile, un.setParams, []string{}, claim.Parameters) + params, err := calculateParamValues(claim.Bundle, un.valuesFile, un.setParams, []string{}) if err != nil { return err } diff --git a/cmd/duffle/upgrade.go b/cmd/duffle/upgrade.go index 2a241af3..b28027e8 100644 --- a/cmd/duffle/upgrade.go +++ b/cmd/duffle/upgrade.go @@ -109,7 +109,7 @@ func (up *upgradeCmd) run() error { // Override parameters only if some are set. if up.valuesFile != "" || len(up.setParams) > 0 { - claim.Parameters, err = calculateParamValues(claim.Bundle, up.valuesFile, up.setParams, up.setFiles, claim.Parameters) + claim.Parameters, err = calculateParamValues(claim.Bundle, up.valuesFile, up.setParams, up.setFiles) if err != nil { return err } diff --git a/pkg/builder/builder.go b/pkg/builder/builder.go index 86982915..e9935f03 100644 --- a/pkg/builder/builder.go +++ b/pkg/builder/builder.go @@ -67,19 +67,21 @@ func (b *Builder) PrepareBuild(bldr *Builder, mfst *manifest.Manifest, appDir st } bf := &bundle.Bundle{ - Actions: ctx.Manifest.Actions, - Credentials: ctx.Manifest.Credentials, - Custom: ctx.Manifest.Custom, - Definitions: ctx.Manifest.Definitions, - Description: ctx.Manifest.Description, - Images: ctx.Manifest.Images, - Keywords: ctx.Manifest.Keywords, - Maintainers: ctx.Manifest.Maintainers, - Name: ctx.Manifest.Name, - Outputs: ctx.Manifest.Outputs, - Parameters: ctx.Manifest.Parameters, - SchemaVersion: ctx.Manifest.SchemaVersion, - Version: ctx.Manifest.Version, + Actions: ctx.Manifest.Actions, + Credentials: ctx.Manifest.Credentials, + Custom: ctx.Manifest.Custom, + Definitions: ctx.Manifest.Definitions, + Description: ctx.Manifest.Description, + Images: ctx.Manifest.Images, + Keywords: ctx.Manifest.Keywords, + License: ctx.Manifest.License, + Maintainers: ctx.Manifest.Maintainers, + Name: ctx.Manifest.Name, + Outputs: ctx.Manifest.Outputs, + Parameters: ctx.Manifest.Parameters, + RequiredExtensions: ctx.Manifest.RequiredExtensions, + SchemaVersion: ctx.Manifest.SchemaVersion, + Version: ctx.Manifest.Version, } for _, imb := range imageBuilders { diff --git a/pkg/builder/builder_test.go b/pkg/builder/builder_test.go index 3999d05d..1b27aa66 100644 --- a/pkg/builder/builder_test.go +++ b/pkg/builder/builder_test.go @@ -52,12 +52,9 @@ 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": {}}, - } - params := &bundle.ParametersDefinition{ - Fields: map[string]bundle.ParameterDefinition{"param1": {}}, - } + outputs := map[string]bundle.Output{"output1": {}} + params := map[string]bundle.Parameter{"param1": {}} + mfst := &manifest.Manifest{ Actions: map[string]bundle.Action{"act1": {}}, Credentials: map[string]bundle.Credential{"cred1": {}}, @@ -79,11 +76,13 @@ func TestPrepareBuild(t *testing.T) { URL: "https://test.com", }, }, - Name: "foo", - Outputs: outputs, - Parameters: params, - SchemaVersion: "v1.0.0", - Version: "0.1.0", + Name: "foo", + Outputs: outputs, + Parameters: params, + SchemaVersion: "v1.0.0", + Version: "0.1.0", + License: "MIT", + RequiredExtensions: []string{"ext1", "ext2"}, } components := []imagebuilder.ImageBuilder{ @@ -175,6 +174,16 @@ func TestPrepareBuild(t *testing.T) { } checksPerformed++ + if b.License != mfst.License { + t.Errorf("expected licnse %v, got %v", mfst.License, b.License) + } + checksPerformed++ + + if !reflect.DeepEqual(b.RequiredExtensions, mfst.RequiredExtensions) { + t.Errorf("expected credentials to be %+v but was %+v", mfst.RequiredExtensions, b.RequiredExtensions) + } + checksPerformed++ + // Ensure that all the fields have been checked. If the structures need to diverge in the future, this test should be modified. mfstFields := getFields(manifest.Manifest{}) if len(mfstFields) != checksPerformed { diff --git a/pkg/duffle/manifest/manifest.go b/pkg/duffle/manifest/manifest.go index f19fbfab..9eba0ceb 100644 --- a/pkg/duffle/manifest/manifest.go +++ b/pkg/duffle/manifest/manifest.go @@ -12,20 +12,22 @@ import ( // Manifest represents a duffle manifest. type Manifest struct { - Name string `json:"name"` - Version string `json:"version"` - SchemaVersion string `json:"schemaVersion"` - Description string `json:"description,omitempty"` - Keywords []string `json:"keywords,omitempty"` - Maintainers []bundle.Maintainer `json:"maintainers,omitempty"` - InvocationImages map[string]*InvocationImage `json:"invocationImages,omitempty"` - Images map[string]bundle.Image `json:"images,omitempty"` - Actions map[string]bundle.Action `json:"actions,omitempty"` - Parameters *bundle.ParametersDefinition `json:"parameters,omitempty"` - Credentials map[string]bundle.Credential `json:"credentials,omitempty"` - Definitions definition.Definitions `json:"definitions,omitempty"` - Outputs *bundle.OutputsDefinition `json:"outputs,omitempty"` - Custom map[string]interface{} `json:"custom,omitempty"` + Name string `json:"name"` + Version string `json:"version"` + SchemaVersion string `json:"schemaVersion"` + Description string `json:"description,omitempty"` + Keywords []string `json:"keywords,omitempty"` + Maintainers []bundle.Maintainer `json:"maintainers,omitempty"` + InvocationImages map[string]*InvocationImage `json:"invocationImages,omitempty"` + Images map[string]bundle.Image `json:"images,omitempty"` + Actions map[string]bundle.Action `json:"actions,omitempty"` + Parameters map[string]bundle.Parameter `json:"parameters,omitempty"` + Credentials map[string]bundle.Credential `json:"credentials,omitempty"` + Definitions definition.Definitions `json:"definitions,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"` } // InvocationImage represents an invocation image component of a CNAB bundle diff --git a/pkg/duffle/manifest/manifest_test.go b/pkg/duffle/manifest/manifest_test.go index 207da9e7..ebaa3d31 100644 --- a/pkg/duffle/manifest/manifest_test.go +++ b/pkg/duffle/manifest/manifest_test.go @@ -63,11 +63,11 @@ func TestLoad(t *testing.T) { t.Errorf("exp docker but was \"%v\"", img.ImageType) } - if len(m.Parameters.Fields) != 1 { - t.Fatalf("expected 1 parameter but got %d", len(m.Parameters.Fields)) + if len(m.Parameters) != 1 { + t.Fatalf("expected 1 parameter but got %d", len(m.Parameters)) } - _, ok := m.Parameters.Fields["foo"] + _, ok := m.Parameters["foo"] if !ok { t.Errorf("expected a parameter named foo but got %v", m.Parameters) } diff --git a/pkg/duffle/manifest/testdata/duffle.json b/pkg/duffle/manifest/testdata/duffle.json index 64e45c4a..ce9d019f 100644 --- a/pkg/duffle/manifest/testdata/duffle.json +++ b/pkg/duffle/manifest/testdata/duffle.json @@ -23,10 +23,8 @@ } }, "parameters": { - "fields": { - "foo": { - "definition": "foo" - } + "foo": { + "definition": "foo" } }, "credentials": { diff --git a/vendor/github.com/deislabs/cnab-go/action/action.go b/vendor/github.com/deislabs/cnab-go/action/action.go index afcf0573..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") @@ -56,7 +181,7 @@ func getImageMap(b *bundle.Bundle) ([]byte, error) { return json.Marshal(imgs) } -func appliesToAction(action string, parameter bundle.ParameterDefinition) bool { +func appliesToAction(action string, parameter bundle.Parameter) bool { if len(parameter.ApplyTo) == 0 { return true } @@ -76,15 +201,13 @@ func opFromClaim(action string, stateless bool, c *claim.Claim, ii bundle.Invoca // Quick verification that no params were passed that are not actual legit params. for key := range c.Parameters { - if _, ok := c.Bundle.Parameters.Fields[key]; !ok { + if _, ok := c.Bundle.Parameters[key]; !ok { return nil, fmt.Errorf("undefined parameter %q", key) } } - if c.Bundle.Parameters != nil { - if err := injectParameters(action, c, env, files); err != nil { - return nil, err - } + if err := injectParameters(action, c, env, files); err != nil { + return nil, err } imgMap, err := getImageMap(c.Bundle) @@ -98,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, @@ -107,20 +237,16 @@ func opFromClaim(action string, stateless bool, c *claim.Claim, ii bundle.Invoca Revision: c.Revision, Environment: env, Files: files, + Outputs: outputs, Out: w, }, nil } func injectParameters(action string, c *claim.Claim, env, files map[string]string) error { - requiredMap := map[string]struct{}{} - for _, key := range c.Bundle.Parameters.Required { - requiredMap[key] = struct{}{} - } - for k, param := range c.Bundle.Parameters.Fields { + for k, param := range c.Bundle.Parameters { rawval, ok := c.Parameters[k] if !ok { - _, required := requiredMap[k] - if required && appliesToAction(action, param) { + if param.Required && appliesToAction(action, param) { return fmt.Errorf("missing required parameter %q for action %q", k, action) } continue 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 fd7a2595..3c2568a7 100644 --- a/vendor/github.com/deislabs/cnab-go/bundle/bundle.go +++ b/vendor/github.com/deislabs/cnab-go/bundle/bundle.go @@ -16,23 +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 *ParametersDefinition `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"` + 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. @@ -70,33 +72,33 @@ 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"` } -// Map that stores the relocated images +// ImageRelocationMap stores the relocated images // The key is the Image in bundle.json and the value is the new Image // from the relocated registry type ImageRelocationMap map[string]string @@ -106,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. @@ -125,36 +127,24 @@ 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 -// the schema validation, a required parameter is missing or an immutable parameter is set with a new value. -func ValuesOrDefaults(vals map[string]interface{}, currentVals map[string]interface{}, b *Bundle) (map[string]interface{}, error) { +// the schema validation or a required parameter is missing. +func ValuesOrDefaults(vals map[string]interface{}, b *Bundle) (map[string]interface{}, error) { res := map[string]interface{}{} - if b.Parameters == nil { - return res, nil - } - - requiredMap := map[string]struct{}{} - for _, key := range b.Parameters.Required { - requiredMap[key] = struct{}{} - } - - for name, def := range b.Parameters.Fields { - s, ok := b.Definitions[def.Definition] + for name, param := range b.Parameters { + s, ok := b.Definitions[param.Definition] if !ok { return res, fmt.Errorf("unable to find definition for %s", name) } if val, ok := vals[name]; ok { - if currentVal, ok := currentVals[name]; def.Immutable && ok && currentVal != val { - return res, fmt.Errorf("parameter %s is immutable and cannot be overridden with value %v", name, val) - } valErrs, err := s.Validate(val) if err != nil { return res, pkgErrors.Wrapf(err, "encountered an error validating parameter %s", name) @@ -168,7 +158,7 @@ func ValuesOrDefaults(vals map[string]interface{}, currentVals map[string]interf typedVal := s.CoerceValue(val) res[name] = typedVal continue - } else if _, ok := requiredMap[name]; ok { + } else if param.Required { return res, fmt.Errorf("parameter %q is required", name) } res[name] = s.Default @@ -191,6 +181,22 @@ func (b Bundle) Validate() error { return errors.New("'latest' is not a valid bundle version") } + reqExt := make(map[string]bool, len(b.RequiredExtensions)) + for _, requiredExtension := range b.RequiredExtensions { + // Verify the custom extension declared as required exists + if _, exists := b.Custom[requiredExtension]; !exists { + return fmt.Errorf("required extension '%s' is not defined in the Custom section of the bundle", requiredExtension) + } + + // Check for duplicate entries + if _, exists := reqExt[requiredExtension]; exists { + return fmt.Errorf("required extension '%s' is already declared", requiredExtension) + } + + // Populate map with required extension, for duplicate check above + reqExt[requiredExtension] = true + } + for _, img := range b.InvocationImages { err := img.Validate() if err != nil { 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 da959f93..c461c577 100644 --- a/vendor/github.com/deislabs/cnab-go/bundle/parameters.go +++ b/vendor/github.com/deislabs/cnab-go/bundle/parameters.go @@ -1,15 +1,10 @@ package bundle -type ParametersDefinition struct { - Fields map[string]ParameterDefinition `json:"fields" mapstructure:"fields"` - Required []string `json:"required,omitempty" mapstructure:"required,omitempty"` -} - -// ParameterDefinition defines a single parameter for a CNAB bundle -type ParameterDefinition 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"` - Immutable bool `json:"immutable,omitempty" mapstructure:"immutable,omitempty"` +// Parameter defines a single parameter for a CNAB bundle +type Parameter struct { + 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 {