diff --git a/crds/workspace.devfile.io_devworkspaces.v1beta1.yaml b/crds/workspace.devfile.io_devworkspaces.v1beta1.yaml index bc25b4d9f..5eafae52c 100644 --- a/crds/workspace.devfile.io_devworkspaces.v1beta1.yaml +++ b/crds/workspace.devfile.io_devworkspaces.v1beta1.yaml @@ -4140,6 +4140,10 @@ spec: description: Structure of the devworkspace. This is also the specification of a devworkspace template. properties: + attributes: + description: Map of implementation-dependant free-form YAML attributes. + type: object + x-kubernetes-preserve-unknown-fields: true commands: description: Predefined, ready-to-use, devworkspace-related commands items: @@ -5561,6 +5565,12 @@ spec: - required: - kubernetes properties: + attributes: + description: Overrides of attributes encapsulated in a parent + devfile. Overriding is done according to K8S strategic merge + patch standard rules. + type: object + x-kubernetes-preserve-unknown-fields: true commands: description: Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S @@ -7148,6 +7158,13 @@ spec: uri: description: Uri of a Devfile yaml file type: string + variables: + additionalProperties: + type: string + description: Overrides of variables encapsulated in a parent + devfile. Overriding is done according to K8S strategic merge + patch standard rules. + type: object type: object projects: description: Projects worked on in the devworkspace, containing @@ -7402,6 +7419,18 @@ spec: - name type: object type: array + variables: + additionalProperties: + type: string + description: "Map of key-value variables used for string replacement + in the devfile. Values can can be referenced via {{variable-key}} + to replace the corresponding value in string fields in the devfile. + Replacement cannot be used for \n - schemaVersion, metadata, + parent source - element identifiers, e.g. command id, component + name, endpoint name, project name - references to identifiers, + e.g. in events, a command's component, container's volume mount + name - string enums, e.g. command group kind, endpoint exposure" + type: object type: object required: - started diff --git a/crds/workspace.devfile.io_devworkspaces.yaml b/crds/workspace.devfile.io_devworkspaces.yaml index af4af7b32..439456298 100644 --- a/crds/workspace.devfile.io_devworkspaces.yaml +++ b/crds/workspace.devfile.io_devworkspaces.yaml @@ -4138,6 +4138,10 @@ spec: description: Structure of the devworkspace. This is also the specification of a devworkspace template. properties: + attributes: + description: Map of implementation-dependant free-form YAML attributes. + type: object + x-kubernetes-preserve-unknown-fields: true commands: description: Predefined, ready-to-use, devworkspace-related commands items: @@ -5566,6 +5570,12 @@ spec: - required: - kubernetes properties: + attributes: + description: Overrides of attributes encapsulated in a parent + devfile. Overriding is done according to K8S strategic merge + patch standard rules. + type: object + x-kubernetes-preserve-unknown-fields: true commands: description: Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S @@ -7153,6 +7163,13 @@ spec: uri: description: Uri of a Devfile yaml file type: string + variables: + additionalProperties: + type: string + description: Overrides of variables encapsulated in a parent + devfile. Overriding is done according to K8S strategic merge + patch standard rules. + type: object type: object projects: description: Projects worked on in the devworkspace, containing @@ -7407,6 +7424,18 @@ spec: - name type: object type: array + variables: + additionalProperties: + type: string + description: "Map of key-value variables used for string replacement + in the devfile. Values can can be referenced via {{variable-key}} + to replace the corresponding value in string fields in the devfile. + Replacement cannot be used for \n - schemaVersion, metadata, + parent source - element identifiers, e.g. command id, component + name, endpoint name, project name - references to identifiers, + e.g. in events, a command's component, container's volume mount + name - string enums, e.g. command group kind, endpoint exposure" + type: object type: object required: - started diff --git a/crds/workspace.devfile.io_devworkspacetemplates.v1beta1.yaml b/crds/workspace.devfile.io_devworkspacetemplates.v1beta1.yaml index 6acf50752..fda3dc322 100644 --- a/crds/workspace.devfile.io_devworkspacetemplates.v1beta1.yaml +++ b/crds/workspace.devfile.io_devworkspacetemplates.v1beta1.yaml @@ -3914,6 +3914,10 @@ spec: description: Structure of the devworkspace. This is also the specification of a devworkspace template. properties: + attributes: + description: Map of implementation-dependant free-form YAML attributes. + type: object + x-kubernetes-preserve-unknown-fields: true commands: description: Predefined, ready-to-use, devworkspace-related commands items: @@ -5284,6 +5288,12 @@ spec: - required: - kubernetes properties: + attributes: + description: Overrides of attributes encapsulated in a parent + devfile. Overriding is done according to K8S strategic merge + patch standard rules. + type: object + x-kubernetes-preserve-unknown-fields: true commands: description: Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge @@ -6813,6 +6823,13 @@ spec: uri: description: Uri of a Devfile yaml file type: string + variables: + additionalProperties: + type: string + description: Overrides of variables encapsulated in a parent devfile. + Overriding is done according to K8S strategic merge patch standard + rules. + type: object type: object projects: description: Projects worked on in the devworkspace, containing names @@ -7053,6 +7070,18 @@ spec: - name type: object type: array + variables: + additionalProperties: + type: string + description: "Map of key-value variables used for string replacement + in the devfile. Values can can be referenced via {{variable-key}} + to replace the corresponding value in string fields in the devfile. + Replacement cannot be used for \n - schemaVersion, metadata, parent + source - element identifiers, e.g. command id, component name, + endpoint name, project name - references to identifiers, e.g. in + events, a command's component, container's volume mount name - + string enums, e.g. command group kind, endpoint exposure" + type: object type: object type: object served: true diff --git a/crds/workspace.devfile.io_devworkspacetemplates.yaml b/crds/workspace.devfile.io_devworkspacetemplates.yaml index 18cca9c1f..1645b72cc 100644 --- a/crds/workspace.devfile.io_devworkspacetemplates.yaml +++ b/crds/workspace.devfile.io_devworkspacetemplates.yaml @@ -3912,6 +3912,10 @@ spec: description: Structure of the devworkspace. This is also the specification of a devworkspace template. properties: + attributes: + description: Map of implementation-dependant free-form YAML attributes. + type: object + x-kubernetes-preserve-unknown-fields: true commands: description: Predefined, ready-to-use, devworkspace-related commands items: @@ -5289,6 +5293,12 @@ spec: - required: - kubernetes properties: + attributes: + description: Overrides of attributes encapsulated in a parent + devfile. Overriding is done according to K8S strategic merge + patch standard rules. + type: object + x-kubernetes-preserve-unknown-fields: true commands: description: Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge @@ -6818,6 +6828,13 @@ spec: uri: description: Uri of a Devfile yaml file type: string + variables: + additionalProperties: + type: string + description: Overrides of variables encapsulated in a parent devfile. + Overriding is done according to K8S strategic merge patch standard + rules. + type: object type: object projects: description: Projects worked on in the devworkspace, containing names @@ -7058,6 +7075,18 @@ spec: - name type: object type: array + variables: + additionalProperties: + type: string + description: "Map of key-value variables used for string replacement + in the devfile. Values can can be referenced via {{variable-key}} + to replace the corresponding value in string fields in the devfile. + Replacement cannot be used for \n - schemaVersion, metadata, parent + source - element identifiers, e.g. command id, component name, + endpoint name, project name - references to identifiers, e.g. in + events, a command's component, container's volume mount name - + string enums, e.g. command group kind, endpoint exposure" + type: object type: object type: object served: true diff --git a/pkg/apis/workspaces/v1alpha2/devworkspacetemplate_spec.go b/pkg/apis/workspaces/v1alpha2/devworkspacetemplate_spec.go index 6511bcfa0..f6afd03b2 100644 --- a/pkg/apis/workspaces/v1alpha2/devworkspacetemplate_spec.go +++ b/pkg/apis/workspaces/v1alpha2/devworkspacetemplate_spec.go @@ -1,5 +1,7 @@ package v1alpha2 +import attributes "github.com/devfile/api/v2/pkg/attributes" + // Structure of the devworkspace. This is also the specification of a devworkspace template. // +devfile:jsonschema:generate type DevWorkspaceTemplateSpec struct { @@ -12,6 +14,24 @@ type DevWorkspaceTemplateSpec struct { // +devfile:overrides:generate type DevWorkspaceTemplateSpecContent struct { + // Map of key-value variables used for string replacement in the devfile. Values can can be referenced via {{variable-key}} + // to replace the corresponding value in string fields in the devfile. Replacement cannot be used for + // + // - schemaVersion, metadata, parent source + // - element identifiers, e.g. command id, component name, endpoint name, project name + // - references to identifiers, e.g. in events, a command's component, container's volume mount name + // - string enums, e.g. command group kind, endpoint exposure + // +optional + // +patchStrategy=merge + // +devfile:overrides:include:omitInPlugin=true,description=Overrides of variables encapsulated in a parent devfile. + Variables map[string]string `json:"variables,omitempty" patchStrategy:"merge"` + + // Map of implementation-dependant free-form YAML attributes. + // +optional + // +patchStrategy=merge + // +devfile:overrides:include:omitInPlugin=true,description=Overrides of attributes encapsulated in a parent devfile. + Attributes attributes.Attributes `json:"attributes,omitempty" patchStrategy:"merge"` + // List of the devworkspace components, such as editor and plugins, // user-provided containers, or other types of components // +optional diff --git a/pkg/apis/workspaces/v1alpha2/zz_generated.deepcopy.go b/pkg/apis/workspaces/v1alpha2/zz_generated.deepcopy.go index 0569781f4..612e62bb3 100644 --- a/pkg/apis/workspaces/v1alpha2/zz_generated.deepcopy.go +++ b/pkg/apis/workspaces/v1alpha2/zz_generated.deepcopy.go @@ -1416,6 +1416,20 @@ func (in *DevWorkspaceTemplateSpec) DeepCopy() *DevWorkspaceTemplateSpec { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *DevWorkspaceTemplateSpecContent) DeepCopyInto(out *DevWorkspaceTemplateSpecContent) { *out = *in + if in.Variables != nil { + in, out := &in.Variables, &out.Variables + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Attributes != nil { + in, out := &in.Attributes, &out.Attributes + *out = make(attributes.Attributes, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } if in.Components != nil { in, out := &in.Components, &out.Components *out = make([]Component, len(*in)) @@ -2355,6 +2369,20 @@ func (in *Parent) DeepCopy() *Parent { func (in *ParentOverrides) DeepCopyInto(out *ParentOverrides) { *out = *in out.OverridesBase = in.OverridesBase + if in.Variables != nil { + in, out := &in.Variables, &out.Variables + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Attributes != nil { + in, out := &in.Attributes, &out.Attributes + *out = make(attributes.Attributes, len(*in)) + for key, val := range *in { + (*out)[key] = *val.DeepCopy() + } + } if in.Components != nil { in, out := &in.Components, &out.Components *out = make([]ComponentParentOverride, len(*in)) diff --git a/pkg/apis/workspaces/v1alpha2/zz_generated.parent_overrides.go b/pkg/apis/workspaces/v1alpha2/zz_generated.parent_overrides.go index bcc457cc0..7d19fe6a1 100644 --- a/pkg/apis/workspaces/v1alpha2/zz_generated.parent_overrides.go +++ b/pkg/apis/workspaces/v1alpha2/zz_generated.parent_overrides.go @@ -8,6 +8,18 @@ import ( type ParentOverrides struct { OverridesBase `json:",inline"` + // Overrides of variables encapsulated in a parent devfile. + // Overriding is done according to K8S strategic merge patch standard rules. + // +optional + // +patchStrategy=merge + Variables map[string]string `json:"variables,omitempty" patchStrategy:"merge"` + + // Overrides of attributes encapsulated in a parent devfile. + // Overriding is done according to K8S strategic merge patch standard rules. + // +optional + // +patchStrategy=merge + Attributes attributes.Attributes `json:"attributes,omitempty" patchStrategy:"merge"` + // Overrides of components encapsulated in a parent devfile or a plugin. // Overriding is done according to K8S strategic merge patch standard rules. // +optional diff --git a/pkg/devfile/header.go b/pkg/devfile/header.go index 6cd81d2bb..5b6aa75e6 100644 --- a/pkg/devfile/header.go +++ b/pkg/devfile/header.go @@ -27,7 +27,7 @@ type DevfileMetadata struct { // +kubebuilder:validation:Pattern=^([0-9]+)\.([0-9]+)\.([0-9]+)(\-[0-9a-z-]+(\.[0-9a-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$ Version string `json:"version,omitempty"` - // Map of implementation-dependant free-form YAML attributes. + // Map of implementation-dependant free-form YAML attributes. Deprecated, use the top-level attributes field instead. // +optional Attributes attributes.Attributes `json:"attributes,omitempty"` diff --git a/pkg/utils/overriding/keys.go b/pkg/utils/overriding/keys.go index aa25dc472..b525e8a7b 100644 --- a/pkg/utils/overriding/keys.go +++ b/pkg/utils/overriding/keys.go @@ -1,7 +1,11 @@ package overriding import ( + "fmt" + "reflect" + dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + attributesPkg "github.com/devfile/api/v2/pkg/attributes" "github.com/hashicorp/go-multierror" "k8s.io/apimachinery/pkg/util/sets" ) @@ -27,7 +31,49 @@ func checkKeys(doCheck checkFn, toplevelListContainers ...dw.TopLevelListContain for listType, listElem := range topLevelList { listTypeToKeys[listType] = append(listTypeToKeys[listType], sets.NewString(listElem.GetKeys()...)) } + + value := reflect.ValueOf(topLevelListContainer) + + var variableValue reflect.Value + var attributeValue reflect.Value + + // toplevelListContainers can contain either a pointer or a struct and needs to be safeguarded when using reflect + if value.Kind() == reflect.Ptr { + variableValue = value.Elem().FieldByName("Variables") + attributeValue = value.Elem().FieldByName("Attributes") + } else { + variableValue = value.FieldByName("Variables") + attributeValue = value.FieldByName("Attributes") + } + + if variableValue.IsValid() && variableValue.Kind() == reflect.Map { + mapIter := variableValue.MapRange() + + var variableKeys []string + for mapIter.Next() { + k := mapIter.Key() + v := mapIter.Value() + if k.Kind() != reflect.String || v.Kind() != reflect.String { + return fmt.Errorf("unable to fetch top-level Variables, top-level Variables should be map of strings") + } + variableKeys = append(variableKeys, k.String()) + } + listTypeToKeys["Variables"] = append(listTypeToKeys["Variables"], sets.NewString(variableKeys...)) + } + + if attributeValue.IsValid() && attributeValue.CanInterface() { + attributes, ok := attributeValue.Interface().(attributesPkg.Attributes) + if !ok { + return fmt.Errorf("unable to fetch top-level Attributes from the devfile data") + } + var attributeKeys []string + for k := range attributes { + attributeKeys = append(attributeKeys, k) + } + listTypeToKeys["Attributes"] = append(listTypeToKeys["Attributes"], sets.NewString(attributeKeys...)) + } } + for listType, keySets := range listTypeToKeys { errors = multierror.Append(errors, doCheck(listType, keySets)...) } diff --git a/pkg/utils/overriding/merging.go b/pkg/utils/overriding/merging.go index 72a1e5093..279e54de9 100644 --- a/pkg/utils/overriding/merging.go +++ b/pkg/utils/overriding/merging.go @@ -6,6 +6,7 @@ import ( "strings" dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" "k8s.io/apimachinery/pkg/util/json" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apimachinery/pkg/util/yaml" @@ -105,6 +106,28 @@ func MergeDevWorkspaceTemplateSpec( preStopCommands = preStopCommands.Union(sets.NewString(content.Events.PreStop...)) postStopCommands = postStopCommands.Union(sets.NewString(content.Events.PostStop...)) } + + if len(content.Variables) > 0 { + if len(result.Variables) == 0 { + result.Variables = make(map[string]string) + } + for k, v := range content.Variables { + result.Variables[k] = v + } + } + + var err error + if len(content.Attributes) > 0 { + if len(result.Attributes) == 0 { + result.Attributes = attributes.Attributes{} + } + for k, v := range content.Attributes { + result.Attributes.Put(k, v, &err) + if err != nil { + return nil, err + } + } + } } if result.Events != nil { diff --git a/pkg/utils/overriding/merging_test.go b/pkg/utils/overriding/merging_test.go new file mode 100644 index 000000000..59e1b4bb6 --- /dev/null +++ b/pkg/utils/overriding/merging_test.go @@ -0,0 +1,120 @@ +package overriding + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/stretchr/testify/assert" +) + +func mergingPatchTest(main, parent []byte, expected dw.DevWorkspaceTemplateSpecContent, expectedError string, plugins ...[]byte) func(t *testing.T) { + return func(t *testing.T) { + actual, err := MergeDevWorkspaceTemplateSpecBytes(main, parent, plugins...) + if err != nil { + compareErrorMessages(t, expectedError, err.Error(), "wrong error") + return + } + if expectedError != "" { + t.Error("Expected error but did not get one") + return + } + + assert.Equal(t, &expected, actual, "The two values should be the same") + } +} + +func TestMerging(t *testing.T) { + filepath.Walk("test-fixtures/merges", func(path string, info os.FileInfo, err error) error { + if !info.IsDir() && info.Name() == "main.yaml" { + if err != nil { + t.Error(err) + return nil + } + main, err := ioutil.ReadFile(path) + if err != nil { + t.Error(err) + return nil + } + dirPath := filepath.Dir(path) + parent := []byte{} + parentFile := filepath.Join(dirPath, "parent.yaml") + if _, err = os.Stat(parentFile); err == nil { + parent, err = ioutil.ReadFile(parentFile) + if err != nil { + t.Error(err) + return nil + } + } + + plugins := [][]byte{} + pluginFile := filepath.Join(dirPath, "plugin.yaml") + if _, err = os.Stat(pluginFile); err == nil { + plugin, err := ioutil.ReadFile(filepath.Join(dirPath, "plugin.yaml")) + if err != nil { + t.Error(err) + return nil + } + plugins = append(plugins, plugin) + } + var resultTemplate dw.DevWorkspaceTemplateSpecContent + resultError := "" + errorFile := filepath.Join(dirPath, "result-error.txt") + if _, err = os.Stat(errorFile); err == nil { + resultErrorBytes, err := ioutil.ReadFile(errorFile) + if err != nil { + t.Error(err) + return nil + } + resultError = string(resultErrorBytes) + } else { + readFileToStruct(t, filepath.Join(dirPath, "result.yaml"), &resultTemplate) + } + testName := filepath.Base(dirPath) + + t.Run(testName, mergingPatchTest(main, parent, resultTemplate, resultError, plugins...)) + } + return nil + }) +} + +func TestMergingOnlyPlugins(t *testing.T) { + baseFile := "test-fixtures/merges/no-parent/main.yaml" + pluginFile := "test-fixtures/merges/no-parent/plugin.yaml" + resultFile := "test-fixtures/merges/no-parent/result.yaml" + + baseDWT := dw.DevWorkspaceTemplateSpecContent{} + pluginDWT := dw.DevWorkspaceTemplateSpecContent{} + expectedDWT := dw.DevWorkspaceTemplateSpecContent{} + + readFileToStruct(t, baseFile, &baseDWT) + readFileToStruct(t, pluginFile, &pluginDWT) + readFileToStruct(t, resultFile, &expectedDWT) + + gotDWT, err := MergeDevWorkspaceTemplateSpec(&baseDWT, nil, &pluginDWT) + if assert.NoError(t, err) { + assert.Equal(t, &expectedDWT, gotDWT) + } +} + +func TestMergingOnlyParent(t *testing.T) { + // Reuse only plugin case since it's compatible + baseFile := "test-fixtures/merges/no-parent/main.yaml" + parentFile := "test-fixtures/merges/no-parent/plugin.yaml" + resultFile := "test-fixtures/merges/no-parent/result.yaml" + + baseDWT := dw.DevWorkspaceTemplateSpecContent{} + parentDWT := dw.DevWorkspaceTemplateSpecContent{} + expectedDWT := dw.DevWorkspaceTemplateSpecContent{} + + readFileToStruct(t, baseFile, &baseDWT) + readFileToStruct(t, parentFile, &parentDWT) + readFileToStruct(t, resultFile, &expectedDWT) + + gotDWT, err := MergeDevWorkspaceTemplateSpec(&baseDWT, &parentDWT) + if assert.NoError(t, err) { + assert.Equal(t, &expectedDWT, gotDWT) + } +} diff --git a/pkg/utils/overriding/overriding_test.go b/pkg/utils/overriding/overriding_test.go index 5b6af2dd5..41c9499af 100644 --- a/pkg/utils/overriding/overriding_test.go +++ b/pkg/utils/overriding/overriding_test.go @@ -8,9 +8,8 @@ import ( "testing" dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + attributesPkg "github.com/devfile/api/v2/pkg/attributes" "github.com/stretchr/testify/assert" - "k8s.io/apimachinery/pkg/util/json" - yamlMachinery "k8s.io/apimachinery/pkg/util/yaml" "sigs.k8s.io/yaml" ) @@ -51,6 +50,14 @@ func TestBasicToplevelOverriding(t *testing.T) { }, }, }, + Variables: map[string]string{ + "version": "main", + "xyz": "xyz", + }, + Attributes: attributesPkg.Attributes{}.FromMap(map[string]interface{}{ + "version": "main", + "xyz": "xyz", + }, nil), } patch := dw.ParentOverrides{ @@ -81,6 +88,14 @@ func TestBasicToplevelOverriding(t *testing.T) { }, }, }, + Variables: map[string]string{ + "version": "patch", + }, + Attributes: attributesPkg.Attributes{}.FromMap(map[string]interface{}{ + "version": map[string]interface{}{ + "patch": true, + }, + }, nil), } expected := &dw.DevWorkspaceTemplateSpecContent{ @@ -125,19 +140,30 @@ func TestBasicToplevelOverriding(t *testing.T) { }, }, }, + Variables: map[string]string{ + "version": "patch", + "xyz": "xyz", + }, + Attributes: attributesPkg.Attributes{}.FromMap(map[string]interface{}{ + "version": map[string]interface{}{ + "patch": true, + }, + "xyz": "xyz", + }, nil), } result, err := OverrideDevWorkspaceTemplateSpec(&original, &patch) if err != nil { t.Error(err) + return } assert.Equal(t, expected, result, "The two values should be the same.") } -func overridingPatchTest(original, patch, expected []byte, expectedError string) func(t *testing.T) { +func overridingPatchTest(original, patch []byte, expected dw.DevWorkspaceTemplateSpecContent, expectedError string) func(t *testing.T) { return func(t *testing.T) { - result, err := OverrideDevWorkspaceTemplateSpecBytes(original, patch) + actual, err := OverrideDevWorkspaceTemplateSpecBytes(original, patch) if err != nil { compareErrorMessages(t, expectedError, err.Error(), "wrong error") return @@ -147,25 +173,7 @@ func overridingPatchTest(original, patch, expected []byte, expectedError string) return } - resultJson, err := json.Marshal(result) - if err != nil { - t.Error(err) - } - resultYaml, err := yaml.JSONToYAML(resultJson) - if err != nil { - t.Error(err) - } - - expectedJson, err := yamlMachinery.ToJSON(expected) - if err != nil { - t.Error(err) - } - expectedYaml, err := yaml.JSONToYAML(expectedJson) - if err != nil { - t.Error(err) - } - - assert.Equal(t, string(expectedYaml), string(resultYaml), "The two values should be the same.") + assert.Equal(t, &expected, actual, "The two values should be the same") } } @@ -187,7 +195,7 @@ func TestOverridingPatches(t *testing.T) { t.Error(err) return nil } - result := []byte{} + var resultTemplate dw.DevWorkspaceTemplateSpecContent resultError := "" errorFile := filepath.Join(dirPath, "result-error.txt") if _, err = os.Stat(errorFile); err == nil { @@ -198,107 +206,11 @@ func TestOverridingPatches(t *testing.T) { } resultError = string(resultErrorBytes) } else { - result, err = ioutil.ReadFile(filepath.Join(dirPath, "result.yaml")) - if err != nil { - t.Error(err) - return nil - } + readFileToStruct(t, filepath.Join(dirPath, "result.yaml"), &resultTemplate) } testName := filepath.Base(dirPath) - t.Run(testName, overridingPatchTest(original, patch, result, resultError)) - } - return nil - }) -} - -func mergingPatchTest(main, parent, expected []byte, expectedError string, plugins ...[]byte) func(t *testing.T) { - return func(t *testing.T) { - result, err := MergeDevWorkspaceTemplateSpecBytes(main, parent, plugins...) - if err != nil { - compareErrorMessages(t, expectedError, err.Error(), "wrong error") - return - } - if expectedError != "" { - t.Error("Expected error but did not get one") - return - } - - resultJson, err := json.Marshal(result) - if err != nil { - t.Error(err) - } - resultYaml, err := yaml.JSONToYAML(resultJson) - if err != nil { - t.Error(err) - } - - expectedJson, err := yamlMachinery.ToJSON(expected) - if err != nil { - t.Error(err) - } - expectedYaml, err := yaml.JSONToYAML(expectedJson) - if err != nil { - t.Error(err) - } - - assert.Equal(t, string(expectedYaml), string(resultYaml), "The two values should be the same.") - } -} - -func TestMerging(t *testing.T) { - filepath.Walk("test-fixtures/merges", func(path string, info os.FileInfo, err error) error { - if !info.IsDir() && info.Name() == "main.yaml" { - if err != nil { - t.Error(err) - return nil - } - main, err := ioutil.ReadFile(path) - if err != nil { - t.Error(err) - return nil - } - dirPath := filepath.Dir(path) - parent := []byte{} - parentFile := filepath.Join(dirPath, "parent.yaml") - if _, err = os.Stat(parentFile); err == nil { - parent, err = ioutil.ReadFile(parentFile) - if err != nil { - t.Error(err) - return nil - } - } - - plugins := [][]byte{} - pluginFile := filepath.Join(dirPath, "plugin.yaml") - if _, err = os.Stat(pluginFile); err == nil { - plugin, err := ioutil.ReadFile(filepath.Join(dirPath, "plugin.yaml")) - if err != nil { - t.Error(err) - return nil - } - plugins = append(plugins, plugin) - } - result := []byte{} - resultError := "" - errorFile := filepath.Join(dirPath, "result-error.txt") - if _, err = os.Stat(errorFile); err == nil { - resultErrorBytes, err := ioutil.ReadFile(errorFile) - if err != nil { - t.Error(err) - return nil - } - resultError = string(resultErrorBytes) - } else { - result, err = ioutil.ReadFile(filepath.Join(dirPath, "result.yaml")) - if err != nil { - t.Error(err) - return nil - } - } - testName := filepath.Base(dirPath) - - t.Run(testName, mergingPatchTest(main, parent, result, resultError, plugins...)) + t.Run(testName, overridingPatchTest(original, patch, resultTemplate, resultError)) } return nil }) @@ -323,45 +235,6 @@ func TestPluginOverrides(t *testing.T) { } } -func TestMergingOnlyPlugins(t *testing.T) { - baseFile := "test-fixtures/merges/no-parent/main.yaml" - pluginFile := "test-fixtures/merges/no-parent/plugin.yaml" - resultFile := "test-fixtures/merges/no-parent/result.yaml" - - baseDWT := dw.DevWorkspaceTemplateSpecContent{} - pluginDWT := dw.DevWorkspaceTemplateSpecContent{} - expectedDWT := dw.DevWorkspaceTemplateSpecContent{} - - readFileToStruct(t, baseFile, &baseDWT) - readFileToStruct(t, pluginFile, &pluginDWT) - readFileToStruct(t, resultFile, &expectedDWT) - - gotDWT, err := MergeDevWorkspaceTemplateSpec(&baseDWT, nil, &pluginDWT) - if assert.NoError(t, err) { - assert.Equal(t, &expectedDWT, gotDWT) - } -} - -func TestMergingOnlyParent(t *testing.T) { - // Reuse only plugin case since it's compatible - baseFile := "test-fixtures/merges/no-parent/main.yaml" - parentFile := "test-fixtures/merges/no-parent/plugin.yaml" - resultFile := "test-fixtures/merges/no-parent/result.yaml" - - baseDWT := dw.DevWorkspaceTemplateSpecContent{} - parentDWT := dw.DevWorkspaceTemplateSpecContent{} - expectedDWT := dw.DevWorkspaceTemplateSpecContent{} - - readFileToStruct(t, baseFile, &baseDWT) - readFileToStruct(t, parentFile, &parentDWT) - readFileToStruct(t, resultFile, &expectedDWT) - - gotDWT, err := MergeDevWorkspaceTemplateSpec(&baseDWT, &parentDWT) - if assert.NoError(t, err) { - assert.Equal(t, &expectedDWT, gotDWT) - } -} - func readFileToStruct(t *testing.T, path string, into interface{}) { bytes, err := ioutil.ReadFile(path) if err != nil { diff --git a/pkg/utils/overriding/test-fixtures/merges/duplicate-with-parent/main.yaml b/pkg/utils/overriding/test-fixtures/merges/duplicate-with-parent/main.yaml index 8a832c6e3..b1ac1a152 100644 --- a/pkg/utils/overriding/test-fixtures/merges/duplicate-with-parent/main.yaml +++ b/pkg/utils/overriding/test-fixtures/merges/duplicate-with-parent/main.yaml @@ -1,5 +1,9 @@ parent: uri: "anyParent" +variables: + objectVariable: mainValue +attributes: + mainAttribute: true components: - container: image: "aDifferentValue" diff --git a/pkg/utils/overriding/test-fixtures/merges/duplicate-with-parent/parent.yaml b/pkg/utils/overriding/test-fixtures/merges/duplicate-with-parent/parent.yaml index 989a491c6..045d2b33a 100644 --- a/pkg/utils/overriding/test-fixtures/merges/duplicate-with-parent/parent.yaml +++ b/pkg/utils/overriding/test-fixtures/merges/duplicate-with-parent/parent.yaml @@ -1,3 +1,7 @@ +variables: + objectVariable: parentValue +attributes: + mainAttribute: false components: - container: image: "aValue" diff --git a/pkg/utils/overriding/test-fixtures/merges/duplicate-with-parent/result-error.txt b/pkg/utils/overriding/test-fixtures/merges/duplicate-with-parent/result-error.txt index be0507ac0..358f7a5b9 100644 --- a/pkg/utils/overriding/test-fixtures/merges/duplicate-with-parent/result-error.txt +++ b/pkg/utils/overriding/test-fixtures/merges/duplicate-with-parent/result-error.txt @@ -1,2 +1,4 @@ -1 error occurred: +3 errors occurred: * Some Components are already defined in parent: existing-in-parent. If you want to override them, you should do it in the parent scope. + * Some Variables are already defined in parent: objectVariable. If you want to override them, you should do it in the parent scope. + * Some Attributes are already defined in parent: mainAttribute. If you want to override them, you should do it in the parent scope. diff --git a/pkg/utils/overriding/test-fixtures/merges/no-duplicate/main.yaml b/pkg/utils/overriding/test-fixtures/merges/no-duplicate/main.yaml index bf23b1dd0..b1e2fb0d9 100644 --- a/pkg/utils/overriding/test-fixtures/merges/no-duplicate/main.yaml +++ b/pkg/utils/overriding/test-fixtures/merges/no-duplicate/main.yaml @@ -1,5 +1,9 @@ parent: uri: "anyParent" +variables: + version1: main +attributes: + main: true components: - plugin: uri: "aCustomLocation" diff --git a/pkg/utils/overriding/test-fixtures/merges/no-duplicate/parent.yaml b/pkg/utils/overriding/test-fixtures/merges/no-duplicate/parent.yaml index 4bf4a854c..5e5637f72 100644 --- a/pkg/utils/overriding/test-fixtures/merges/no-duplicate/parent.yaml +++ b/pkg/utils/overriding/test-fixtures/merges/no-duplicate/parent.yaml @@ -1,3 +1,7 @@ +variables: + version2: parent +attributes: + parent: true components: - container: image: "aValue" diff --git a/pkg/utils/overriding/test-fixtures/merges/no-duplicate/plugin.yaml b/pkg/utils/overriding/test-fixtures/merges/no-duplicate/plugin.yaml index 43c089c57..1ad3f11c7 100644 --- a/pkg/utils/overriding/test-fixtures/merges/no-duplicate/plugin.yaml +++ b/pkg/utils/overriding/test-fixtures/merges/no-duplicate/plugin.yaml @@ -1,3 +1,7 @@ +variables: + version3: plugin +attributes: + plugin: true components: - container: image: "aValue" diff --git a/pkg/utils/overriding/test-fixtures/merges/no-duplicate/result.yaml b/pkg/utils/overriding/test-fixtures/merges/no-duplicate/result.yaml index fc5c89742..fe10426e2 100644 --- a/pkg/utils/overriding/test-fixtures/merges/no-duplicate/result.yaml +++ b/pkg/utils/overriding/test-fixtures/merges/no-duplicate/result.yaml @@ -1,3 +1,11 @@ +variables: + version1: main + version2: parent + version3: plugin +attributes: + main: true + parent: true + plugin: true components: - container: image: "aValue" diff --git a/pkg/utils/overriding/test-fixtures/patches/global-variables-and-attributes-err/original.yaml b/pkg/utils/overriding/test-fixtures/patches/global-variables-and-attributes-err/original.yaml new file mode 100644 index 000000000..a7281aec0 --- /dev/null +++ b/pkg/utils/overriding/test-fixtures/patches/global-variables-and-attributes-err/original.yaml @@ -0,0 +1,11 @@ +variables: + stringVariableToChange: original + stringVariableToKeep: stringValue +attributes: + boolAttributeToChange: true + stringAttributeToKeep: stringValue + + objectAttributeToChange: + attributeField: 9.9 + objectAttributeField: + objectAttributeSubField: 10 diff --git a/pkg/utils/overriding/test-fixtures/patches/global-variables-and-attributes-err/patch.yaml b/pkg/utils/overriding/test-fixtures/patches/global-variables-and-attributes-err/patch.yaml new file mode 100644 index 000000000..fccfbe8a7 --- /dev/null +++ b/pkg/utils/overriding/test-fixtures/patches/global-variables-and-attributes-err/patch.yaml @@ -0,0 +1,14 @@ +variables: + stringVariableToChange: patch + + newVariableValid: nope +attributes: + boolAttributeToChange: false + + newAttributeValid: false + objectAttributeToChange: + + newObjectAttributeField: + objectAttributeSubField: 11 + newObjectAttributeSubField: newObjectAttributeFieldValue + newField: true diff --git a/pkg/utils/overriding/test-fixtures/patches/global-variables-and-attributes-err/result-error.txt b/pkg/utils/overriding/test-fixtures/patches/global-variables-and-attributes-err/result-error.txt new file mode 100644 index 000000000..5c994d27b --- /dev/null +++ b/pkg/utils/overriding/test-fixtures/patches/global-variables-and-attributes-err/result-error.txt @@ -0,0 +1,3 @@ +2 errors occurred: + * Some Variables do not override any existing element: newVariableValid. They should be defined in the main body, as new elements, not in the overriding section + * Some Attributes do not override any existing element: newAttributeValid. They should be defined in the main body, as new elements, not in the overriding section diff --git a/pkg/utils/overriding/test-fixtures/patches/global-variables-and-attributes/original.yaml b/pkg/utils/overriding/test-fixtures/patches/global-variables-and-attributes/original.yaml new file mode 100644 index 000000000..8ceb4913e --- /dev/null +++ b/pkg/utils/overriding/test-fixtures/patches/global-variables-and-attributes/original.yaml @@ -0,0 +1,10 @@ +variables: + stringVariableToChange: originalValue + stringVariableToKeep: stringValue +attributes: + boolAttributeToChange: true + stringAttributeToKeep: stringValue + objectAttributeToChange: + attributeField: 9.9 + objectAttributeField: + objectAttributeSubField: 10 diff --git a/pkg/utils/overriding/test-fixtures/patches/global-variables-and-attributes/patch.yaml b/pkg/utils/overriding/test-fixtures/patches/global-variables-and-attributes/patch.yaml new file mode 100644 index 000000000..3a12b69d1 --- /dev/null +++ b/pkg/utils/overriding/test-fixtures/patches/global-variables-and-attributes/patch.yaml @@ -0,0 +1,11 @@ +variables: + stringVariableToChange: patchedValue +attributes: + boolAttributeToChange: false + + objectAttributeToChange: + + newObjectAttributeField: + objectAttributeSubField: 11 + newObjectAttributeSubField: newObjectAttributeFieldValue + newField: true diff --git a/pkg/utils/overriding/test-fixtures/patches/global-variables-and-attributes/result.yaml b/pkg/utils/overriding/test-fixtures/patches/global-variables-and-attributes/result.yaml new file mode 100644 index 000000000..003cced0b --- /dev/null +++ b/pkg/utils/overriding/test-fixtures/patches/global-variables-and-attributes/result.yaml @@ -0,0 +1,12 @@ +variables: + stringVariableToChange: patchedValue + stringVariableToKeep: stringValue +attributes: + boolAttributeToChange: false + stringAttributeToKeep: stringValue + objectAttributeToChange: + + newObjectAttributeField: + objectAttributeSubField: 11 + newObjectAttributeSubField: newObjectAttributeFieldValue + newField: true diff --git a/pkg/validation/variables/errors.go b/pkg/validation/variables/errors.go new file mode 100644 index 000000000..572eefaac --- /dev/null +++ b/pkg/validation/variables/errors.go @@ -0,0 +1,31 @@ +package variables + +import ( + "fmt" + "sort" + "strings" +) + +// InvalidKeysError returns an error for the invalid keys +type InvalidKeysError struct { + Keys []string +} + +func (e *InvalidKeysError) Error() string { + return fmt.Sprintf("invalid variable references - %s", strings.Join(e.Keys, ",")) +} + +// newInvalidKeysError processes the invalid key set and returns an InvalidKeysError if present +func newInvalidKeysError(keySet map[string]bool) error { + var invalidKeysArr []string + for key := range keySet { + invalidKeysArr = append(invalidKeysArr, key) + } + + if len(invalidKeysArr) > 0 { + sort.Strings(invalidKeysArr) + return &InvalidKeysError{Keys: invalidKeysArr} + } + + return nil +} diff --git a/pkg/validation/variables/test-fixtures/all/devfile-bad-output.yaml b/pkg/validation/variables/test-fixtures/all/devfile-bad-output.yaml new file mode 100644 index 000000000..373fd73bb --- /dev/null +++ b/pkg/validation/variables/test-fixtures/all/devfile-bad-output.yaml @@ -0,0 +1,80 @@ +variables: + devnull: /dev/null +projects: +- name: project1 + clonePath: "{{path}}" + sparseCheckoutDirs: + - xyz + - "{{dir}}" + git: + checkoutFrom: + revision: "{{tag}}" + remotes: + "{{dir}}": "{{version1}}/dev/null-/dev/null" + "{{version}}": "test" +- name: project2 + zip: + location: "{{tag}}" +starterProjects: +- name: starterproject1 + description: "{{desc}}" + subDir: "{{dir}}" + git: + checkoutFrom: + revision: "{{tag}}" + remotes: + "{{tag}}": "/dev/null" + "{{dir}}": "test" +- name: starterproject2 + zip: + location: "{{tag}}" +components: +- name: component1 + container: + image: "{{a}}" + env: + - name: BAR + value: "{{b}}" + - name: "{{c}}" + value: "{{bar}}" + command: + - tail + - -f + - "{{b}}" + - "{{c}}" +- name: component2 + kubernetes: + inlined: "{{foo}}" + endpoints: + - name: endpoint1 + exposure: "public" + protocol: "https" + path : "/{{x}}}" + targetPort: 9998 + - name: endpoint2 + path : "{{bar}}" + targetPort: 9999 +- name: component3 + volume: + size: "{{xyz}}" +- name: component4 + openshift: + uri: "{{foo}}" +commands: +- id: command1 + exec: + commandLine: "test-{{tag}}" + env: + - name: tag + value: "{{tag}}" + - name: FOO + value: "{{BAR}}" +- id: command2 + composite: + commands: + - xyz + - command1 + label: "{{abc}}" +- id: command3 + apply: + label: "{{abc}}" diff --git a/pkg/validation/variables/test-fixtures/all/devfile-bad.yaml b/pkg/validation/variables/test-fixtures/all/devfile-bad.yaml new file mode 100644 index 000000000..7e46bc4c6 --- /dev/null +++ b/pkg/validation/variables/test-fixtures/all/devfile-bad.yaml @@ -0,0 +1,80 @@ +variables: + devnull: /dev/null +projects: +- name: project1 + clonePath: "{{path}}" + sparseCheckoutDirs: + - xyz + - "{{dir}}" + git: + checkoutFrom: + revision: "{{tag}}" + remotes: + "{{dir}}": "{{version1}}{{devnull}}-{{devnull}}" + "{{version}}": "test" +- name: project2 + zip: + location: "{{tag}}" +starterProjects: +- name: starterproject1 + description: "{{desc}}" + subDir: "{{dir}}" + git: + checkoutFrom: + revision: "{{tag}}" + remotes: + "{{tag}}": "{{devnull}}" + "{{dir}}": "test" +- name: starterproject2 + zip: + location: "{{tag}}" +components: +- name: component1 + container: + image: "{{a}}" + env: + - name: BAR + value: "{{b}}" + - name: "{{c}}" + value: "{{bar}}" + command: + - tail + - -f + - "{{b}}" + - "{{c}}" +- name: component2 + kubernetes: + inlined: "{{foo}}" + endpoints: + - name: endpoint1 + exposure: "public" + protocol: "https" + path : "/{{x}}}" + targetPort: 9998 + - name: endpoint2 + path : "{{bar}}" + targetPort: 9999 +- name: component3 + volume: + size: "{{xyz}}" +- name: component4 + openshift: + uri: "{{foo}}" +commands: +- id: command1 + exec: + commandLine: "test-{{tag}}" + env: + - name: tag + value: "{{tag}}" + - name: FOO + value: "{{BAR}}" +- id: command2 + composite: + commands: + - xyz + - command1 + label: "{{abc}}" +- id: command3 + apply: + label: "{{abc}}" diff --git a/pkg/validation/variables/test-fixtures/all/devfile-good-output.yaml b/pkg/validation/variables/test-fixtures/all/devfile-good-output.yaml new file mode 100644 index 000000000..4cd2a463b --- /dev/null +++ b/pkg/validation/variables/test-fixtures/all/devfile-good-output.yaml @@ -0,0 +1,63 @@ +variables: + tag: xyz + version: "1" + foo: FOO + devnull: /dev/null +projects: +- name: project1 + git: + checkoutFrom: + revision: "xyz" + remotes: + "xyz": "/dev/null" + "1": "test" +- name: project2 + zip: + location: "xyz" +starterProjects: +- name: starterproject1 + git: + checkoutFrom: + revision: "xyz" + remotes: + "xyz": "/dev/null" + "1": "test" +components: +- name: component1 + container: + image: image + env: + - name: BAR + value: "FOO" + - name: FOO + value: BAR + command: + - tail + - -f + - "/dev/null" +- name: component2 + kubernetes: + inlined: "FOO" + endpoints: + - name: endpoint1 + exposure: "public" + targetPort: 9999 +commands: +- id: command1 + exec: + commandLine: "test-xyz" + env: + - name: tag + value: "xyz" + - name: FOO + value: BAR +- id: command2 + composite: + commands: + - xyz + - command1 +events: + preStart: + - command1 + preStop: + - command2 diff --git a/pkg/validation/variables/test-fixtures/all/devfile-good.yaml b/pkg/validation/variables/test-fixtures/all/devfile-good.yaml new file mode 100644 index 000000000..7a33047e6 --- /dev/null +++ b/pkg/validation/variables/test-fixtures/all/devfile-good.yaml @@ -0,0 +1,63 @@ +variables: + tag: xyz + version: "1" + foo: FOO + devnull: /dev/null +projects: +- name: project1 + git: + checkoutFrom: + revision: "{{tag}}" + remotes: + "{{tag}}": "{{devnull}}" + "{{version}}": "test" +- name: project2 + zip: + location: "{{tag}}" +starterProjects: +- name: starterproject1 + git: + checkoutFrom: + revision: "{{tag}}" + remotes: + "{{tag}}": "{{devnull}}" + "{{version}}": "test" +components: +- name: component1 + container: + image: image + env: + - name: BAR + value: "{{foo}}" + - name: FOO + value: BAR + command: + - tail + - -f + - "{{devnull}}" +- name: component2 + kubernetes: + inlined: "{{foo}}" + endpoints: + - name: endpoint1 + exposure: "public" + targetPort: 9999 +commands: +- id: command1 + exec: + commandLine: "test-{{tag}}" + env: + - name: tag + value: "{{tag}}" + - name: FOO + value: BAR +- id: command2 + composite: + commands: + - xyz + - command1 +events: + preStart: + - command1 + preStop: + - command2 diff --git a/pkg/validation/variables/test-fixtures/commands/apply-output.yaml b/pkg/validation/variables/test-fixtures/commands/apply-output.yaml new file mode 100644 index 000000000..be0a939fe --- /dev/null +++ b/pkg/validation/variables/test-fixtures/commands/apply-output.yaml @@ -0,0 +1,2 @@ +label: "1" +component: component diff --git a/pkg/validation/variables/test-fixtures/commands/apply.yaml b/pkg/validation/variables/test-fixtures/commands/apply.yaml new file mode 100644 index 000000000..fa8fda518 --- /dev/null +++ b/pkg/validation/variables/test-fixtures/commands/apply.yaml @@ -0,0 +1,3 @@ +# Variables are defined in test-fixtures/variables/variables-referenced.yaml +label: "{{version}}" +component: component diff --git a/pkg/validation/variables/test-fixtures/commands/composite-output.yaml b/pkg/validation/variables/test-fixtures/commands/composite-output.yaml new file mode 100644 index 000000000..56b773d35 --- /dev/null +++ b/pkg/validation/variables/test-fixtures/commands/composite-output.yaml @@ -0,0 +1,4 @@ +label: "1" +commands: + - FOO + - BAR diff --git a/pkg/validation/variables/test-fixtures/commands/composite.yaml b/pkg/validation/variables/test-fixtures/commands/composite.yaml new file mode 100644 index 000000000..064aa0b69 --- /dev/null +++ b/pkg/validation/variables/test-fixtures/commands/composite.yaml @@ -0,0 +1,5 @@ +# Variables are defined in test-fixtures/variables/variables-referenced.yaml +label: "{{version}}" +commands: + - FOO + - BAR diff --git a/pkg/validation/variables/test-fixtures/commands/exec-output.yaml b/pkg/validation/variables/test-fixtures/commands/exec-output.yaml new file mode 100644 index 000000000..0c2b64896 --- /dev/null +++ b/pkg/validation/variables/test-fixtures/commands/exec-output.yaml @@ -0,0 +1,7 @@ +component: component +commandLine: tail -f /dev/null +workingDir: "FOO" +label: "1" +env: + - name: "FOO" + value: "BAR" diff --git a/pkg/validation/variables/test-fixtures/commands/exec.yaml b/pkg/validation/variables/test-fixtures/commands/exec.yaml new file mode 100644 index 000000000..16cbf5744 --- /dev/null +++ b/pkg/validation/variables/test-fixtures/commands/exec.yaml @@ -0,0 +1,8 @@ +# Variables are defined in test-fixtures/variables/variables-referenced.yaml +component: component +commandLine: tail -f {{devnull}} +workingDir: "{{foo}}" +label: "{{version}}" +env: + - name: "{{foo}}" + value: "{{bar}}" diff --git a/pkg/validation/variables/test-fixtures/components/container-output.yaml b/pkg/validation/variables/test-fixtures/components/container-output.yaml new file mode 100644 index 000000000..15322bd49 --- /dev/null +++ b/pkg/validation/variables/test-fixtures/components/container-output.yaml @@ -0,0 +1,20 @@ +image: "image-1" +env: + - name: "FOO" + value: "BAR" +command: + - tail + - -f + - "/dev/null" +args: + - "/dev/null" +memoryLimit: "FOO" +memoryRequest: "FOO" +sourceMapping: "FOO" +volumeMounts: + - name: vol1 + path: "/FOO" +endpoints: + - name: endpoint1 + exposure: public + path: "FOO" diff --git a/pkg/validation/variables/test-fixtures/components/container.yaml b/pkg/validation/variables/test-fixtures/components/container.yaml new file mode 100644 index 000000000..e7d89047e --- /dev/null +++ b/pkg/validation/variables/test-fixtures/components/container.yaml @@ -0,0 +1,21 @@ +# Variables are defined in test-fixtures/variables/variables-referenced.yaml +image: "image-{{version}}" +env: + - name: "{{foo}}" + value: "{{bar}}" +command: + - tail + - -f + - "{{devnull}}" +args: + - "{{devnull}}" +memoryLimit: "{{foo}}" +memoryRequest: "{{foo}}" +sourceMapping: "{{foo}}" +volumeMounts: + - name: vol1 + path: "/{{foo}}" +endpoints: + - name: endpoint1 + exposure: public + path: "{{foo}}" diff --git a/pkg/validation/variables/test-fixtures/components/endpoint-output.yaml b/pkg/validation/variables/test-fixtures/components/endpoint-output.yaml new file mode 100644 index 000000000..6e9ad1e3e --- /dev/null +++ b/pkg/validation/variables/test-fixtures/components/endpoint-output.yaml @@ -0,0 +1,5 @@ +name: endpoint1 +exposure: "public" +protocol: "https" +path : "/FOO" +targetPort: 9999 diff --git a/pkg/validation/variables/test-fixtures/components/endpoint.yaml b/pkg/validation/variables/test-fixtures/components/endpoint.yaml new file mode 100644 index 000000000..a7be586ab --- /dev/null +++ b/pkg/validation/variables/test-fixtures/components/endpoint.yaml @@ -0,0 +1,6 @@ +# Variables are defined in test-fixtures/variables/variables-referenced.yaml +name: endpoint1 +exposure: "public" +protocol: "https" +path : "/{{foo}}" +targetPort: 9999 diff --git a/pkg/validation/variables/test-fixtures/components/env-output.yaml b/pkg/validation/variables/test-fixtures/components/env-output.yaml new file mode 100644 index 000000000..efb23067d --- /dev/null +++ b/pkg/validation/variables/test-fixtures/components/env-output.yaml @@ -0,0 +1,2 @@ +name: "FOO" +value: "BAR" diff --git a/pkg/validation/variables/test-fixtures/components/env.yaml b/pkg/validation/variables/test-fixtures/components/env.yaml new file mode 100644 index 000000000..b3b2ff490 --- /dev/null +++ b/pkg/validation/variables/test-fixtures/components/env.yaml @@ -0,0 +1,3 @@ +# Variables are defined in test-fixtures/variables/variables-referenced.yaml +name: "{{foo}}" +value: "{{bar}}" diff --git a/pkg/validation/variables/test-fixtures/components/openshift-kubernetes-output.yaml b/pkg/validation/variables/test-fixtures/components/openshift-kubernetes-output.yaml new file mode 100644 index 000000000..fb8a4a8f7 --- /dev/null +++ b/pkg/validation/variables/test-fixtures/components/openshift-kubernetes-output.yaml @@ -0,0 +1,16 @@ +uri: "http://link/uri" +inlined: | + apiVersion: batch/v1 + kind: Job + metadata: + name: pi + spec: + template: + spec: + containers: + - name: job + image: inlined +endpoints: + - name: endpoint1 + exposure: public + path: "FOO" diff --git a/pkg/validation/variables/test-fixtures/components/openshift-kubernetes.yaml b/pkg/validation/variables/test-fixtures/components/openshift-kubernetes.yaml new file mode 100644 index 000000000..5af4e4ca4 --- /dev/null +++ b/pkg/validation/variables/test-fixtures/components/openshift-kubernetes.yaml @@ -0,0 +1,17 @@ +# Variables are defined in test-fixtures/variables/variables-referenced.yaml +uri: "http://link/{{uri}}" +inlined: | + apiVersion: batch/v1 + kind: Job + metadata: + name: pi + spec: + template: + spec: + containers: + - name: job + image: {{inlined}} +endpoints: + - name: endpoint1 + exposure: public + path: "{{foo}}" diff --git a/pkg/validation/variables/test-fixtures/components/volume-output.yaml b/pkg/validation/variables/test-fixtures/components/volume-output.yaml new file mode 100644 index 000000000..2272abcb4 --- /dev/null +++ b/pkg/validation/variables/test-fixtures/components/volume-output.yaml @@ -0,0 +1 @@ +size: "1Gi" diff --git a/pkg/validation/variables/test-fixtures/components/volume.yaml b/pkg/validation/variables/test-fixtures/components/volume.yaml new file mode 100644 index 000000000..f8339ebf1 --- /dev/null +++ b/pkg/validation/variables/test-fixtures/components/volume.yaml @@ -0,0 +1,2 @@ +# Variables are defined in test-fixtures/variables/variables-referenced.yaml +size: "{{size}}" diff --git a/pkg/validation/variables/test-fixtures/projects/git-output.yaml b/pkg/validation/variables/test-fixtures/projects/git-output.yaml new file mode 100644 index 000000000..6e8c843d7 --- /dev/null +++ b/pkg/validation/variables/test-fixtures/projects/git-output.yaml @@ -0,0 +1,7 @@ +git: + checkoutFrom: + revision: "FOO" + remote: "BAR" + remotes: + "foo": "BAR" + "FOOBAR": "BARFOO" diff --git a/pkg/validation/variables/test-fixtures/projects/git.yaml b/pkg/validation/variables/test-fixtures/projects/git.yaml new file mode 100644 index 000000000..987cf3434 --- /dev/null +++ b/pkg/validation/variables/test-fixtures/projects/git.yaml @@ -0,0 +1,8 @@ +# Variables are defined in test-fixtures/variables/variables-referenced.yaml +git: + checkoutFrom: + revision: "{{foo}}" + remote: "{{bar}}" + remotes: + "foo": "{{bar}}" + "{{foo}}{{bar}}": "{{bar}}{{foo}}" diff --git a/pkg/validation/variables/test-fixtures/projects/project-output.yaml b/pkg/validation/variables/test-fixtures/projects/project-output.yaml new file mode 100644 index 000000000..47920c2fc --- /dev/null +++ b/pkg/validation/variables/test-fixtures/projects/project-output.yaml @@ -0,0 +1,11 @@ +name: project1 +clonePath: "/FOO" +sparseCheckoutDirs: + - /FOO + - "/BAR" +git: + checkoutFrom: + revision: "FOO" + remotes: + "foo": "BAR" + "FOOBAR": "BARFOO" diff --git a/pkg/validation/variables/test-fixtures/projects/project.yaml b/pkg/validation/variables/test-fixtures/projects/project.yaml new file mode 100644 index 000000000..399a803ef --- /dev/null +++ b/pkg/validation/variables/test-fixtures/projects/project.yaml @@ -0,0 +1,12 @@ +# Variables are defined in test-fixtures/variables/variables-referenced.yaml +name: project1 +clonePath: "/{{foo}}" +sparseCheckoutDirs: + - /FOO + - "/{{bar}}" +git: + checkoutFrom: + revision: "{{foo}}" + remotes: + "foo": "{{bar}}" + "{{foo}}{{bar}}": "{{bar}}{{foo}}" diff --git a/pkg/validation/variables/test-fixtures/projects/starterproject-output.yaml b/pkg/validation/variables/test-fixtures/projects/starterproject-output.yaml new file mode 100644 index 000000000..9ab49330c --- /dev/null +++ b/pkg/validation/variables/test-fixtures/projects/starterproject-output.yaml @@ -0,0 +1,5 @@ +name: starterproject1 +zip: + location: "/FOO" +description: "FOOBAR is not BARFOO" +subDir: "/FOO" diff --git a/pkg/validation/variables/test-fixtures/projects/starterproject.yaml b/pkg/validation/variables/test-fixtures/projects/starterproject.yaml new file mode 100644 index 000000000..0db6f1c2e --- /dev/null +++ b/pkg/validation/variables/test-fixtures/projects/starterproject.yaml @@ -0,0 +1,6 @@ +# Variables are defined in test-fixtures/variables/variables-referenced.yaml +name: starterproject1 +zip: + location: "/{{foo}}" +description: "{{foo}}{{bar}} is not {{bar}}{{foo}}" +subDir: "/{{foo}}" diff --git a/pkg/validation/variables/test-fixtures/projects/zip-output.yaml b/pkg/validation/variables/test-fixtures/projects/zip-output.yaml new file mode 100644 index 000000000..bfc61e30a --- /dev/null +++ b/pkg/validation/variables/test-fixtures/projects/zip-output.yaml @@ -0,0 +1,2 @@ +zip: + location: "/FOOBAR" diff --git a/pkg/validation/variables/test-fixtures/projects/zip.yaml b/pkg/validation/variables/test-fixtures/projects/zip.yaml new file mode 100644 index 000000000..390c96ead --- /dev/null +++ b/pkg/validation/variables/test-fixtures/projects/zip.yaml @@ -0,0 +1,3 @@ +# Variables are defined in test-fixtures/variables/variables-referenced.yaml +zip: + location: "/{{foo}}{{bar}}" diff --git a/pkg/validation/variables/test-fixtures/variables/variables-notreferenced.yaml b/pkg/validation/variables/test-fixtures/variables/variables-notreferenced.yaml new file mode 100644 index 000000000..49fd05879 --- /dev/null +++ b/pkg/validation/variables/test-fixtures/variables/variables-notreferenced.yaml @@ -0,0 +1 @@ +abc: xyz diff --git a/pkg/validation/variables/test-fixtures/variables/variables-referenced.yaml b/pkg/validation/variables/test-fixtures/variables/variables-referenced.yaml new file mode 100644 index 000000000..7470c63f5 --- /dev/null +++ b/pkg/validation/variables/test-fixtures/variables/variables-referenced.yaml @@ -0,0 +1,8 @@ +tag: xyz +version: "1" +foo: FOO +bar: BAR +devnull: /dev/null +size: "1Gi" +uri: uri +inlined: inlined diff --git a/pkg/validation/variables/utils.go b/pkg/validation/variables/utils.go new file mode 100644 index 000000000..be047ee25 --- /dev/null +++ b/pkg/validation/variables/utils.go @@ -0,0 +1,10 @@ +package variables + +// checkForInvalidError checks for InvalidKeysError and stores the key in the map +func checkForInvalidError(invalidKeys map[string]bool, err error) { + if verr, ok := err.(*InvalidKeysError); ok { + for _, key := range verr.Keys { + invalidKeys[key] = true + } + } +} diff --git a/pkg/validation/variables/utils_test.go b/pkg/validation/variables/utils_test.go new file mode 100644 index 000000000..a77d25f3d --- /dev/null +++ b/pkg/validation/variables/utils_test.go @@ -0,0 +1,76 @@ +package variables + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestCheckForInvalidError(t *testing.T) { + + tests := []struct { + name string + wantInvalidKeys map[string]bool + err error + }{ + { + name: "No error", + wantInvalidKeys: make(map[string]bool), + err: nil, + }, + { + name: "Different error", + wantInvalidKeys: make(map[string]bool), + err: fmt.Errorf("an error"), + }, + { + name: "InvalidKeysError error", + wantInvalidKeys: map[string]bool{ + "key1": true, + "key2": true, + }, + err: &InvalidKeysError{Keys: []string{"key1", "key2"}}, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testMap := make(map[string]bool) + checkForInvalidError(testMap, tt.err) + assert.Equal(t, tt.wantInvalidKeys, testMap, "the key map should be the same") + }) + } +} + +func TestNewInvalidKeysError(t *testing.T) { + + tests := []struct { + name string + invalidKeys map[string]bool + wantErr bool + }{ + { + name: "No invalid keys", + invalidKeys: make(map[string]bool), + wantErr: false, + }, + { + name: "InvalidKeysError error", + invalidKeys: map[string]bool{ + "key1": true, + "key2": true, + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := newInvalidKeysError(tt.invalidKeys) + if tt.wantErr && err == nil { + t.Errorf("Expected error from test but got nil") + } else if !tt.wantErr && err != nil { + t.Errorf("Got unexpected error: %s", err) + } + }) + } +} diff --git a/pkg/validation/variables/variables.go b/pkg/validation/variables/variables.go new file mode 100644 index 000000000..0de2f0d25 --- /dev/null +++ b/pkg/validation/variables/variables.go @@ -0,0 +1,68 @@ +package variables + +import ( + "regexp" + "strings" + + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" +) + +var globalVariableRegex = regexp.MustCompile(`\{\{(.*?)\}\}`) + +// VariableWarning stores the invalid variable references for each devfile object +type VariableWarning struct { + // Commands stores a map of command ids to the invalid variable references + Commands map[string][]string + + // Components stores a map of component names to the invalid variable references + Components map[string][]string + + // Projects stores a map of project names to the invalid variable references + Projects map[string][]string + + // StarterProjects stores a map of starter project names to the invalid variable references + StarterProjects map[string][]string +} + +// ValidateAndReplaceGlobalVariable validates the workspace template spec data for global variable references and replaces them with the variable value +func ValidateAndReplaceGlobalVariable(workspaceTemplateSpec *v1alpha2.DevWorkspaceTemplateSpec) VariableWarning { + + var variableWarning VariableWarning + + if workspaceTemplateSpec != nil { + // Validate the components and replace for global variable + variableWarning.Components = ValidateAndReplaceForComponents(workspaceTemplateSpec.Variables, workspaceTemplateSpec.Components) + + // Validate the commands and replace for global variable + variableWarning.Commands = ValidateAndReplaceForCommands(workspaceTemplateSpec.Variables, workspaceTemplateSpec.Commands) + + // Validate the projects and replace for global variable + variableWarning.Projects = ValidateAndReplaceForProjects(workspaceTemplateSpec.Variables, workspaceTemplateSpec.Projects) + + // Validate the starter projects and replace for global variable + variableWarning.StarterProjects = ValidateAndReplaceForStarterProjects(workspaceTemplateSpec.Variables, workspaceTemplateSpec.StarterProjects) + } + + return variableWarning +} + +// validateAndReplaceDataWithVariable validates the string for a global variable and replaces it. An error +// is returned if the string references an invalid global variable key +func validateAndReplaceDataWithVariable(val string, variables map[string]string) (string, error) { + matches := globalVariableRegex.FindAllStringSubmatch(val, -1) + var invalidKeys []string + for _, match := range matches { + varValue, ok := variables[match[1]] + if !ok { + invalidKeys = append(invalidKeys, match[1]) + } else { + val = strings.Replace(val, match[0], varValue, -1) + } + } + + if len(invalidKeys) > 0 { + return val, &InvalidKeysError{Keys: invalidKeys} + } + + return val, nil +} diff --git a/pkg/validation/variables/variables_command.go b/pkg/validation/variables/variables_command.go new file mode 100644 index 000000000..8e9c08679 --- /dev/null +++ b/pkg/validation/variables/variables_command.go @@ -0,0 +1,105 @@ +package variables + +import ( + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" +) + +// ValidateAndReplaceForCommands validates the commands data for global variable references and replaces them with the variable value. +// Returns a map of command ids and invalid variable references if present. +func ValidateAndReplaceForCommands(variables map[string]string, commands []v1alpha2.Command) map[string][]string { + + commandsWarningMap := make(map[string][]string) + + for i := range commands { + var err error + + // Validate various command types + switch { + case commands[i].Exec != nil: + if err = validateAndReplaceForExecCommand(variables, commands[i].Exec); err != nil { + if verr, ok := err.(*InvalidKeysError); ok { + commandsWarningMap[commands[i].Id] = verr.Keys + } + } + case commands[i].Composite != nil: + if err = validateAndReplaceForCompositeCommand(variables, commands[i].Composite); err != nil { + if verr, ok := err.(*InvalidKeysError); ok { + commandsWarningMap[commands[i].Id] = verr.Keys + } + } + case commands[i].Apply != nil: + if err = validateAndReplaceForApplyCommand(variables, commands[i].Apply); err != nil { + if verr, ok := err.(*InvalidKeysError); ok { + commandsWarningMap[commands[i].Id] = verr.Keys + } + } + } + } + + return commandsWarningMap +} + +// validateAndReplaceForExecCommand validates the exec command data for global variable references and replaces them with the variable value +func validateAndReplaceForExecCommand(variables map[string]string, exec *v1alpha2.ExecCommand) error { + var err error + + invalidKeys := make(map[string]bool) + + if exec != nil { + // Validate exec command line + if exec.CommandLine, err = validateAndReplaceDataWithVariable(exec.CommandLine, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + + // Validate exec working dir + if exec.WorkingDir, err = validateAndReplaceDataWithVariable(exec.WorkingDir, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + + // Validate exec label + if exec.Label, err = validateAndReplaceDataWithVariable(exec.Label, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + + // Validate exec env + if len(exec.Env) > 0 { + if err = validateAndReplaceForEnv(variables, exec.Env); err != nil { + checkForInvalidError(invalidKeys, err) + } + } + } + + return newInvalidKeysError(invalidKeys) +} + +// validateAndReplaceForCompositeCommand validates the composite command data for global variable references and replaces them with the variable value +func validateAndReplaceForCompositeCommand(variables map[string]string, composite *v1alpha2.CompositeCommand) error { + var err error + + invalidKeys := make(map[string]bool) + + if composite != nil { + // Validate composite label + if composite.Label, err = validateAndReplaceDataWithVariable(composite.Label, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + } + + return newInvalidKeysError(invalidKeys) +} + +// validateAndReplaceForApplyCommand validates the apply command data for global variable references and replaces them with the variable value +func validateAndReplaceForApplyCommand(variables map[string]string, apply *v1alpha2.ApplyCommand) error { + var err error + + invalidKeys := make(map[string]bool) + + if apply != nil { + // Validate apply label + if apply.Label, err = validateAndReplaceDataWithVariable(apply.Label, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + } + + return newInvalidKeysError(invalidKeys) +} diff --git a/pkg/validation/variables/variables_command_test.go b/pkg/validation/variables/variables_command_test.go new file mode 100644 index 000000000..d5f34ae18 --- /dev/null +++ b/pkg/validation/variables/variables_command_test.go @@ -0,0 +1,149 @@ +package variables + +import ( + "testing" + + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/stretchr/testify/assert" +) + +func TestValidateAndReplaceExecCommand(t *testing.T) { + + tests := []struct { + name string + testFile string + outputFile string + variableFile string + wantErr bool + }{ + { + name: "Good Substitution", + testFile: "test-fixtures/commands/exec.yaml", + outputFile: "test-fixtures/commands/exec-output.yaml", + variableFile: "test-fixtures/variables/variables-referenced.yaml", + wantErr: false, + }, + { + name: "Invalid Reference", + testFile: "test-fixtures/commands/exec.yaml", + outputFile: "test-fixtures/commands/exec.yaml", + variableFile: "test-fixtures/variables/variables-notreferenced.yaml", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testExecCommand := v1alpha2.ExecCommand{} + readFileToStruct(t, tt.testFile, &testExecCommand) + + testVariable := make(map[string]string) + readFileToStruct(t, tt.variableFile, &testVariable) + + err := validateAndReplaceForExecCommand(testVariable, &testExecCommand) + _, ok := err.(*InvalidKeysError) + if tt.wantErr && !ok { + t.Errorf("Expected InvalidKeysError error from test but got %+v", err) + } else if !tt.wantErr && err != nil { + t.Errorf("Got unexpected error: %s", err) + } else { + expectedExecCommand := v1alpha2.ExecCommand{} + readFileToStruct(t, tt.outputFile, &expectedExecCommand) + assert.Equal(t, expectedExecCommand, testExecCommand, "The two values should be the same.") + } + }) + } +} + +func TestValidateAndReplaceCompositeCommand(t *testing.T) { + + tests := []struct { + name string + testFile string + outputFile string + variableFile string + wantErr bool + }{ + { + name: "Good Substitution", + testFile: "test-fixtures/commands/composite.yaml", + outputFile: "test-fixtures/commands/composite-output.yaml", + variableFile: "test-fixtures/variables/variables-referenced.yaml", + wantErr: false, + }, + { + name: "Invalid Reference", + testFile: "test-fixtures/commands/composite.yaml", + outputFile: "test-fixtures/commands/composite.yaml", + variableFile: "test-fixtures/variables/variables-notreferenced.yaml", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testCompositeCommand := v1alpha2.CompositeCommand{} + readFileToStruct(t, tt.testFile, &testCompositeCommand) + + testVariable := make(map[string]string) + readFileToStruct(t, tt.variableFile, &testVariable) + + err := validateAndReplaceForCompositeCommand(testVariable, &testCompositeCommand) + _, ok := err.(*InvalidKeysError) + if tt.wantErr && !ok { + t.Errorf("Expected InvalidKeysError error from test but got %+v", err) + } else if !tt.wantErr && err != nil { + t.Errorf("Got unexpected error: %s", err) + } else { + expectedCompositeCommand := v1alpha2.CompositeCommand{} + readFileToStruct(t, tt.outputFile, &expectedCompositeCommand) + assert.Equal(t, expectedCompositeCommand, testCompositeCommand, "The two values should be the same.") + } + }) + } +} + +func TestValidateAndReplaceApplyCommand(t *testing.T) { + + tests := []struct { + name string + testFile string + outputFile string + variableFile string + wantErr bool + }{ + { + name: "Good Substitution", + testFile: "test-fixtures/commands/apply.yaml", + outputFile: "test-fixtures/commands/apply-output.yaml", + variableFile: "test-fixtures/variables/variables-referenced.yaml", + wantErr: false, + }, + { + name: "Invalid Reference", + testFile: "test-fixtures/commands/apply.yaml", + outputFile: "test-fixtures/commands/apply.yaml", + variableFile: "test-fixtures/variables/variables-notreferenced.yaml", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testApplyCommand := v1alpha2.ApplyCommand{} + readFileToStruct(t, tt.testFile, &testApplyCommand) + + testVariable := make(map[string]string) + readFileToStruct(t, tt.variableFile, &testVariable) + + err := validateAndReplaceForApplyCommand(testVariable, &testApplyCommand) + _, ok := err.(*InvalidKeysError) + if tt.wantErr && !ok { + t.Errorf("Expected InvalidKeysError error from test but got %+v", err) + } else if !tt.wantErr && err != nil { + t.Errorf("Got unexpected error: %s", err) + } else { + expectedApplyCommand := v1alpha2.ApplyCommand{} + readFileToStruct(t, tt.outputFile, &expectedApplyCommand) + assert.Equal(t, expectedApplyCommand, testApplyCommand, "The two values should be the same.") + } + }) + } +} diff --git a/pkg/validation/variables/variables_component.go b/pkg/validation/variables/variables_component.go new file mode 100644 index 000000000..ce5a3f1bc --- /dev/null +++ b/pkg/validation/variables/variables_component.go @@ -0,0 +1,206 @@ +package variables + +import ( + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" +) + +// ValidateAndReplaceForComponents validates the components data for global variable references and replaces them with the variable value +// Returns a map of component names and invalid variable references if present. +func ValidateAndReplaceForComponents(variables map[string]string, components []v1alpha2.Component) map[string][]string { + + componentsWarningMap := make(map[string][]string) + + for i := range components { + var err error + + // Validate various component types + switch { + case components[i].Container != nil: + if err = validateAndReplaceForContainerComponent(variables, components[i].Container); err != nil { + if verr, ok := err.(*InvalidKeysError); ok { + componentsWarningMap[components[i].Name] = verr.Keys + } + } + case components[i].Kubernetes != nil: + if err = validateAndReplaceForKubernetesComponent(variables, components[i].Kubernetes); err != nil { + if verr, ok := err.(*InvalidKeysError); ok { + componentsWarningMap[components[i].Name] = verr.Keys + } + } + case components[i].Openshift != nil: + if err = validateAndReplaceForOpenShiftComponent(variables, components[i].Openshift); err != nil { + if verr, ok := err.(*InvalidKeysError); ok { + componentsWarningMap[components[i].Name] = verr.Keys + } + } + case components[i].Volume != nil: + if err = validateAndReplaceForVolumeComponent(variables, components[i].Volume); err != nil { + if verr, ok := err.(*InvalidKeysError); ok { + componentsWarningMap[components[i].Name] = verr.Keys + } + } + } + } + + return componentsWarningMap +} + +// validateAndReplaceForContainerComponent validates the container component data for global variable references and replaces them with the variable value +func validateAndReplaceForContainerComponent(variables map[string]string, container *v1alpha2.ContainerComponent) error { + var err error + + invalidKeys := make(map[string]bool) + + if container != nil { + // Validate container image + if container.Image, err = validateAndReplaceDataWithVariable(container.Image, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + + // Validate container commands + for i := range container.Command { + if container.Command[i], err = validateAndReplaceDataWithVariable(container.Command[i], variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + } + + // Validate container args + for i := range container.Args { + if container.Args[i], err = validateAndReplaceDataWithVariable(container.Args[i], variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + } + + // Validate memory limit + if container.MemoryLimit, err = validateAndReplaceDataWithVariable(container.MemoryLimit, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + + // Validate memory request + if container.MemoryRequest, err = validateAndReplaceDataWithVariable(container.MemoryRequest, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + + // Validate source mapping + if container.SourceMapping, err = validateAndReplaceDataWithVariable(container.SourceMapping, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + + // Validate container env + if len(container.Env) > 0 { + if err = validateAndReplaceForEnv(variables, container.Env); err != nil { + checkForInvalidError(invalidKeys, err) + } + } + + // Validate container volume mounts + for i := range container.VolumeMounts { + if container.VolumeMounts[i].Path, err = validateAndReplaceDataWithVariable(container.VolumeMounts[i].Path, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + } + + // Validate container endpoints + if len(container.Endpoints) > 0 { + if err = validateAndReplaceForEndpoint(variables, container.Endpoints); err != nil { + checkForInvalidError(invalidKeys, err) + } + } + } + + return newInvalidKeysError(invalidKeys) +} + +// validateAndReplaceForEnv validates the env data for global variable references and replaces them with the variable value +func validateAndReplaceForEnv(variables map[string]string, env []v1alpha2.EnvVar) error { + + invalidKeys := make(map[string]bool) + + for i := range env { + var err error + + // Validate env name + if env[i].Name, err = validateAndReplaceDataWithVariable(env[i].Name, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + + // Validate env value + if env[i].Value, err = validateAndReplaceDataWithVariable(env[i].Value, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + } + + return newInvalidKeysError(invalidKeys) +} + +// validateAndReplaceForKubernetesComponent validates the kubernetes component data for global variable references and replaces them with the variable value +func validateAndReplaceForKubernetesComponent(variables map[string]string, kubernetes *v1alpha2.KubernetesComponent) error { + var err error + + invalidKeys := make(map[string]bool) + + if kubernetes != nil { + // Validate kubernetes uri + if kubernetes.Uri, err = validateAndReplaceDataWithVariable(kubernetes.Uri, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + + // Validate kubernetes inlined + if kubernetes.Inlined, err = validateAndReplaceDataWithVariable(kubernetes.Inlined, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + + // Validate kubernetes endpoints + if len(kubernetes.Endpoints) > 0 { + if err = validateAndReplaceForEndpoint(variables, kubernetes.Endpoints); err != nil { + checkForInvalidError(invalidKeys, err) + } + } + } + + return newInvalidKeysError(invalidKeys) +} + +// validateAndReplaceForOpenShiftComponent validates the openshift component data for global variable references and replaces them with the variable value +func validateAndReplaceForOpenShiftComponent(variables map[string]string, openshift *v1alpha2.OpenshiftComponent) error { + var err error + + invalidKeys := make(map[string]bool) + + if openshift != nil { + // Validate openshift uri + if openshift.Uri, err = validateAndReplaceDataWithVariable(openshift.Uri, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + + // Validate openshift inlined + if openshift.Inlined, err = validateAndReplaceDataWithVariable(openshift.Inlined, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + + // Validate openshift endpoints + if len(openshift.Endpoints) > 0 { + if err = validateAndReplaceForEndpoint(variables, openshift.Endpoints); err != nil { + checkForInvalidError(invalidKeys, err) + } + } + } + + return newInvalidKeysError(invalidKeys) +} + +// validateAndReplaceForVolumeComponent validates the volume component data for global variable references and replaces them with the variable value +func validateAndReplaceForVolumeComponent(variables map[string]string, volume *v1alpha2.VolumeComponent) error { + var err error + + invalidKeys := make(map[string]bool) + + if volume != nil { + // Validate volume size + if volume.Size, err = validateAndReplaceDataWithVariable(volume.Size, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + } + + return newInvalidKeysError(invalidKeys) +} diff --git a/pkg/validation/variables/variables_component_test.go b/pkg/validation/variables/variables_component_test.go new file mode 100644 index 000000000..c972c8888 --- /dev/null +++ b/pkg/validation/variables/variables_component_test.go @@ -0,0 +1,212 @@ +package variables + +import ( + "testing" + + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/stretchr/testify/assert" +) + +func TestValidateAndReplaceContainerComponent(t *testing.T) { + + tests := []struct { + name string + testFile string + outputFile string + variableFile string + wantErr bool + }{ + { + name: "Good Substitution", + testFile: "test-fixtures/components/container.yaml", + outputFile: "test-fixtures/components/container-output.yaml", + variableFile: "test-fixtures/variables/variables-referenced.yaml", + wantErr: false, + }, + { + name: "Invalid Reference", + testFile: "test-fixtures/components/container.yaml", + outputFile: "test-fixtures/components/container.yaml", + variableFile: "test-fixtures/variables/variables-notreferenced.yaml", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testContainerComponent := v1alpha2.ContainerComponent{} + readFileToStruct(t, tt.testFile, &testContainerComponent) + + testVariable := make(map[string]string) + readFileToStruct(t, tt.variableFile, &testVariable) + + err := validateAndReplaceForContainerComponent(testVariable, &testContainerComponent) + _, ok := err.(*InvalidKeysError) + if tt.wantErr && !ok { + t.Errorf("Expected InvalidKeysError error from test but got %+v", err) + } else if !tt.wantErr && err != nil { + t.Errorf("Got unexpected error: %s", err) + } else { + expectedContainerComponent := v1alpha2.ContainerComponent{} + readFileToStruct(t, tt.outputFile, &expectedContainerComponent) + assert.Equal(t, expectedContainerComponent, testContainerComponent, "The two values should be the same.") + } + }) + } +} + +func TestValidateAndReplaceOpenShiftKubernetesComponent(t *testing.T) { + + tests := []struct { + name string + testFile string + outputFile string + variableFile string + wantErr bool + }{ + { + name: "Good Substitution", + testFile: "test-fixtures/components/openshift-kubernetes.yaml", + outputFile: "test-fixtures/components/openshift-kubernetes-output.yaml", + variableFile: "test-fixtures/variables/variables-referenced.yaml", + wantErr: false, + }, + { + name: "Invalid Reference", + testFile: "test-fixtures/components/openshift-kubernetes.yaml", + outputFile: "test-fixtures/components/openshift-kubernetes.yaml", + variableFile: "test-fixtures/variables/variables-notreferenced.yaml", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testOpenshiftComponent := v1alpha2.OpenshiftComponent{} + testKubernetesComponent := v1alpha2.KubernetesComponent{} + + readFileToStruct(t, tt.testFile, &testOpenshiftComponent) + readFileToStruct(t, tt.testFile, &testKubernetesComponent) + + testVariable := make(map[string]string) + readFileToStruct(t, tt.variableFile, &testVariable) + + err := validateAndReplaceForOpenShiftComponent(testVariable, &testOpenshiftComponent) + if tt.wantErr && err == nil { + t.Errorf("Expected error from test but got nil") + } else if !tt.wantErr && err != nil { + t.Errorf("Got unexpected error: %s", err) + } else if err == nil { + expectedOpenshiftComponent := v1alpha2.OpenshiftComponent{} + readFileToStruct(t, tt.outputFile, &expectedOpenshiftComponent) + assert.Equal(t, expectedOpenshiftComponent, testOpenshiftComponent, "The two values should be the same.") + } + + err = validateAndReplaceForKubernetesComponent(testVariable, &testKubernetesComponent) + _, ok := err.(*InvalidKeysError) + if tt.wantErr && !ok { + t.Errorf("Expected InvalidKeysError error from test but got %+v", err) + } else if !tt.wantErr && err != nil { + t.Errorf("Got unexpected error: %s", err) + } else { + expectedKubernetesComponent := v1alpha2.KubernetesComponent{} + readFileToStruct(t, tt.outputFile, &expectedKubernetesComponent) + assert.Equal(t, expectedKubernetesComponent, testKubernetesComponent, "The two values should be the same.") + } + }) + } +} + +func TestValidateAndReplaceVolumeComponent(t *testing.T) { + + tests := []struct { + name string + testFile string + outputFile string + variableFile string + wantErr bool + }{ + { + name: "Good Substitution", + testFile: "test-fixtures/components/volume.yaml", + outputFile: "test-fixtures/components/volume-output.yaml", + variableFile: "test-fixtures/variables/variables-referenced.yaml", + wantErr: false, + }, + { + name: "Invalid Reference", + testFile: "test-fixtures/components/volume.yaml", + outputFile: "test-fixtures/components/volume.yaml", + variableFile: "test-fixtures/variables/variables-notreferenced.yaml", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testVolumeComponent := v1alpha2.VolumeComponent{} + readFileToStruct(t, tt.testFile, &testVolumeComponent) + + testVariable := make(map[string]string) + readFileToStruct(t, tt.variableFile, &testVariable) + + err := validateAndReplaceForVolumeComponent(testVariable, &testVolumeComponent) + _, ok := err.(*InvalidKeysError) + if tt.wantErr && !ok { + t.Errorf("Expected InvalidKeysError error from test but got %+v", err) + } else if !tt.wantErr && err != nil { + t.Errorf("Got unexpected error: %s", err) + } else { + expectedVolumeComponent := v1alpha2.VolumeComponent{} + readFileToStruct(t, tt.outputFile, &expectedVolumeComponent) + assert.Equal(t, expectedVolumeComponent, testVolumeComponent, "The two values should be the same.") + } + }) + } +} + +func TestValidateAndReplaceEnv(t *testing.T) { + + tests := []struct { + name string + testFile string + outputFile string + variableFile string + wantErr bool + }{ + { + name: "Good Substitution", + testFile: "test-fixtures/components/env.yaml", + outputFile: "test-fixtures/components/env-output.yaml", + variableFile: "test-fixtures/variables/variables-referenced.yaml", + wantErr: false, + }, + { + name: "Invalid Reference", + testFile: "test-fixtures/components/env.yaml", + outputFile: "test-fixtures/components/env.yaml", + variableFile: "test-fixtures/variables/variables-notreferenced.yaml", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testEnv := v1alpha2.EnvVar{} + readFileToStruct(t, tt.testFile, &testEnv) + testEnvArr := []v1alpha2.EnvVar{testEnv} + + testVariable := make(map[string]string) + readFileToStruct(t, tt.variableFile, &testVariable) + + err := validateAndReplaceForEnv(testVariable, testEnvArr) + _, ok := err.(*InvalidKeysError) + if tt.wantErr && !ok { + t.Errorf("Expected InvalidKeysError error from test but got %+v", err) + } else if !tt.wantErr && err != nil { + t.Errorf("Got unexpected error: %s", err) + } else { + expectedEnv := v1alpha2.EnvVar{} + readFileToStruct(t, tt.outputFile, &expectedEnv) + expectedEnvArr := []v1alpha2.EnvVar{expectedEnv} + assert.Equal(t, expectedEnvArr, testEnvArr, "The two values should be the same.") + } + }) + } +} diff --git a/pkg/validation/variables/variables_endpoint.go b/pkg/validation/variables/variables_endpoint.go new file mode 100644 index 000000000..17a1e2ed0 --- /dev/null +++ b/pkg/validation/variables/variables_endpoint.go @@ -0,0 +1,22 @@ +package variables + +import ( + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" +) + +// validateAndReplaceForEndpoint validates the endpoint data for global variable references and replaces them with the variable value +func validateAndReplaceForEndpoint(variables map[string]string, endpoints []v1alpha2.Endpoint) error { + + invalidKeys := make(map[string]bool) + + for i := range endpoints { + var err error + + // Validate endpoint path + if endpoints[i].Path, err = validateAndReplaceDataWithVariable(endpoints[i].Path, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + } + + return newInvalidKeysError(invalidKeys) +} diff --git a/pkg/validation/variables/variables_endpoint_test.go b/pkg/validation/variables/variables_endpoint_test.go new file mode 100644 index 000000000..42e157eb7 --- /dev/null +++ b/pkg/validation/variables/variables_endpoint_test.go @@ -0,0 +1,57 @@ +package variables + +import ( + "testing" + + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/stretchr/testify/assert" +) + +func TestValidateAndReplaceEndpoint(t *testing.T) { + + tests := []struct { + name string + testFile string + outputFile string + variableFile string + wantErr bool + }{ + { + name: "Good Substitution", + testFile: "test-fixtures/components/endpoint.yaml", + outputFile: "test-fixtures/components/endpoint-output.yaml", + variableFile: "test-fixtures/variables/variables-referenced.yaml", + wantErr: false, + }, + { + name: "Invalid Reference", + testFile: "test-fixtures/components/endpoint.yaml", + outputFile: "test-fixtures/components/endpoint.yaml", + variableFile: "test-fixtures/variables/variables-notreferenced.yaml", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testEndpoint := v1alpha2.Endpoint{} + readFileToStruct(t, tt.testFile, &testEndpoint) + testEndpointArr := []v1alpha2.Endpoint{testEndpoint} + + testVariable := make(map[string]string) + readFileToStruct(t, tt.variableFile, &testVariable) + + err := validateAndReplaceForEndpoint(testVariable, testEndpointArr) + _, ok := err.(*InvalidKeysError) + if tt.wantErr && !ok { + t.Errorf("Expected InvalidKeysError error from test but got %+v", err) + } else if !tt.wantErr && err != nil { + t.Errorf("Got unexpected error: %s", err) + } else { + expectedEndpoint := v1alpha2.Endpoint{} + readFileToStruct(t, tt.outputFile, &expectedEndpoint) + expectedEndpointArr := []v1alpha2.Endpoint{expectedEndpoint} + assert.Equal(t, expectedEndpointArr, testEndpointArr, "The two values should be the same.") + } + }) + } +} diff --git a/pkg/validation/variables/variables_project.go b/pkg/validation/variables/variables_project.go new file mode 100644 index 000000000..136e7ec1a --- /dev/null +++ b/pkg/validation/variables/variables_project.go @@ -0,0 +1,132 @@ +package variables + +import ( + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" +) + +// ValidateAndReplaceForProjects validates the projects data for global variable references and replaces them with the variable value. +// Returns a map of project names and invalid variable references if present. +func ValidateAndReplaceForProjects(variables map[string]string, projects []v1alpha2.Project) map[string][]string { + + projectsWarningMap := make(map[string][]string) + + for i := range projects { + var err error + + invalidKeys := make(map[string]bool) + + // Validate project clonepath + if projects[i].ClonePath, err = validateAndReplaceDataWithVariable(projects[i].ClonePath, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + + // Validate project sparse checkout dir + for j := range projects[i].SparseCheckoutDirs { + if projects[i].SparseCheckoutDirs[j], err = validateAndReplaceDataWithVariable(projects[i].SparseCheckoutDirs[j], variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + } + + // Validate project source + if err = validateandReplaceForProjectSource(variables, &projects[i].ProjectSource); err != nil { + checkForInvalidError(invalidKeys, err) + } + + err = newInvalidKeysError(invalidKeys) + if verr, ok := err.(*InvalidKeysError); ok { + projectsWarningMap[projects[i].Name] = verr.Keys + } + } + + return projectsWarningMap +} + +// ValidateAndReplaceForStarterProjects validates the starter projects data for global variable references and replaces them with the variable value. +// Returns a map of starter project names and invalid variable references if present. +func ValidateAndReplaceForStarterProjects(variables map[string]string, starterProjects []v1alpha2.StarterProject) map[string][]string { + + starterProjectsWarningMap := make(map[string][]string) + + for i := range starterProjects { + var err error + + invalidKeys := make(map[string]bool) + + // Validate starter project description + if starterProjects[i].Description, err = validateAndReplaceDataWithVariable(starterProjects[i].Description, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + + // Validate starter project sub dir + if starterProjects[i].SubDir, err = validateAndReplaceDataWithVariable(starterProjects[i].SubDir, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + + // Validate starter project source + if err = validateandReplaceForProjectSource(variables, &starterProjects[i].ProjectSource); err != nil { + checkForInvalidError(invalidKeys, err) + } + + err = newInvalidKeysError(invalidKeys) + if verr, ok := err.(*InvalidKeysError); ok { + starterProjectsWarningMap[starterProjects[i].Name] = verr.Keys + } + } + + return starterProjectsWarningMap +} + +// validateandReplaceForProjectSource validates a project source location for global variable references and replaces them with the variable value +func validateandReplaceForProjectSource(variables map[string]string, projectSource *v1alpha2.ProjectSource) error { + + var err error + + invalidKeys := make(map[string]bool) + + if projectSource != nil { + switch { + case projectSource.Zip != nil: + if projectSource.Zip.Location, err = validateAndReplaceDataWithVariable(projectSource.Zip.Location, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + case projectSource.Git != nil || projectSource.Github != nil: + var gitProject *v1alpha2.GitLikeProjectSource + if projectSource.Git != nil { + gitProject = &projectSource.Git.GitLikeProjectSource + } else if projectSource.Github != nil { + gitProject = &projectSource.Github.GitLikeProjectSource + } + + if gitProject.CheckoutFrom != nil { + // validate git checkout revision + if gitProject.CheckoutFrom.Revision, err = validateAndReplaceDataWithVariable(gitProject.CheckoutFrom.Revision, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + + // // validate git checkout remote + if gitProject.CheckoutFrom.Remote, err = validateAndReplaceDataWithVariable(gitProject.CheckoutFrom.Remote, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + } + + // validate git remotes + for k := range gitProject.Remotes { + // validate remote map value + if gitProject.Remotes[k], err = validateAndReplaceDataWithVariable(gitProject.Remotes[k], variables); err != nil { + checkForInvalidError(invalidKeys, err) + } + + // validate remote map key + var updatedKey string + if updatedKey, err = validateAndReplaceDataWithVariable(k, variables); err != nil { + checkForInvalidError(invalidKeys, err) + } else if updatedKey != k { + gitProject.Remotes[updatedKey] = gitProject.Remotes[k] + delete(gitProject.Remotes, k) + } + } + } + } + + return newInvalidKeysError(invalidKeys) +} diff --git a/pkg/validation/variables/variables_project_test.go b/pkg/validation/variables/variables_project_test.go new file mode 100644 index 000000000..ca932fa57 --- /dev/null +++ b/pkg/validation/variables/variables_project_test.go @@ -0,0 +1,150 @@ +package variables + +import ( + "testing" + + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/stretchr/testify/assert" +) + +func TestValidateAndReplaceProjects(t *testing.T) { + + tests := []struct { + name string + testFile string + outputFile string + variableFile string + wantErr bool + }{ + { + name: "Good Substitution", + testFile: "test-fixtures/projects/project.yaml", + outputFile: "test-fixtures/projects/project-output.yaml", + variableFile: "test-fixtures/variables/variables-referenced.yaml", + wantErr: false, + }, + { + name: "Invalid Reference", + testFile: "test-fixtures/projects/project.yaml", + outputFile: "test-fixtures/projects/project.yaml", + variableFile: "test-fixtures/variables/variables-notreferenced.yaml", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testProject := v1alpha2.Project{} + readFileToStruct(t, tt.testFile, &testProject) + testProjectArr := []v1alpha2.Project{testProject} + + testVariable := make(map[string]string) + readFileToStruct(t, tt.variableFile, &testVariable) + + ValidateAndReplaceForProjects(testVariable, testProjectArr) + expectedProject := v1alpha2.Project{} + readFileToStruct(t, tt.outputFile, &expectedProject) + expectedProjectArr := []v1alpha2.Project{expectedProject} + assert.Equal(t, expectedProjectArr, testProjectArr, "The two values should be the same.") + }) + } +} + +func TestValidateAndReplaceStarterProjects(t *testing.T) { + + tests := []struct { + name string + testFile string + outputFile string + variableFile string + }{ + { + name: "Good Substitution", + testFile: "test-fixtures/projects/starterproject.yaml", + outputFile: "test-fixtures/projects/starterproject-output.yaml", + variableFile: "test-fixtures/variables/variables-referenced.yaml", + }, + { + name: "Invalid Reference", + testFile: "test-fixtures/projects/starterproject.yaml", + outputFile: "test-fixtures/projects/starterproject.yaml", + variableFile: "test-fixtures/variables/variables-notreferenced.yaml", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testStarterProject := v1alpha2.StarterProject{} + readFileToStruct(t, tt.testFile, &testStarterProject) + testStarterProjectArr := []v1alpha2.StarterProject{testStarterProject} + + testVariable := make(map[string]string) + readFileToStruct(t, tt.variableFile, &testVariable) + + ValidateAndReplaceForStarterProjects(testVariable, testStarterProjectArr) + expectedStarterProject := v1alpha2.StarterProject{} + readFileToStruct(t, tt.outputFile, &expectedStarterProject) + expectedStarterProjectArr := []v1alpha2.StarterProject{expectedStarterProject} + assert.Equal(t, expectedStarterProjectArr, testStarterProjectArr, "The two values should be the same.") + }) + } +} + +func TestValidateAndReplaceProjectSrc(t *testing.T) { + + tests := []struct { + name string + testFile string + outputFile string + variableFile string + wantErr bool + }{ + { + name: "Good Git Substitution", + testFile: "test-fixtures/projects/git.yaml", + outputFile: "test-fixtures/projects/git-output.yaml", + variableFile: "test-fixtures/variables/variables-referenced.yaml", + wantErr: false, + }, + { + name: "Good Zip Substitution", + testFile: "test-fixtures/projects/zip.yaml", + outputFile: "test-fixtures/projects/zip-output.yaml", + variableFile: "test-fixtures/variables/variables-referenced.yaml", + wantErr: false, + }, + { + name: "Invalid Git Reference", + testFile: "test-fixtures/projects/git.yaml", + outputFile: "test-fixtures/projects/git.yaml", + variableFile: "test-fixtures/variables/variables-notreferenced.yaml", + wantErr: true, + }, + { + name: "Invalid Zip Reference", + testFile: "test-fixtures/projects/zip.yaml", + outputFile: "test-fixtures/projects/zip.yaml", + variableFile: "test-fixtures/variables/variables-notreferenced.yaml", + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testProjectSrc := v1alpha2.ProjectSource{} + readFileToStruct(t, tt.testFile, &testProjectSrc) + + testVariable := make(map[string]string) + readFileToStruct(t, tt.variableFile, &testVariable) + + err := validateandReplaceForProjectSource(testVariable, &testProjectSrc) + _, ok := err.(*InvalidKeysError) + if tt.wantErr && !ok { + t.Errorf("Expected InvalidKeysError error from test but got %+v", err) + } else if !tt.wantErr && err != nil { + t.Errorf("Got unexpected error: %s", err) + } else { + expectedProjectSrc := v1alpha2.ProjectSource{} + readFileToStruct(t, tt.outputFile, &expectedProjectSrc) + assert.Equal(t, expectedProjectSrc, testProjectSrc, "The two values should be the same.") + } + }) + } +} diff --git a/pkg/validation/variables/variables_test.go b/pkg/validation/variables/variables_test.go new file mode 100644 index 000000000..6aa1936dd --- /dev/null +++ b/pkg/validation/variables/variables_test.go @@ -0,0 +1,164 @@ +package variables + +import ( + "io/ioutil" + "reflect" + "testing" + + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/stretchr/testify/assert" + "sigs.k8s.io/yaml" +) + +func TestValidateGlobalVariableBasic(t *testing.T) { + + tests := []struct { + name string + testFile string + outputFile string + wantWarning VariableWarning + }{ + { + name: "Successful global variable substitution", + testFile: "test-fixtures/all/devfile-good.yaml", + outputFile: "test-fixtures/all/devfile-good-output.yaml", + wantWarning: VariableWarning{}, + }, + { + name: "Invalid Reference", + testFile: "test-fixtures/all/devfile-bad.yaml", + outputFile: "test-fixtures/all/devfile-bad-output.yaml", + wantWarning: VariableWarning{ + Commands: map[string][]string{ + "command1": {"BAR", "tag"}, + "command2": {"abc"}, + "command3": {"abc"}, + }, + Components: map[string][]string{ + "component1": {"a", "b", "bar", "c"}, + "component2": {"bar", "foo", "x"}, + "component3": {"xyz"}, + "component4": {"foo"}, + }, + Projects: map[string][]string{ + "project1": {"dir", "path", "tag", "version", "version1"}, + "project2": {"tag"}, + }, + StarterProjects: map[string][]string{ + "starterproject1": {"desc", "dir", "tag"}, + "starterproject2": {"tag"}, + }, + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testDWT := v1alpha2.DevWorkspaceTemplateSpec{} + readFileToStruct(t, tt.testFile, &testDWT) + + warning := ValidateAndReplaceGlobalVariable(&testDWT) + + expectedDWT := v1alpha2.DevWorkspaceTemplateSpec{} + readFileToStruct(t, tt.outputFile, &expectedDWT) + assert.Equal(t, expectedDWT, testDWT, "The two values should be the same.") + + // match the warning + if !reflect.DeepEqual(tt.wantWarning, VariableWarning{}) { + // commands + for gotCommand, gotInvalidVars := range warning.Commands { + if wantInvalidVars, ok := tt.wantWarning.Commands[gotCommand]; !ok { + t.Errorf("unexpected command %s found in the warning", gotCommand) + } else { + assert.Equal(t, wantInvalidVars, gotInvalidVars, "the invalid keys should be the same") + } + } + + // components + for gotComponent, gotInvalidVars := range warning.Components { + if wantInvalidVars, ok := tt.wantWarning.Components[gotComponent]; !ok { + t.Errorf("unexpected component %s found in the warning", gotComponent) + } else { + assert.Equal(t, wantInvalidVars, gotInvalidVars, "the invalid keys should be the same") + } + } + + // projects + for gotProject, gotInvalidVars := range warning.Projects { + if wantInvalidVars, ok := tt.wantWarning.Projects[gotProject]; !ok { + t.Errorf("unexpected project %s found in the warning", gotProject) + } else { + assert.Equal(t, wantInvalidVars, gotInvalidVars, "the invalid keys should be the same") + } + } + + // starter projects + for gotStarterProject, gotInvalidVars := range warning.StarterProjects { + if wantInvalidVars, ok := tt.wantWarning.StarterProjects[gotStarterProject]; !ok { + t.Errorf("unexpected starter project %s found in the warning", gotStarterProject) + } else { + assert.Equal(t, wantInvalidVars, gotInvalidVars, "the invalid keys should be the same") + } + } + } + }) + } +} + +func TestValidateAndReplaceDataWithVariable(t *testing.T) { + + invalidVariableErr := ".*invalid variable references.*" + + tests := []struct { + name string + testString string + variables map[string]string + wantValue string + wantErr *string + }{ + { + name: "Valid variable reference", + testString: "image-{{version}}:{{tag}}{{development}}-14", + variables: map[string]string{ + "version": "1.x.x", + "tag": "dev", + "development": "sandbox", + }, + wantValue: "image-1.x.x:devsandbox-14", + wantErr: nil, + }, + { + name: "Invalid variable reference", + testString: "image-{{version}}:{{tag}}{{invalid}}-14{{invalid}}", + variables: map[string]string{ + "version": "1.x.x", + "tag": "dev", + }, + wantValue: "image-1.x.x:dev{{invalid}}-14{{invald}}", + wantErr: &invalidVariableErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + gotValue, err := validateAndReplaceDataWithVariable(tt.testString, tt.variables) + if tt.wantErr != nil && assert.Error(t, err) { + assert.Regexp(t, *tt.wantErr, err.Error(), "Error message should match") + } else { + assert.NoError(t, err, "Expected error to be nil") + if gotValue != tt.wantValue { + assert.Equal(t, tt.wantValue, gotValue, "Return value should match") + } + } + }) + } +} + +func readFileToStruct(t *testing.T, path string, into interface{}) { + bytes, err := ioutil.ReadFile(path) + if err != nil { + t.Fatalf("Failed to read test file from %s: %s", path, err.Error()) + } + err = yaml.Unmarshal(bytes, into) + if err != nil { + t.Fatalf("Failed to unmarshal file into struct: %s", err.Error()) + } +} diff --git a/schemas/latest/dev-workspace-template-spec.json b/schemas/latest/dev-workspace-template-spec.json index 7052e3732..43bf992cc 100644 --- a/schemas/latest/dev-workspace-template-spec.json +++ b/schemas/latest/dev-workspace-template-spec.json @@ -3,6 +3,11 @@ "type": "object", "title": "DevWorkspaceTemplateSpec schema - Version 2.1.0-alpha", "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, "commands": { "description": "Predefined, ready-to-use, devworkspace-related commands", "type": "array", @@ -1334,6 +1339,11 @@ } ], "properties": { + "attributes": { + "description": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": true + }, "commands": { "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", "type": "array", @@ -2762,6 +2772,13 @@ "uri": { "description": "Uri of a Devfile yaml file", "type": "string" + }, + "variables": { + "description": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": { + "type": "string" + } } }, "additionalProperties": false @@ -3062,6 +3079,13 @@ }, "additionalProperties": false } + }, + "variables": { + "description": "Map of key-value variables used for string replacement in the devfile. Values can can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source - element identifiers, e.g. command id, component name, endpoint name, project name - references to identifiers, e.g. in events, a command's component, container's volume mount name - string enums, e.g. command group kind, endpoint exposure", + "type": "object", + "additionalProperties": { + "type": "string" + } } }, "additionalProperties": false diff --git a/schemas/latest/dev-workspace-template.json b/schemas/latest/dev-workspace-template.json index 82b985460..3366199a8 100644 --- a/schemas/latest/dev-workspace-template.json +++ b/schemas/latest/dev-workspace-template.json @@ -168,6 +168,11 @@ "description": "Structure of the devworkspace. This is also the specification of a devworkspace template.", "type": "object", "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, "commands": { "description": "Predefined, ready-to-use, devworkspace-related commands", "type": "array", @@ -1499,6 +1504,11 @@ } ], "properties": { + "attributes": { + "description": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": true + }, "commands": { "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", "type": "array", @@ -2927,6 +2937,13 @@ "uri": { "description": "Uri of a Devfile yaml file", "type": "string" + }, + "variables": { + "description": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": { + "type": "string" + } } }, "additionalProperties": false @@ -3227,6 +3244,13 @@ }, "additionalProperties": false } + }, + "variables": { + "description": "Map of key-value variables used for string replacement in the devfile. Values can can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source - element identifiers, e.g. command id, component name, endpoint name, project name - references to identifiers, e.g. in events, a command's component, container's volume mount name - string enums, e.g. command group kind, endpoint exposure", + "type": "object", + "additionalProperties": { + "type": "string" + } } }, "additionalProperties": false diff --git a/schemas/latest/dev-workspace.json b/schemas/latest/dev-workspace.json index aaa101f94..10c00aff0 100644 --- a/schemas/latest/dev-workspace.json +++ b/schemas/latest/dev-workspace.json @@ -181,6 +181,11 @@ "description": "Structure of the devworkspace. This is also the specification of a devworkspace template.", "type": "object", "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, "commands": { "description": "Predefined, ready-to-use, devworkspace-related commands", "type": "array", @@ -1512,6 +1517,11 @@ } ], "properties": { + "attributes": { + "description": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": true + }, "commands": { "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", "type": "array", @@ -2940,6 +2950,13 @@ "uri": { "description": "Uri of a Devfile yaml file", "type": "string" + }, + "variables": { + "description": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": { + "type": "string" + } } }, "additionalProperties": false @@ -3240,6 +3257,13 @@ }, "additionalProperties": false } + }, + "variables": { + "description": "Map of key-value variables used for string replacement in the devfile. Values can can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source - element identifiers, e.g. command id, component name, endpoint name, project name - references to identifiers, e.g. in events, a command's component, container's volume mount name - string enums, e.g. command group kind, endpoint exposure", + "type": "object", + "additionalProperties": { + "type": "string" + } } }, "additionalProperties": false diff --git a/schemas/latest/devfile.json b/schemas/latest/devfile.json index 126f52945..7cb0eb71e 100644 --- a/schemas/latest/devfile.json +++ b/schemas/latest/devfile.json @@ -6,6 +6,11 @@ "schemaVersion" ], "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true + }, "commands": { "description": "Predefined, ready-to-use, devworkspace-related commands", "type": "array", @@ -632,7 +637,7 @@ "type": "object", "properties": { "attributes": { - "description": "Map of implementation-dependant free-form YAML attributes.", + "description": "Map of implementation-dependant free-form YAML attributes. Deprecated, use the top-level attributes field instead.", "type": "object", "additionalProperties": true }, @@ -704,6 +709,11 @@ } ], "properties": { + "attributes": { + "description": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": true + }, "commands": { "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", "type": "array", @@ -1524,6 +1534,13 @@ "uri": { "description": "Uri of a Devfile yaml file", "type": "string" + }, + "variables": { + "description": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": { + "type": "string" + } } }, "additionalProperties": false @@ -1783,6 +1800,13 @@ }, "additionalProperties": false } + }, + "variables": { + "description": "Map of key-value variables used for string replacement in the devfile. Values can can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source - element identifiers, e.g. command id, component name, endpoint name, project name - references to identifiers, e.g. in events, a command's component, container's volume mount name - string enums, e.g. command group kind, endpoint exposure", + "type": "object", + "additionalProperties": { + "type": "string" + } } }, "additionalProperties": false diff --git a/schemas/latest/ide-targeted/dev-workspace-template-spec.json b/schemas/latest/ide-targeted/dev-workspace-template-spec.json index 14c402875..32e4561f4 100644 --- a/schemas/latest/ide-targeted/dev-workspace-template-spec.json +++ b/schemas/latest/ide-targeted/dev-workspace-template-spec.json @@ -3,6 +3,12 @@ "type": "object", "title": "DevWorkspaceTemplateSpec schema - Version 2.1.0-alpha - IDE-targeted variant", "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, "commands": { "description": "Predefined, ready-to-use, devworkspace-related commands", "type": "array", @@ -1474,6 +1480,12 @@ } ], "properties": { + "attributes": { + "description": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." + }, "commands": { "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", "type": "array", @@ -3071,6 +3083,14 @@ "description": "Uri of a Devfile yaml file", "type": "string", "markdownDescription": "Uri of a Devfile yaml file" + }, + "variables": { + "description": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." } }, "additionalProperties": false, @@ -3408,6 +3428,14 @@ "additionalProperties": false }, "markdownDescription": "StarterProjects is a project that can be used as a starting point when bootstrapping new projects" + }, + "variables": { + "description": "Map of key-value variables used for string replacement in the devfile. Values can can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source - element identifiers, e.g. command id, component name, endpoint name, project name - references to identifiers, e.g. in events, a command's component, container's volume mount name - string enums, e.g. command group kind, endpoint exposure", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Map of key-value variables used for string replacement in the devfile. Values can can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source - element identifiers, e.g. command id, component name, endpoint name, project name - references to identifiers, e.g. in events, a command's component, container's volume mount name - string enums, e.g. command group kind, endpoint exposure" } }, "additionalProperties": false, diff --git a/schemas/latest/ide-targeted/dev-workspace-template.json b/schemas/latest/ide-targeted/dev-workspace-template.json index 6f9241d52..7c9f3be2d 100644 --- a/schemas/latest/ide-targeted/dev-workspace-template.json +++ b/schemas/latest/ide-targeted/dev-workspace-template.json @@ -201,6 +201,12 @@ "description": "Structure of the devworkspace. This is also the specification of a devworkspace template.", "type": "object", "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, "commands": { "description": "Predefined, ready-to-use, devworkspace-related commands", "type": "array", @@ -1672,6 +1678,12 @@ } ], "properties": { + "attributes": { + "description": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." + }, "commands": { "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", "type": "array", @@ -3269,6 +3281,14 @@ "description": "Uri of a Devfile yaml file", "type": "string", "markdownDescription": "Uri of a Devfile yaml file" + }, + "variables": { + "description": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." } }, "additionalProperties": false, @@ -3606,6 +3626,14 @@ "additionalProperties": false }, "markdownDescription": "StarterProjects is a project that can be used as a starting point when bootstrapping new projects" + }, + "variables": { + "description": "Map of key-value variables used for string replacement in the devfile. Values can can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source - element identifiers, e.g. command id, component name, endpoint name, project name - references to identifiers, e.g. in events, a command's component, container's volume mount name - string enums, e.g. command group kind, endpoint exposure", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Map of key-value variables used for string replacement in the devfile. Values can can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source - element identifiers, e.g. command id, component name, endpoint name, project name - references to identifiers, e.g. in events, a command's component, container's volume mount name - string enums, e.g. command group kind, endpoint exposure" } }, "additionalProperties": false, diff --git a/schemas/latest/ide-targeted/dev-workspace.json b/schemas/latest/ide-targeted/dev-workspace.json index dc98edb75..91b552514 100644 --- a/schemas/latest/ide-targeted/dev-workspace.json +++ b/schemas/latest/ide-targeted/dev-workspace.json @@ -214,6 +214,12 @@ "description": "Structure of the devworkspace. This is also the specification of a devworkspace template.", "type": "object", "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, "commands": { "description": "Predefined, ready-to-use, devworkspace-related commands", "type": "array", @@ -1685,6 +1691,12 @@ } ], "properties": { + "attributes": { + "description": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." + }, "commands": { "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", "type": "array", @@ -3282,6 +3294,14 @@ "description": "Uri of a Devfile yaml file", "type": "string", "markdownDescription": "Uri of a Devfile yaml file" + }, + "variables": { + "description": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." } }, "additionalProperties": false, @@ -3619,6 +3639,14 @@ "additionalProperties": false }, "markdownDescription": "StarterProjects is a project that can be used as a starting point when bootstrapping new projects" + }, + "variables": { + "description": "Map of key-value variables used for string replacement in the devfile. Values can can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source - element identifiers, e.g. command id, component name, endpoint name, project name - references to identifiers, e.g. in events, a command's component, container's volume mount name - string enums, e.g. command group kind, endpoint exposure", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Map of key-value variables used for string replacement in the devfile. Values can can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source - element identifiers, e.g. command id, component name, endpoint name, project name - references to identifiers, e.g. in events, a command's component, container's volume mount name - string enums, e.g. command group kind, endpoint exposure" } }, "additionalProperties": false, diff --git a/schemas/latest/ide-targeted/devfile.json b/schemas/latest/ide-targeted/devfile.json index 77210b65f..04039a3cf 100644 --- a/schemas/latest/ide-targeted/devfile.json +++ b/schemas/latest/ide-targeted/devfile.json @@ -6,6 +6,12 @@ "schemaVersion" ], "properties": { + "attributes": { + "description": "Map of implementation-dependant free-form YAML attributes.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + }, "commands": { "description": "Predefined, ready-to-use, devworkspace-related commands", "type": "array", @@ -694,10 +700,10 @@ "type": "object", "properties": { "attributes": { - "description": "Map of implementation-dependant free-form YAML attributes.", + "description": "Map of implementation-dependant free-form YAML attributes. Deprecated, use the top-level attributes field instead.", "type": "object", "additionalProperties": true, - "markdownDescription": "Map of implementation-dependant free-form YAML attributes." + "markdownDescription": "Map of implementation-dependant free-form YAML attributes. Deprecated, use the top-level attributes field instead." }, "description": { "description": "Optional devfile description", @@ -778,6 +784,12 @@ } ], "properties": { + "attributes": { + "description": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." + }, "commands": { "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", "type": "array", @@ -1699,6 +1711,14 @@ "description": "Uri of a Devfile yaml file", "type": "string", "markdownDescription": "Uri of a Devfile yaml file" + }, + "variables": { + "description": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." } }, "additionalProperties": false, @@ -1994,6 +2014,14 @@ "additionalProperties": false }, "markdownDescription": "StarterProjects is a project that can be used as a starting point when bootstrapping new projects" + }, + "variables": { + "description": "Map of key-value variables used for string replacement in the devfile. Values can can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source - element identifiers, e.g. command id, component name, endpoint name, project name - references to identifiers, e.g. in events, a command's component, container's volume mount name - string enums, e.g. command group kind, endpoint exposure", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Map of key-value variables used for string replacement in the devfile. Values can can be referenced via {{variable-key}} to replace the corresponding value in string fields in the devfile. Replacement cannot be used for\n\n - schemaVersion, metadata, parent source - element identifiers, e.g. command id, component name, endpoint name, project name - references to identifiers, e.g. in events, a command's component, container's volume mount name - string enums, e.g. command group kind, endpoint exposure" } }, "additionalProperties": false, diff --git a/schemas/latest/ide-targeted/parent-overrides.json b/schemas/latest/ide-targeted/parent-overrides.json index 06ba994bf..0d9ab20d8 100644 --- a/schemas/latest/ide-targeted/parent-overrides.json +++ b/schemas/latest/ide-targeted/parent-overrides.json @@ -3,6 +3,12 @@ "type": "object", "title": "ParentOverrides schema - Version 2.1.0-alpha - IDE-targeted variant", "properties": { + "attributes": { + "description": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": true, + "markdownDescription": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." + }, "commands": { "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", "type": "array", @@ -1570,6 +1576,14 @@ "additionalProperties": false }, "markdownDescription": "Overrides of starterProjects encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." + }, + "variables": { + "description": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": { + "type": "string" + }, + "markdownDescription": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules." } }, "additionalProperties": false, diff --git a/schemas/latest/parent-overrides.json b/schemas/latest/parent-overrides.json index 53f1db705..45e078129 100644 --- a/schemas/latest/parent-overrides.json +++ b/schemas/latest/parent-overrides.json @@ -2,6 +2,11 @@ "type": "object", "title": "ParentOverrides schema - Version 2.1.0-alpha", "properties": { + "attributes": { + "description": "Overrides of attributes encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": true + }, "commands": { "description": "Overrides of commands encapsulated in a parent devfile or a plugin. Overriding is done according to K8S strategic merge patch standard rules.", "type": "array", @@ -1403,6 +1408,13 @@ }, "additionalProperties": false } + }, + "variables": { + "description": "Overrides of variables encapsulated in a parent devfile. Overriding is done according to K8S strategic merge patch standard rules.", + "type": "object", + "additionalProperties": { + "type": "string" + } } }, "additionalProperties": false