diff --git a/cmd/kubernetes/invoke.go b/cmd/kubernetes/invoke.go new file mode 100644 index 000000000..4147642d1 --- /dev/null +++ b/cmd/kubernetes/invoke.go @@ -0,0 +1,16 @@ +package main + +import ( + "github.com/deislabs/porter/pkg/kubernetes" + "github.com/spf13/cobra" +) + +func buildInvokeCommand(mixin *kubernetes.Mixin) *cobra.Command { + return &cobra.Command{ + Use: "invoke", + Short: "Use kubectl to apply manifests to a cluster", + RunE: func(cmd *cobra.Command, args []string) error { + return mixin.Execute() + }, + } +} diff --git a/cmd/kubernetes/main.go b/cmd/kubernetes/main.go index d6fa5f565..29e24bebb 100644 --- a/cmd/kubernetes/main.go +++ b/cmd/kubernetes/main.go @@ -34,8 +34,9 @@ func buildRootCommand(in io.Reader) *cobra.Command { cmd.AddCommand(buildVersionCommand(mixin)) cmd.AddCommand(buildBuildCommand(mixin)) cmd.AddCommand(buildInstallCommand(mixin)) + cmd.AddCommand(buildInvokeCommand(mixin)) cmd.AddCommand(buildUpgradeCommand(mixin)) - cmd.AddCommand(buildUnInstallCommand(mixin)) + cmd.AddCommand(buildUninstallCommand(mixin)) cmd.AddCommand(buildSchemaCommand(mixin)) return cmd } diff --git a/cmd/kubernetes/uninstall.go b/cmd/kubernetes/uninstall.go index 509bb46cd..6055d8a40 100644 --- a/cmd/kubernetes/uninstall.go +++ b/cmd/kubernetes/uninstall.go @@ -5,7 +5,7 @@ import ( "github.com/spf13/cobra" ) -func buildUnInstallCommand(mixin *kubernetes.Mixin) *cobra.Command { +func buildUninstallCommand(mixin *kubernetes.Mixin) *cobra.Command { return &cobra.Command{ Use: "uninstall", Short: "Use kubectl to delete resources contained in a manifest from a cluster", diff --git a/cmd/kubernetes/upgrade.go b/cmd/kubernetes/upgrade.go index 98553ff50..c8d7b3e31 100644 --- a/cmd/kubernetes/upgrade.go +++ b/cmd/kubernetes/upgrade.go @@ -7,10 +7,10 @@ import ( func buildUpgradeCommand(mixin *kubernetes.Mixin) *cobra.Command { return &cobra.Command{ - Use: "Upgrade", + Use: "upgrade", Short: "Use kubectl to apply manifests to a cluster", RunE: func(cmd *cobra.Command, args []string) error { - return mixin.Upgrade() + return mixin.Execute() }, } } diff --git a/pkg/exec/schema/exec.json b/pkg/exec/schema/exec.json index 7d32324a8..e26e5cbe9 100644 --- a/pkg/exec/schema/exec.json +++ b/pkg/exec/schema/exec.json @@ -148,16 +148,18 @@ "$ref": "#/definitions/upgradeStep" } }, - ".*": { + "uninstall": { "type": "array", "items": { - "$ref": "#/definitions/invokeStep" + "$ref": "#/definitions/uninstallStep" } - }, - "uninstall": { + } + }, + "patternProperties": { + ".*": { "type": "array", "items": { - "$ref": "#/definitions/uninstallStep" + "$ref": "#/definitions/invokeStep" } } }, diff --git a/pkg/exec/testdata/schema.json b/pkg/exec/testdata/schema.json index 7d32324a8..e26e5cbe9 100644 --- a/pkg/exec/testdata/schema.json +++ b/pkg/exec/testdata/schema.json @@ -148,16 +148,18 @@ "$ref": "#/definitions/upgradeStep" } }, - ".*": { + "uninstall": { "type": "array", "items": { - "$ref": "#/definitions/invokeStep" + "$ref": "#/definitions/uninstallStep" } - }, - "uninstall": { + } + }, + "patternProperties": { + ".*": { "type": "array", "items": { - "$ref": "#/definitions/uninstallStep" + "$ref": "#/definitions/invokeStep" } } }, diff --git a/pkg/kubernetes/upgrade.go b/pkg/kubernetes/execute.go similarity index 69% rename from pkg/kubernetes/upgrade.go rename to pkg/kubernetes/execute.go index 3ad41abd0..3f66649ce 100644 --- a/pkg/kubernetes/upgrade.go +++ b/pkg/kubernetes/execute.go @@ -10,15 +10,44 @@ import ( yaml "gopkg.in/yaml.v2" ) -type UpgradeAction struct { - Steps []UpgradeStep `yaml:"upgrade"` +type ExecuteAction struct { + Steps []ExecuteStep // using UnmarshalYAML so that we don't need a custom type per action } -type UpgradeStep struct { - UpgradeArguments `yaml:"kubernetes"` +// UnmarshalYAML takes any yaml in this form +// ACTION: +// - kubernetes: ... +// and puts the steps into the Action.Steps field +func (a *ExecuteAction) UnmarshalYAML(unmarshal func(interface{}) error) error { + actionMap := map[interface{}][]interface{}{} + err := unmarshal(&actionMap) + if err != nil { + return errors.Wrap(err, "could not unmarshal yaml into an action map of kubernetes steps") + } + + for _, stepMaps := range actionMap { + b, err := yaml.Marshal(stepMaps) + if err != nil { + return err + } + + var steps []ExecuteStep + err = yaml.Unmarshal(b, &steps) + if err != nil { + return err + } + + a.Steps = append(a.Steps, steps...) + } + + return nil +} + +type ExecuteStep struct { + ExecuteInstruction `yaml:"kubernetes"` } -type UpgradeArguments struct { +type ExecuteInstruction struct { InstallArguments `yaml:",inline"` // Upgrade specific arguments @@ -29,15 +58,15 @@ type UpgradeArguments struct { Timeout *int `yaml:"timeout,omitempty"` } -// Upgrade will reapply manifests using kubectl -func (m *Mixin) Upgrade() error { +// Execute will reapply manifests using kubectl +func (m *Mixin) Execute() error { payload, err := m.getPayloadData() if err != nil { return err } - var action UpgradeAction + var action ExecuteAction err = yaml.Unmarshal(payload, &action) if err != nil { return err @@ -51,7 +80,7 @@ func (m *Mixin) Upgrade() error { var commands []*exec.Cmd for _, manifestPath := range step.Manifests { - commandPayload, err := m.buildUpgradeCommand(step.UpgradeArguments, manifestPath) + commandPayload, err := m.buildExecuteCommand(step.ExecuteInstruction, manifestPath) if err != nil { return err } @@ -79,7 +108,7 @@ func (m *Mixin) Upgrade() error { return err } -func (m *Mixin) buildUpgradeCommand(args UpgradeArguments, manifestPath string) ([]string, error) { +func (m *Mixin) buildExecuteCommand(args ExecuteInstruction, manifestPath string) ([]string, error) { command, err := m.buildInstallCommand(args.InstallArguments, manifestPath) if err != nil { return nil, errors.Wrap(err, "unable to create upgrade command") diff --git a/pkg/kubernetes/upgrade_test.go b/pkg/kubernetes/execute_test.go similarity index 80% rename from pkg/kubernetes/upgrade_test.go rename to pkg/kubernetes/execute_test.go index 002e3b586..937e9fddd 100644 --- a/pkg/kubernetes/upgrade_test.go +++ b/pkg/kubernetes/execute_test.go @@ -12,12 +12,12 @@ import ( yaml "gopkg.in/yaml.v2" ) -type UpgradeTest struct { +type ExecuteTest struct { expectedCommand string - upgradeStep UpgradeStep + executeStep ExecuteStep } -func TestMixin_UpgradeStep(t *testing.T) { +func TestMixin_ExecuteStep(t *testing.T) { manifestDirectory := "/cnab/app/manifests" @@ -40,13 +40,13 @@ func TestMixin_UpgradeStep(t *testing.T) { timeout := 1 - upgradeTests := []UpgradeTest{ + upgradeTests := []ExecuteTest{ // These tests are largely the same as the install, just testing that the embedded // install gets handled correctly { expectedCommand: fmt.Sprintf("%s %s --wait", upgradeCmd, manifestDirectory), - upgradeStep: UpgradeStep{ - UpgradeArguments: UpgradeArguments{ + executeStep: ExecuteStep{ + ExecuteInstruction: ExecuteInstruction{ InstallArguments: InstallArguments{ Step: Step{ Description: "Hello", @@ -58,8 +58,8 @@ func TestMixin_UpgradeStep(t *testing.T) { }, { expectedCommand: fmt.Sprintf("%s %s --wait", upgradeCmd, manifestDirectory), - upgradeStep: UpgradeStep{ - UpgradeArguments: UpgradeArguments{ + executeStep: ExecuteStep{ + ExecuteInstruction: ExecuteInstruction{ InstallArguments: InstallArguments{ Step: Step{ Description: "Hello", @@ -71,8 +71,8 @@ func TestMixin_UpgradeStep(t *testing.T) { }, { expectedCommand: fmt.Sprintf("%s %s", upgradeCmd, manifestDirectory), - upgradeStep: UpgradeStep{ - UpgradeArguments: UpgradeArguments{ + executeStep: ExecuteStep{ + ExecuteInstruction: ExecuteInstruction{ InstallArguments: InstallArguments{ Step: Step{ Description: "Hello", @@ -85,8 +85,8 @@ func TestMixin_UpgradeStep(t *testing.T) { }, { expectedCommand: fmt.Sprintf("%s %s -n %s", upgradeCmd, manifestDirectory, namespace), - upgradeStep: UpgradeStep{ - UpgradeArguments: UpgradeArguments{ + executeStep: ExecuteStep{ + ExecuteInstruction: ExecuteInstruction{ InstallArguments: InstallArguments{ Step: Step{ Description: "Hello", @@ -100,8 +100,8 @@ func TestMixin_UpgradeStep(t *testing.T) { }, { expectedCommand: fmt.Sprintf("%s %s -n %s --validate=false", upgradeCmd, manifestDirectory, namespace), - upgradeStep: UpgradeStep{ - UpgradeArguments: UpgradeArguments{ + executeStep: ExecuteStep{ + ExecuteInstruction: ExecuteInstruction{ InstallArguments: InstallArguments{ Step: Step{ Description: "Hello", @@ -116,8 +116,8 @@ func TestMixin_UpgradeStep(t *testing.T) { }, { expectedCommand: fmt.Sprintf("%s %s -n %s --record=true", upgradeCmd, manifestDirectory, namespace), - upgradeStep: UpgradeStep{ - UpgradeArguments: UpgradeArguments{ + executeStep: ExecuteStep{ + ExecuteInstruction: ExecuteInstruction{ InstallArguments: InstallArguments{ Step: Step{ Description: "Hello", @@ -132,8 +132,8 @@ func TestMixin_UpgradeStep(t *testing.T) { }, { expectedCommand: fmt.Sprintf("%s %s --selector=%s --wait", upgradeCmd, manifestDirectory, selector), - upgradeStep: UpgradeStep{ - UpgradeArguments: UpgradeArguments{ + executeStep: ExecuteStep{ + ExecuteInstruction: ExecuteInstruction{ InstallArguments: InstallArguments{ Step: Step{ Description: "Hello", @@ -148,8 +148,8 @@ func TestMixin_UpgradeStep(t *testing.T) { // These tests exercise the upgrade options { expectedCommand: fmt.Sprintf("%s %s --wait --force --grace-period=0", upgradeCmd, manifestDirectory), - upgradeStep: UpgradeStep{ - UpgradeArguments: UpgradeArguments{ + executeStep: ExecuteStep{ + ExecuteInstruction: ExecuteInstruction{ Force: &forceIt, InstallArguments: InstallArguments{ Step: Step{ @@ -162,8 +162,8 @@ func TestMixin_UpgradeStep(t *testing.T) { }, { expectedCommand: fmt.Sprintf("%s %s --wait --grace-period=%d", upgradeCmd, manifestDirectory, withGrace), - upgradeStep: UpgradeStep{ - UpgradeArguments: UpgradeArguments{ + executeStep: ExecuteStep{ + ExecuteInstruction: ExecuteInstruction{ GracePeriod: &withGrace, InstallArguments: InstallArguments{ Step: Step{ @@ -176,8 +176,8 @@ func TestMixin_UpgradeStep(t *testing.T) { }, { expectedCommand: fmt.Sprintf("%s %s --wait --overwrite=false", upgradeCmd, manifestDirectory), - upgradeStep: UpgradeStep{ - UpgradeArguments: UpgradeArguments{ + executeStep: ExecuteStep{ + ExecuteInstruction: ExecuteInstruction{ Overwrite: &overwriteIt, InstallArguments: InstallArguments{ Step: Step{ @@ -190,8 +190,8 @@ func TestMixin_UpgradeStep(t *testing.T) { }, { expectedCommand: fmt.Sprintf("%s %s --wait --prune=true", upgradeCmd, manifestDirectory), - upgradeStep: UpgradeStep{ - UpgradeArguments: UpgradeArguments{ + executeStep: ExecuteStep{ + ExecuteInstruction: ExecuteInstruction{ Prune: &pruneIt, InstallArguments: InstallArguments{ Step: Step{ @@ -204,8 +204,8 @@ func TestMixin_UpgradeStep(t *testing.T) { }, { expectedCommand: fmt.Sprintf("%s %s --wait --timeout=%ds", upgradeCmd, manifestDirectory, timeout), - upgradeStep: UpgradeStep{ - UpgradeArguments: UpgradeArguments{ + executeStep: ExecuteStep{ + ExecuteInstruction: ExecuteInstruction{ Timeout: &timeout, InstallArguments: InstallArguments{ Step: Step{ @@ -223,13 +223,13 @@ func TestMixin_UpgradeStep(t *testing.T) { t.Run(upgradeTest.expectedCommand, func(t *testing.T) { os.Setenv(test.ExpectedCommandEnv, upgradeTest.expectedCommand) - action := UpgradeAction{Steps: []UpgradeStep{upgradeTest.upgradeStep}} + action := ExecuteAction{Steps: []ExecuteStep{upgradeTest.executeStep}} b, _ := yaml.Marshal(action) h := NewTestMixin(t) h.In = bytes.NewReader(b) - err := h.Upgrade() + err := h.Execute() require.NoError(t, err) }) diff --git a/pkg/kubernetes/schema/kubernetes.json b/pkg/kubernetes/schema/kubernetes.json index 3c6fb3e3a..76d7a0353 100644 --- a/pkg/kubernetes/schema/kubernetes.json +++ b/pkg/kubernetes/schema/kubernetes.json @@ -1,220 +1,295 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "installStep": { - "type": "object", - "properties": { - "kubernetes": { - "type": "object", - "properties": { - "description": { + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "installStep": { + "type": "object", + "properties": { + "kubernetes": { + "type": "object", + "properties": { + "description": { + "type": "string", + "minLength": 1 + }, + "namespace": { + "type": "string" + }, + "manifests": { + "type": "array", + "items": { "type": "string", - "minLength": 1 - }, - "namespace": { - "type": "string" - }, - "manifests": { - "type": "array", - "items": { - "type": "string", - "minItems": 1 - } - }, - "record": { - "type": "boolean", - "default":"false" - }, - "selector": { - "type": "string" - }, - "validate": { - "type": "boolean", - "default":"true" - }, - "wait": { - "type": "boolean", - "default":"true" - }, - "outputs": { - "$ref": "#/definitions/outputs" + "minItems": 1 } }, - "additionalProperties": false, - "required": [ - "description", "manifests" - ] - } - }, - "additionalProperties": false, - "required": [ - "kubernetes" - ] + "record": { + "type": "boolean", + "default":"false" + }, + "selector": { + "type": "string" + }, + "validate": { + "type": "boolean", + "default":"true" + }, + "wait": { + "type": "boolean", + "default":"true" + }, + "outputs": { + "$ref": "#/definitions/outputs" + } + }, + "additionalProperties": false, + "required": [ + "description", "manifests" + ] + } }, - "upgradeStep": { - "type": "object", - "properties": { - "kubernetes": { - "type": "object", - "properties": { - "description": { + "additionalProperties": false, + "required": [ + "kubernetes" + ] + }, + "upgradeStep": { + "type": "object", + "properties": { + "kubernetes": { + "type": "object", + "properties": { + "description": { + "type": "string", + "minLength": 1 + }, + "namespace": { + "type": "string" + }, + "manifests": { + "type": "array", + "items": { "type": "string", - "minLength": 1 - }, - "namespace": { - "type": "string" - }, - "manifests": { - "type": "array", - "items": { - "type": "string", - "minItems": 1 - } - }, - "force": { - "type": ["boolean","null"], - "default":"false" - }, - "gracePeriod" : { - "type": "integer" - }, - "overwrite": { - "type": ["boolean","null"], - "default":"true" - }, - "prune": { - "type": ["boolean","null"], - "default":"true" - }, - "record": { - "type": ["boolean","null"], - "default":"false" - }, - "selector": { - "type": "string" - }, - "timeout" : { - "type": "integer" - }, - "validate": { - "type": ["boolean","null"], - "default":"true" - }, - "wait": { - "type": ["boolean","null"], - "default":"true" - }, - "outputs": { - "$ref": "#/definitions/outputs" + "minItems": 1 } }, - "additionalProperties": false, - "required": [ - "description", "manifests" - ] - } - }, - "additionalProperties": false, - "required": [ - "kubernetes" - ] + "force": { + "type": ["boolean","null"], + "default":"false" + }, + "gracePeriod" : { + "type": "integer" + }, + "overwrite": { + "type": ["boolean","null"], + "default":"true" + }, + "prune": { + "type": ["boolean","null"], + "default":"true" + }, + "record": { + "type": ["boolean","null"], + "default":"false" + }, + "selector": { + "type": "string" + }, + "timeout" : { + "type": "integer" + }, + "validate": { + "type": ["boolean","null"], + "default":"true" + }, + "wait": { + "type": ["boolean","null"], + "default":"true" + }, + "outputs": { + "$ref": "#/definitions/outputs" + } + }, + "additionalProperties": false, + "required": [ + "description", "manifests" + ] + } }, - "uninstallStep": { - "type": "object", - "properties": { - "kubernetes": { - "type": "object", - "properties": { - "description": { + "additionalProperties": false, + "required": [ + "kubernetes" + ] + }, + "invokeStep": { + "type": "object", + "properties": { + "kubernetes": { + "type": "object", + "properties": { + "description": { + "type": "string", + "minLength": 1 + }, + "namespace": { + "type": "string" + }, + "manifests": { + "type": "array", + "items": { "type": "string", - "minLength": 1 - }, - "namespace": { - "type": "string" - }, - "manifests": { - "type": "array", - "items": { - "type": "string", - "minItems": 1 - } - }, - "force": { - "type": ["boolean","null"], - "default":"false" - }, - "gracePeriod" : { - "type": "integer" - }, - "selector": { - "type": "string" - }, - "timeout" : { - "type": "integer" - }, - "wait": { - "type": "boolean", - "default":"true" + "minItems": 1 } }, - "additionalProperties": false, - "required": [ - "description", "manifests" - ] - } - }, - "additionalProperties": false, - "required": [ - "kubernetes" - ] + "force": { + "type": ["boolean","null"], + "default":"false" + }, + "gracePeriod" : { + "type": "integer" + }, + "overwrite": { + "type": ["boolean","null"], + "default":"true" + }, + "prune": { + "type": ["boolean","null"], + "default":"true" + }, + "record": { + "type": ["boolean","null"], + "default":"false" + }, + "selector": { + "type": "string" + }, + "timeout" : { + "type": "integer" + }, + "validate": { + "type": ["boolean","null"], + "default":"true" + }, + "wait": { + "type": ["boolean","null"], + "default":"true" + }, + "outputs": { + "$ref": "#/definitions/outputs" + } + }, + "additionalProperties": false, + "required": [ + "description", "manifests" + ] + } }, - "outputs": { - "type": "array", - "items": { + "additionalProperties": false, + "required": [ + "kubernetes" + ] + }, + "uninstallStep": { + "type": "object", + "properties": { + "kubernetes": { "type": "object", "properties": { - "name": { - "type": "string" + "description": { + "type": "string", + "minLength": 1 }, "namespace": { "type": "string" }, - "resourceType": { - "type": "string" + "manifests": { + "type": "array", + "items": { + "type": "string", + "minItems": 1 + } }, - "resourceName": { - "type": "string" + "force": { + "type": ["boolean","null"], + "default":"false" + }, + "gracePeriod" : { + "type": "integer" }, - "jsonPath": { + "selector": { "type": "string" + }, + "timeout" : { + "type": "integer" + }, + "wait": { + "type": "boolean", + "default":"true" } }, "additionalProperties": false, - "required": ["name", "resourceType", "resourceName", "jsonPath"] + "required": [ + "description", "manifests" + ] } + }, + "additionalProperties": false, + "required": [ + "kubernetes" + ] + }, + "outputs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "resourceType": { + "type": "string" + }, + "resourceName": { + "type": "string" + }, + "jsonPath": { + "type": "string" + } + }, + "additionalProperties": false, + "required": ["name", "resourceType", "resourceName", "jsonPath"] + } + } + }, + "type": "object", + "properties": { + "install": { + "type": "array", + "items": { + "$ref": "#/definitions/installStep" } }, - "type": "object", - "properties": { - "install": { - "type": "array", - "items": { - "$ref": "#/definitions/installStep" - } - }, - "upgrade": { - "type": "array", - "items": { - "$ref": "#/definitions/upgradeStep" - } - }, - "uninstall": { - "type": "array", - "items": { - "$ref": "#/definitions/uninstallStep" - } + "upgrade": { + "type": "array", + "items": { + "$ref": "#/definitions/upgradeStep" } }, - "additionalProperties": false - } - \ No newline at end of file + "uninstall": { + "type": "array", + "items": { + "$ref": "#/definitions/uninstallStep" + } + } + }, + "patternProperties": { + ".*": { + "type": "array", + "items": { + "$ref": "#/definitions/invokeStep" + } + } + }, + "additionalProperties": false +} diff --git a/pkg/kubernetes/schema_test.go b/pkg/kubernetes/schema_test.go index 6a0556dbb..8be553988 100644 --- a/pkg/kubernetes/schema_test.go +++ b/pkg/kubernetes/schema_test.go @@ -37,6 +37,7 @@ func TestMixin_ValidatePayload(t *testing.T) { }{ {"install", "testdata/install-input.yaml", true, ""}, {"upgrade", "testdata/upgrade-input.yaml", true, ""}, + {"invoke", "testdata/invoke-input.yaml", true, ""}, {"uninstall", "testdata/uninstall-input.yaml", true, ""}, {"install-bad-wait-flag", "testdata/install-input-bad-wait-flag.yaml", false, "install.0.kubernetes.wait: Invalid type. Expected: boolean, given: string"}, {"install-no-manifests", "testdata/install-input-no-manifests.yaml", false, "install.0.kubernetes: manifests is required"}, @@ -53,7 +54,7 @@ func TestMixin_ValidatePayload(t *testing.T) { if tc.pass { require.NoError(t, err) } else { - require.EqualError(t, err, tc.error) + require.Contains(t, err.Error(), tc.error) } }) } diff --git a/pkg/kubernetes/testdata/invoke-input.yaml b/pkg/kubernetes/testdata/invoke-input.yaml new file mode 100644 index 000000000..148cec3d4 --- /dev/null +++ b/pkg/kubernetes/testdata/invoke-input.yaml @@ -0,0 +1,6 @@ +poke: + - kubernetes: + description: "Bump hello world app counter" + manifests: + - /cnab/app/manifests/hello + wait: true diff --git a/pkg/kubernetes/testdata/schema.json b/pkg/kubernetes/testdata/schema.json index 3c6fb3e3a..76d7a0353 100644 --- a/pkg/kubernetes/testdata/schema.json +++ b/pkg/kubernetes/testdata/schema.json @@ -1,220 +1,295 @@ { - "$schema": "http://json-schema.org/draft-07/schema#", - "definitions": { - "installStep": { - "type": "object", - "properties": { - "kubernetes": { - "type": "object", - "properties": { - "description": { + "$schema": "http://json-schema.org/draft-07/schema#", + "definitions": { + "installStep": { + "type": "object", + "properties": { + "kubernetes": { + "type": "object", + "properties": { + "description": { + "type": "string", + "minLength": 1 + }, + "namespace": { + "type": "string" + }, + "manifests": { + "type": "array", + "items": { "type": "string", - "minLength": 1 - }, - "namespace": { - "type": "string" - }, - "manifests": { - "type": "array", - "items": { - "type": "string", - "minItems": 1 - } - }, - "record": { - "type": "boolean", - "default":"false" - }, - "selector": { - "type": "string" - }, - "validate": { - "type": "boolean", - "default":"true" - }, - "wait": { - "type": "boolean", - "default":"true" - }, - "outputs": { - "$ref": "#/definitions/outputs" + "minItems": 1 } }, - "additionalProperties": false, - "required": [ - "description", "manifests" - ] - } - }, - "additionalProperties": false, - "required": [ - "kubernetes" - ] + "record": { + "type": "boolean", + "default":"false" + }, + "selector": { + "type": "string" + }, + "validate": { + "type": "boolean", + "default":"true" + }, + "wait": { + "type": "boolean", + "default":"true" + }, + "outputs": { + "$ref": "#/definitions/outputs" + } + }, + "additionalProperties": false, + "required": [ + "description", "manifests" + ] + } }, - "upgradeStep": { - "type": "object", - "properties": { - "kubernetes": { - "type": "object", - "properties": { - "description": { + "additionalProperties": false, + "required": [ + "kubernetes" + ] + }, + "upgradeStep": { + "type": "object", + "properties": { + "kubernetes": { + "type": "object", + "properties": { + "description": { + "type": "string", + "minLength": 1 + }, + "namespace": { + "type": "string" + }, + "manifests": { + "type": "array", + "items": { "type": "string", - "minLength": 1 - }, - "namespace": { - "type": "string" - }, - "manifests": { - "type": "array", - "items": { - "type": "string", - "minItems": 1 - } - }, - "force": { - "type": ["boolean","null"], - "default":"false" - }, - "gracePeriod" : { - "type": "integer" - }, - "overwrite": { - "type": ["boolean","null"], - "default":"true" - }, - "prune": { - "type": ["boolean","null"], - "default":"true" - }, - "record": { - "type": ["boolean","null"], - "default":"false" - }, - "selector": { - "type": "string" - }, - "timeout" : { - "type": "integer" - }, - "validate": { - "type": ["boolean","null"], - "default":"true" - }, - "wait": { - "type": ["boolean","null"], - "default":"true" - }, - "outputs": { - "$ref": "#/definitions/outputs" + "minItems": 1 } }, - "additionalProperties": false, - "required": [ - "description", "manifests" - ] - } - }, - "additionalProperties": false, - "required": [ - "kubernetes" - ] + "force": { + "type": ["boolean","null"], + "default":"false" + }, + "gracePeriod" : { + "type": "integer" + }, + "overwrite": { + "type": ["boolean","null"], + "default":"true" + }, + "prune": { + "type": ["boolean","null"], + "default":"true" + }, + "record": { + "type": ["boolean","null"], + "default":"false" + }, + "selector": { + "type": "string" + }, + "timeout" : { + "type": "integer" + }, + "validate": { + "type": ["boolean","null"], + "default":"true" + }, + "wait": { + "type": ["boolean","null"], + "default":"true" + }, + "outputs": { + "$ref": "#/definitions/outputs" + } + }, + "additionalProperties": false, + "required": [ + "description", "manifests" + ] + } }, - "uninstallStep": { - "type": "object", - "properties": { - "kubernetes": { - "type": "object", - "properties": { - "description": { + "additionalProperties": false, + "required": [ + "kubernetes" + ] + }, + "invokeStep": { + "type": "object", + "properties": { + "kubernetes": { + "type": "object", + "properties": { + "description": { + "type": "string", + "minLength": 1 + }, + "namespace": { + "type": "string" + }, + "manifests": { + "type": "array", + "items": { "type": "string", - "minLength": 1 - }, - "namespace": { - "type": "string" - }, - "manifests": { - "type": "array", - "items": { - "type": "string", - "minItems": 1 - } - }, - "force": { - "type": ["boolean","null"], - "default":"false" - }, - "gracePeriod" : { - "type": "integer" - }, - "selector": { - "type": "string" - }, - "timeout" : { - "type": "integer" - }, - "wait": { - "type": "boolean", - "default":"true" + "minItems": 1 } }, - "additionalProperties": false, - "required": [ - "description", "manifests" - ] - } - }, - "additionalProperties": false, - "required": [ - "kubernetes" - ] + "force": { + "type": ["boolean","null"], + "default":"false" + }, + "gracePeriod" : { + "type": "integer" + }, + "overwrite": { + "type": ["boolean","null"], + "default":"true" + }, + "prune": { + "type": ["boolean","null"], + "default":"true" + }, + "record": { + "type": ["boolean","null"], + "default":"false" + }, + "selector": { + "type": "string" + }, + "timeout" : { + "type": "integer" + }, + "validate": { + "type": ["boolean","null"], + "default":"true" + }, + "wait": { + "type": ["boolean","null"], + "default":"true" + }, + "outputs": { + "$ref": "#/definitions/outputs" + } + }, + "additionalProperties": false, + "required": [ + "description", "manifests" + ] + } }, - "outputs": { - "type": "array", - "items": { + "additionalProperties": false, + "required": [ + "kubernetes" + ] + }, + "uninstallStep": { + "type": "object", + "properties": { + "kubernetes": { "type": "object", "properties": { - "name": { - "type": "string" + "description": { + "type": "string", + "minLength": 1 }, "namespace": { "type": "string" }, - "resourceType": { - "type": "string" + "manifests": { + "type": "array", + "items": { + "type": "string", + "minItems": 1 + } }, - "resourceName": { - "type": "string" + "force": { + "type": ["boolean","null"], + "default":"false" + }, + "gracePeriod" : { + "type": "integer" }, - "jsonPath": { + "selector": { "type": "string" + }, + "timeout" : { + "type": "integer" + }, + "wait": { + "type": "boolean", + "default":"true" } }, "additionalProperties": false, - "required": ["name", "resourceType", "resourceName", "jsonPath"] + "required": [ + "description", "manifests" + ] } + }, + "additionalProperties": false, + "required": [ + "kubernetes" + ] + }, + "outputs": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "namespace": { + "type": "string" + }, + "resourceType": { + "type": "string" + }, + "resourceName": { + "type": "string" + }, + "jsonPath": { + "type": "string" + } + }, + "additionalProperties": false, + "required": ["name", "resourceType", "resourceName", "jsonPath"] + } + } + }, + "type": "object", + "properties": { + "install": { + "type": "array", + "items": { + "$ref": "#/definitions/installStep" } }, - "type": "object", - "properties": { - "install": { - "type": "array", - "items": { - "$ref": "#/definitions/installStep" - } - }, - "upgrade": { - "type": "array", - "items": { - "$ref": "#/definitions/upgradeStep" - } - }, - "uninstall": { - "type": "array", - "items": { - "$ref": "#/definitions/uninstallStep" - } + "upgrade": { + "type": "array", + "items": { + "$ref": "#/definitions/upgradeStep" } }, - "additionalProperties": false - } - \ No newline at end of file + "uninstall": { + "type": "array", + "items": { + "$ref": "#/definitions/uninstallStep" + } + } + }, + "patternProperties": { + ".*": { + "type": "array", + "items": { + "$ref": "#/definitions/invokeStep" + } + } + }, + "additionalProperties": false +} diff --git a/pkg/porter/schema.go b/pkg/porter/schema.go index 6e57a9633..30ee7f5d5 100644 --- a/pkg/porter/schema.go +++ b/pkg/porter/schema.go @@ -43,6 +43,11 @@ func (p *Porter) GetManifestSchema() (jsonSchema, error) { return nil, errors.Errorf("root porter manifest schema has invalid properties type, expected map[string]interface{} but got %T", manifestSchema["properties"]) } + patternPropertiesSchema, ok := manifestSchema["patternProperties"].(jsonSchema)[".*"].(jsonSchema) + if !ok { + return nil, errors.Errorf("root porter manifest schema has invalid patternProperties type, expected map[string]interface{} but got %T", manifestSchema["patternProperties"]) + } + mixinSchema, ok := propertiesSchema["mixins"].(jsonSchema) if !ok { return nil, errors.Errorf("root porter manifest schema has invalid properties.mixins type, expected map[string]interface{} but got %T", propertiesSchema["mixins"]) @@ -58,9 +63,9 @@ func (p *Porter) GetManifestSchema() (jsonSchema, error) { return nil, errors.Errorf("root porter manifest schema has invalid properties.mixins.items.enum type, expected []interface{} but got %T", mixinItemSchema["enum"]) } - supportedActions := []string{"install", "upgrade", "uninstall", ".*"} // custom actions are defined in json schema as a wildcard .* - actionSchemas := make(map[string]jsonSchema, len(supportedActions)) - for _, action := range supportedActions { + coreActions := []string{"install", "upgrade", "uninstall"} // custom actions are defined in json schema as a wildcard .* under patternProperties + actionSchemas := make(map[string]jsonSchema, len(coreActions)+1) + for _, action := range coreActions { actionSchema, ok := propertiesSchema[string(action)].(jsonSchema) if !ok { return nil, errors.Errorf("root porter manifest schema has invalid properties.%s type, expected map[string]interface{} but got %T", action, propertiesSchema[string(action)]) @@ -97,19 +102,7 @@ func (p *Porter) GetManifestSchema() (jsonSchema, error) { // embed the entire mixin schema in the root manifestSchema["mixin."+mixin.Name] = mixinSchemaMap - for _, action := range supportedActions { - // Some mixins don't have custom actions defined in their schema - if action == ".*" { - mixinProperties, ok := mixinSchemaMap["properties"].(jsonSchema) - if !ok { - return nil, errors.Errorf("%s mixin schema has invalid properties type, expected map[string]interface{} but got %T", mixin.Name, mixinSchemaMap["properties"]) - } - - if _, hasCustomActions := mixinProperties[".*"]; !hasCustomActions { - continue - } - } - + for _, action := range coreActions { actionItemSchema, ok := actionSchemas[string(action)]["items"].(jsonSchema) if err != nil { return nil, errors.Errorf("root porter manifest schema has invalid properties.%s.items type, expected map[string]interface{} but got %T", action, actionSchemas[string(action)]["items"]) @@ -120,17 +113,32 @@ func (p *Porter) GetManifestSchema() (jsonSchema, error) { return nil, errors.Errorf("root porter manifest schema has invalid properties.%s.items.anyOf type, expected []interface{} but got %T", action, actionItemSchema["anyOf"]) } - stepPrefix := action - if action == ".*" { - stepPrefix = "invoke" - } - actionRef := fmt.Sprintf("#/mixin.%s/definitions/%sStep", mixin.Name, stepPrefix) + actionRef := fmt.Sprintf("#/mixin.%s/definitions/%sStep", mixin.Name, action) // WORKAROUND bug in the RedHat yaml lib used by VS Code, it doesn't handle more than one ref dereference // actionRef := fmt.Sprintf("#/mixin.%s/properties/%s/items", mixin.Name, action) actionAnyOfSchema = append(actionAnyOfSchema, jsonObject{"$ref": actionRef}) actionItemSchema["anyOf"] = actionAnyOfSchema } + + // TODO: Do a better merge in case the mixin has a more limited pattern than .* + _, hasCustomActions := mixinSchemaMap["patternProperties"] + if hasCustomActions{ + actionRef := fmt.Sprintf("#/mixin.%s/definitions/invokeStep", mixin.Name) + + actionItemSchema, ok := patternPropertiesSchema["items"].(jsonSchema) + if err != nil { + return nil, errors.Errorf("root porter manifest schema has invalid patternProperties.items type, expected map[string]interface{} but got %T", patternPropertiesSchema["items"]) + } + + actionAnyOfSchema, ok := actionItemSchema["anyOf"].([]interface{}) + if !ok { + return nil, errors.Errorf("root porter manifest schema has invalid patternProperties.items.anyOf type, expected []interface{} but got %T", actionItemSchema["anyOf"]) + } + + actionAnyOfSchema = append(actionAnyOfSchema, jsonObject{"$ref": actionRef}) + actionItemSchema["anyOf"] = actionAnyOfSchema + } } // Save the updated arrays into the json schema document diff --git a/pkg/porter/templates/schema.json b/pkg/porter/templates/schema.json index 9711f87de..08a1f92de 100644 --- a/pkg/porter/templates/schema.json +++ b/pkg/porter/templates/schema.json @@ -121,12 +121,6 @@ } }, "properties": { - ".*": { - "type": "array", - "items": { - "anyOf": [] - } - }, "credentials": { "additionalProperties": { "$ref": "#/definitions/credential" @@ -205,6 +199,14 @@ "description": "The relative path to a Dockerfile to use as a template during porter build" } }, + "patternProperties": { + ".*": { + "type": "array", + "items": { + "anyOf": [] + } + } + }, "additionalProperties": false, "required": ["name","version", "invocationImage", "mixins"] } diff --git a/pkg/porter/testdata/schema.json b/pkg/porter/testdata/schema.json index acbaed6f4..7b755443a 100644 --- a/pkg/porter/testdata/schema.json +++ b/pkg/porter/testdata/schema.json @@ -259,13 +259,15 @@ "type": "object" } }, - "properties": { + "patternProperties": { ".*": { "items": { "$ref": "#/mixin.exec/definitions/invokeStep" }, "type": "array" - }, + } + }, + "properties": { "install": { "items": { "$ref": "#/mixin.exec/definitions/installStep" @@ -287,7 +289,7 @@ }, "type": "object" }, - "properties": { + "patternProperties": { ".*": { "items": { "anyOf": [ @@ -297,7 +299,9 @@ ] }, "type": "array" - }, + } + }, + "properties": { "credentials": { "additionalProperties": { "$ref": "#/definitions/credential"