diff --git a/pkg/devfile/parser/parse.go b/pkg/devfile/parser/parse.go index ad3c70c0..8c948159 100644 --- a/pkg/devfile/parser/parse.go +++ b/pkg/devfile/parser/parse.go @@ -193,25 +193,30 @@ func parseParentAndPlugin(d DevfileObj, resolveCtx *resolutionContextTree, tool switch { case parent.Uri != "": parentDevfileObj, err = parseFromURI(parent.ImportReference, d.Ctx, resolveCtx, tool) - if err != nil { - return err - } case parent.Id != "": parentDevfileObj, err = parseFromRegistry(parent.ImportReference, resolveCtx, tool) - if err != nil { - return err - } case parent.Kubernetes != nil: parentDevfileObj, err = parseFromKubeCRD(parent.ImportReference, resolveCtx, tool) - if err != nil { - return err - } default: return fmt.Errorf("devfile parent does not define any resources") } + if err != nil { + return err + } parentWorkspaceContent := parentDevfileObj.Data.GetDevfileWorkspaceSpecContent() + // add attribute to parent elements + err = addSourceAttributesForOverrideAndMerge(parent.ImportReference, parentWorkspaceContent) + if err != nil { + return err + } if !reflect.DeepEqual(parent.ParentOverrides, v1.ParentOverrides{}) { + // add attribute to parentOverrides elements + curNodeImportReference := resolveCtx.importReference + err = addSourceAttributesForOverrideAndMerge(curNodeImportReference, &parent.ParentOverrides) + if err != nil { + return err + } flattenedParent, err = apiOverride.OverrideDevWorkspaceTemplateSpec(parentWorkspaceContent, parent.ParentOverrides) if err != nil { return err @@ -236,22 +241,30 @@ func parseParentAndPlugin(d DevfileObj, resolveCtx *resolutionContextTree, tool switch { case plugin.Uri != "": pluginDevfileObj, err = parseFromURI(plugin.ImportReference, d.Ctx, resolveCtx, tool) - if err != nil { - return err - } case plugin.Id != "": pluginDevfileObj, err = parseFromRegistry(plugin.ImportReference, resolveCtx, tool) - if err != nil { - return err - } case plugin.Kubernetes != nil: pluginDevfileObj, err = parseFromKubeCRD(plugin.ImportReference, resolveCtx, tool) default: return fmt.Errorf("plugin %s does not define any resources", component.Name) } + if err != nil { + return err + } pluginWorkspaceContent := pluginDevfileObj.Data.GetDevfileWorkspaceSpecContent() + // add attribute to plugin elements + err = addSourceAttributesForOverrideAndMerge(plugin.ImportReference, pluginWorkspaceContent) + if err != nil { + return err + } flattenedPlugin := pluginWorkspaceContent if !reflect.DeepEqual(plugin.PluginOverrides, v1.PluginOverrides{}) { + // add attribute to pluginOverrides elements + curNodeImportReference := resolveCtx.importReference + err = addSourceAttributesForOverrideAndMerge(curNodeImportReference, &plugin.PluginOverrides) + if err != nil { + return err + } flattenedPlugin, err = apiOverride.OverrideDevWorkspaceTemplateSpec(pluginWorkspaceContent, plugin.PluginOverrides) if err != nil { return err diff --git a/pkg/devfile/parser/parse_test.go b/pkg/devfile/parser/parse_test.go index b0df6c4a..9a11b8c1 100644 --- a/pkg/devfile/parser/parse_test.go +++ b/pkg/devfile/parser/parse_test.go @@ -14,6 +14,7 @@ import ( "testing" v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" devfilepkg "github.com/devfile/api/v2/pkg/devfile" devfileCtx "github.com/devfile/library/pkg/devfile/parser/context" v2 "github.com/devfile/library/pkg/devfile/parser/data/v2" @@ -26,6 +27,71 @@ import ( const schemaV200 = "2.0.0" func Test_parseParentAndPluginFromURI(t *testing.T) { + const uri1 = "127.0.0.1:8080" + const uri2 = "127.0.0.1:9090" + importFromUri1 := attributes.Attributes{}.PutString(importSourceAttribute, fmt.Sprintf("uri: http://%s", uri1)) + importFromUri2 := attributes.Attributes{}.PutString(importSourceAttribute, fmt.Sprintf("uri: http://%s", uri2)) + parentOverridesFromMainDevfile := attributes.Attributes{}.PutString(importSourceAttribute, + fmt.Sprintf("uri: http://%s", uri1)).PutString(parentOverrideAttribute, "main devfile") + pluginOverridesFromMainDevfile := attributes.Attributes{}.PutString(importSourceAttribute, + fmt.Sprintf("uri: http://%s", uri2)).PutString(pluginOverrideAttribute, "main devfile") + + parentDevfile := DevfileObj{ + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevfileHeader: devfilepkg.DevfileHeader{ + SchemaVersion: schemaV200, + }, + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devrun", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects", + CommandLine: "npm run", + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "nodejs", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-10", + }, + }, + }, + }, + }, + Events: &v1.Events{ + DevWorkspaceEvents: v1.DevWorkspaceEvents{ + PostStart: []string{"post-start-0"}, + }, + }, + Projects: []v1.Project{ + { + ClonePath: "/data", + ProjectSource: v1.ProjectSource{ + Github: &v1.GithubProjectSource{ + GitLikeProjectSource: v1.GitLikeProjectSource{ + Remotes: map[string]string{ + "master": "https://githube.com/somerepo/someproject.git", + }, + }, + }, + }, + Name: "nodejs-starter", + }, + }, + }, + }, + }, + }, + } type args struct { devFileObj DevfileObj @@ -120,62 +186,7 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { }, }, }, - parentDevfile: DevfileObj{ - Data: &v2.DevfileV2{ - Devfile: v1.Devfile{ - DevfileHeader: devfilepkg.DevfileHeader{ - SchemaVersion: schemaV200, - }, - DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ - DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ - Commands: []v1.Command{ - { - Id: "devrun", - CommandUnion: v1.CommandUnion{ - Exec: &v1.ExecCommand{ - WorkingDir: "/projects", - CommandLine: "npm run", - }, - }, - }, - }, - Components: []v1.Component{ - { - Name: "nodejs", - ComponentUnion: v1.ComponentUnion{ - Container: &v1.ContainerComponent{ - Container: v1.Container{ - Image: "quay.io/nodejs-10", - }, - }, - }, - }, - }, - Events: &v1.Events{ - DevWorkspaceEvents: v1.DevWorkspaceEvents{ - PostStart: []string{"post-start-0"}, - }, - }, - Projects: []v1.Project{ - { - ClonePath: "/data", - ProjectSource: v1.ProjectSource{ - Github: &v1.GithubProjectSource{ - GitLikeProjectSource: v1.GitLikeProjectSource{ - Remotes: map[string]string{ - "master": "https://githube.com/somerepo/someproject.git", - }, - }, - }, - }, - Name: "nodejs-starter", - }, - }, - }, - }, - }, - }, - }, + parentDevfile: parentDevfile, wantDevFile: DevfileObj{ Data: &v2.DevfileV2{ Devfile: v1.Devfile{ @@ -183,7 +194,8 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ Commands: []v1.Command{ { - Id: "devrun", + Attributes: parentOverridesFromMainDevfile, + Id: "devrun", CommandUnion: v1.CommandUnion{ Exec: &v1.ExecCommand{ CommandLine: "npm run", @@ -202,7 +214,8 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { }, Components: []v1.Component{ { - Name: "nodejs", + Attributes: parentOverridesFromMainDevfile, + Name: "nodejs", ComponentUnion: v1.ComponentUnion{ Container: &v1.ContainerComponent{ Container: v1.Container{ @@ -232,7 +245,8 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { }, Projects: []v1.Project{ { - ClonePath: "/projects", + Attributes: parentOverridesFromMainDevfile, + ClonePath: "/projects", ProjectSource: v1.ProjectSource{ Github: &v1.GithubProjectSource{ GitLikeProjectSource: v1.GitLikeProjectSource{ @@ -303,62 +317,7 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { }, }, }, - parentDevfile: DevfileObj{ - Data: &v2.DevfileV2{ - Devfile: v1.Devfile{ - DevfileHeader: devfilepkg.DevfileHeader{ - SchemaVersion: schemaV200, - }, - DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ - DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ - Commands: []v1.Command{ - { - Id: "devrun", - CommandUnion: v1.CommandUnion{ - Exec: &v1.ExecCommand{ - WorkingDir: "/projects", - CommandLine: "npm run", - }, - }, - }, - }, - Components: []v1.Component{ - { - Name: "nodejs", - ComponentUnion: v1.ComponentUnion{ - Container: &v1.ContainerComponent{ - Container: v1.Container{ - Image: "quay.io/nodejs-10", - }, - }, - }, - }, - }, - Events: &v1.Events{ - DevWorkspaceEvents: v1.DevWorkspaceEvents{ - PostStart: []string{"post-start-0"}, - }, - }, - Projects: []v1.Project{ - { - ClonePath: "/data", - ProjectSource: v1.ProjectSource{ - Github: &v1.GithubProjectSource{ - GitLikeProjectSource: v1.GitLikeProjectSource{ - Remotes: map[string]string{ - "master": "https://githube.com/somerepo/someproject.git", - }, - }, - }, - }, - Name: "nodejs-starter", - }, - }, - }, - }, - }, - }, - }, + parentDevfile: parentDevfile, wantDevFile: DevfileObj{ Data: &v2.DevfileV2{ Devfile: v1.Devfile{ @@ -366,7 +325,8 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ Commands: []v1.Command{ { - Id: "devrun", + Attributes: importFromUri1, + Id: "devrun", CommandUnion: v1.CommandUnion{ Exec: &v1.ExecCommand{ CommandLine: "npm run", @@ -385,7 +345,8 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { }, Components: []v1.Component{ { - Name: "nodejs", + Attributes: importFromUri1, + Name: "nodejs", ComponentUnion: v1.ComponentUnion{ Container: &v1.ContainerComponent{ Container: v1.Container{ @@ -415,7 +376,8 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { }, Projects: []v1.Project{ { - ClonePath: "/data", + Attributes: importFromUri1, + ClonePath: "/data", ProjectSource: v1.ProjectSource{ Github: &v1.GithubProjectSource{ GitLikeProjectSource: v1.GitLikeProjectSource{ @@ -827,7 +789,8 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ Commands: []v1.Command{ { - Id: "devrun", + Attributes: importFromUri2, + Id: "devrun", CommandUnion: v1.CommandUnion{ Exec: &v1.ExecCommand{ CommandLine: "npm run", @@ -846,7 +809,8 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { }, Components: []v1.Component{ { - Name: "nodejs", + Attributes: importFromUri2, + Name: "nodejs", ComponentUnion: v1.ComponentUnion{ Container: &v1.ContainerComponent{ Container: v1.Container{ @@ -876,7 +840,8 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { }, Projects: []v1.Project{ { - ClonePath: "/data", + Attributes: importFromUri2, + ClonePath: "/data", ProjectSource: v1.ProjectSource{ Github: &v1.GithubProjectSource{ GitLikeProjectSource: v1.GitLikeProjectSource{ @@ -1037,7 +1002,8 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ Commands: []v1.Command{ { - Id: "devrun", + Attributes: pluginOverridesFromMainDevfile, + Id: "devrun", CommandUnion: v1.CommandUnion{ Exec: &v1.ExecCommand{ CommandLine: "npm build", @@ -1056,7 +1022,8 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { }, Components: []v1.Component{ { - Name: "nodejs", + Attributes: pluginOverridesFromMainDevfile, + Name: "nodejs", ComponentUnion: v1.ComponentUnion{ Container: &v1.ContainerComponent{ Container: v1.Container{ @@ -1086,7 +1053,8 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { }, Projects: []v1.Project{ { - ClonePath: "/data", + Attributes: importFromUri2, + ClonePath: "/data", ProjectSource: v1.ProjectSource{ Github: &v1.GithubProjectSource{ GitLikeProjectSource: v1.GitLikeProjectSource{ @@ -1705,7 +1673,8 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ Commands: []v1.Command{ { - Id: "devrun", + Attributes: parentOverridesFromMainDevfile, + Id: "devrun", CommandUnion: v1.CommandUnion{ Exec: &v1.ExecCommand{ CommandLine: "npm run", @@ -1714,7 +1683,8 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { }, }, { - Id: "devdebug", + Attributes: importFromUri2, + Id: "devdebug", CommandUnion: v1.CommandUnion{ Exec: &v1.ExecCommand{ WorkingDir: "/projects", @@ -1733,7 +1703,8 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { }, Components: []v1.Component{ { - Name: "nodejs", + Attributes: pluginOverridesFromMainDevfile, + Name: "nodejs", ComponentUnion: v1.ComponentUnion{ Container: &v1.ContainerComponent{ Container: v1.Container{ @@ -1763,7 +1734,8 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { }, Projects: []v1.Project{ { - ClonePath: "/projects", + Attributes: parentOverridesFromMainDevfile, + ClonePath: "/projects", ProjectSource: v1.ProjectSource{ Github: &v1.GithubProjectSource{ GitLikeProjectSource: v1.GitLikeProjectSource{ @@ -1899,7 +1871,8 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ Components: []v1.Component{ { - Name: "runtime", + Attributes: pluginOverridesFromMainDevfile, + Name: "runtime", ComponentUnion: v1.ComponentUnion{ Container: &v1.ContainerComponent{ Container: v1.Container{ @@ -2034,7 +2007,8 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ Components: []v1.Component{ { - Name: "runtime", + Attributes: parentOverridesFromMainDevfile, + Name: "runtime", ComponentUnion: v1.ComponentUnion{ Container: &v1.ContainerComponent{ Container: v1.Container{ @@ -2071,7 +2045,7 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { Parent: &v1.Parent{ ImportReference: v1.ImportReference{ ImportReferenceUnion: v1.ImportReferenceUnion{ - Uri: "http://127.0.0.1:8080", + Uri: "http://" + uri2, }, }, }, @@ -2102,8 +2076,10 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + var parentTestServer *httptest.Server + var pluginTestServer *httptest.Server if !reflect.DeepEqual(tt.parentDevfile, DevfileObj{}) { - testServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + parentTestServer = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { data, err := yaml.Marshal(tt.parentDevfile.Data) if err != nil { t.Errorf("unexpected error: %v", err) @@ -2113,18 +2089,31 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { t.Errorf("unexpected error: %v", err) } })) - defer testServer.Close() + // create a listener with the desired port. + l1, err := net.Listen("tcp", uri1) + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + // NewUnstartedServer creates a listener. Close that listener and replace + // with the one we created. + parentTestServer.Listener.Close() + parentTestServer.Listener = l1 + + parentTestServer.Start() + defer parentTestServer.Close() + parent := tt.args.devFileObj.Data.GetParent() if parent == nil { parent = &v1.Parent{} } - parent.Uri = testServer.URL + parent.Uri = parentTestServer.URL tt.args.devFileObj.Data.SetParent(parent) } if !reflect.DeepEqual(tt.pluginDevfile, DevfileObj{}) { - testServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + pluginTestServer = httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { data, err := yaml.Marshal(tt.pluginDevfile.Data) if err != nil { t.Errorf("unexpected error: %v", err) @@ -2134,20 +2123,18 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { t.Errorf("unexpected error: %v", err) } })) - if tt.testRecursiveReference { - // create a listener with the desired port. - l, err := net.Listen("tcp", "127.0.0.1:8080") - if err != nil { - t.Errorf("unexpected error: %v", err) - } - - // NewUnstartedServer creates a listener. Close that listener and replace - // with the one we created. - testServer.Listener.Close() - testServer.Listener = l + l, err := net.Listen("tcp", uri2) + if err != nil { + t.Errorf("unexpected error: %v", err) } - testServer.Start() - defer testServer.Close() + + // NewUnstartedServer creates a listener. Close that listener and replace + // with the one we created. + pluginTestServer.Listener.Close() + pluginTestServer.Listener = l + + pluginTestServer.Start() + defer pluginTestServer.Close() plugincomp := []v1.Component{ { @@ -2156,7 +2143,7 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { Plugin: &v1.PluginComponent{ ImportReference: v1.ImportReference{ ImportReferenceUnion: v1.ImportReferenceUnion{ - Uri: testServer.URL, + Uri: pluginTestServer.URL, }, }, PluginOverrides: tt.pluginOverride, @@ -2183,7 +2170,6 @@ func Test_parseParentAndPluginFromURI(t *testing.T) { if !reflect.DeepEqual(tt.args.devFileObj.Data, tt.wantDevFile.Data) { t.Errorf("wanted: %v, got: %v, difference at %v", tt.wantDevFile.Data, tt.args.devFileObj.Data, pretty.Compare(tt.args.devFileObj.Data, tt.wantDevFile.Data)) } - }) } } @@ -2414,6 +2400,7 @@ func Test_parseParentFromRegistry(t *testing.T) { tool := resolverTools{ registryURLs: []string{"http://" + validRegistry}, } + parentDevfile := DevfileObj{ Data: &v2.DevfileV2{ Devfile: v1.Devfile{ @@ -2534,6 +2521,11 @@ func Test_parseParentFromRegistry(t *testing.T) { }, }, } + + importFromRegistry := attributes.Attributes{}.PutString(importSourceAttribute, resolveImportReference(mainDevfileContent.Parent.ImportReference)) + parentOverridesFromMainDevfile := attributes.Attributes{}.PutString(importSourceAttribute, + resolveImportReference(mainDevfileContent.Parent.ImportReference)).PutString(parentOverrideAttribute, "main devfile") + wantDevfileContent := v1.Devfile{ DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ @@ -2549,7 +2541,8 @@ func Test_parseParentFromRegistry(t *testing.T) { }, Components: []v1.Component{ { - Name: "parent-runtime", + Attributes: parentOverridesFromMainDevfile, + Name: "parent-runtime", ComponentUnion: v1.ComponentUnion{ Container: &v1.ContainerComponent{ Container: v1.Container{ @@ -2623,50 +2616,136 @@ func Test_parseParentFromRegistry(t *testing.T) { }, }, { - name: "it should error out with invalid registry provided", + name: "it should merge the requested parent's data from provided registryURL if no override is set", mainDevfile: DevfileObj{ - Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), Data: &v2.DevfileV2{ Devfile: v1.Devfile{ DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ Parent: &v1.Parent{ ImportReference: v1.ImportReference{ + RegistryUrl: "http://" + validRegistry, ImportReferenceUnion: v1.ImportReferenceUnion{ Id: "nodejs", }, - RegistryUrl: invalidRegistry, }, }, - DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{}, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "runtime2", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, }, }, }, }, - wantErr: true, - }, - { - name: "it should error out with non-exist registry id provided", - mainDevfile: DevfileObj{ - Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + wantDevFile: DevfileObj{ Data: &v2.DevfileV2{ Devfile: v1.Devfile{ DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ - Parent: &v1.Parent{ - ImportReference: v1.ImportReference{ - ImportReferenceUnion: v1.ImportReferenceUnion{ - Id: "not-exist", + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Attributes: importFromRegistry, + Name: "parent-runtime", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + { + Name: "runtime2", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, }, }, }, - DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{}, }, }, }, }, - wantErr: true, }, - } - for _, tt := range tests { + { + name: "it should error out with invalid registry provided", + mainDevfile: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: "nodejs", + }, + RegistryUrl: invalidRegistry, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{}, + }, + }, + }, + }, + wantErr: true, + }, + { + name: "it should error out with non-exist registry id provided", + mainDevfile: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Id: "not-exist", + }, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{}, + }, + }, + }, + }, + wantErr: true, + }, + } + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { err := parseParentAndPlugin(tt.mainDevfile, &resolutionContextTree{}, tool) @@ -2690,6 +2769,306 @@ func Test_parseParentFromRegistry(t *testing.T) { } } +func Test_parseParentFromKubeCRD(t *testing.T) { + + const ( + namespace = "default" + name = "test-parent-k8s" + apiVersion = "testgroup/v1alpha2" + ) + + kubeCRDReference := v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Kubernetes: &v1.KubernetesCustomResourceImportReference{ + Name: name, + Namespace: namespace, + }, + }, + } + + importFromKubeCRD := attributes.Attributes{}.PutString(importSourceAttribute, resolveImportReference(kubeCRDReference)) + parentOverridesFromMainDevfile := attributes.Attributes{}.PutString(importSourceAttribute, + resolveImportReference(kubeCRDReference)).PutString(parentOverrideAttribute, "main devfile") + + parentSpec := v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "parent-runtime", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + }, + }, + } + + tests := []struct { + name string + devWorkspaceResources map[string]v1.DevWorkspaceTemplate + errors map[string]string + mainDevfile DevfileObj + wantDevFile DevfileObj + wantErr bool + }{ + { + name: "should successfully override the parent data", + mainDevfile: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: kubeCRDReference, + ParentOverrides: v1.ParentOverrides{ + Components: []v1.ComponentParentOverride{ + { + Name: "parent-runtime", + ComponentUnionParentOverride: v1.ComponentUnionParentOverride{ + Container: &v1.ContainerComponentParentOverride{ + ContainerParentOverride: v1.ContainerParentOverride{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Attributes: parentOverridesFromMainDevfile, + Name: "parent-runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + devWorkspaceResources: map[string]v1.DevWorkspaceTemplate{ + name: { + TypeMeta: kubev1.TypeMeta{ + Kind: "DevWorkspaceTemplate", + APIVersion: apiVersion, + }, + Spec: parentSpec, + }, + }, + wantErr: false, + }, + { + name: "should successfully merge the parent data without override defined", + mainDevfile: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: kubeCRDReference, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + wantDevFile: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{ + Commands: []v1.Command{ + { + Id: "devbuild", + CommandUnion: v1.CommandUnion{ + Exec: &v1.ExecCommand{ + WorkingDir: "/projects/nodejs-starter", + }, + }, + }, + }, + Components: []v1.Component{ + { + Attributes: importFromKubeCRD, + Name: "parent-runtime", + ComponentUnion: v1.ComponentUnion{ + Volume: &v1.VolumeComponent{ + Volume: v1.Volume{ + Size: "500Mi", + }, + }, + }, + }, + { + Name: "runtime", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-12", + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + devWorkspaceResources: map[string]v1.DevWorkspaceTemplate{ + name: { + TypeMeta: kubev1.TypeMeta{ + Kind: "DevWorkspaceTemplate", + APIVersion: apiVersion, + }, + Spec: parentSpec, + }, + }, + wantErr: false, + }, + { + name: "should fail if kclient get returns error", + mainDevfile: DevfileObj{ + Ctx: devfileCtx.NewDevfileCtx(OutputDevfileYamlPath), + Data: &v2.DevfileV2{ + Devfile: v1.Devfile{ + DevWorkspaceTemplateSpec: v1.DevWorkspaceTemplateSpec{ + Parent: &v1.Parent{ + ImportReference: kubeCRDReference, + }, + DevWorkspaceTemplateSpecContent: v1.DevWorkspaceTemplateSpecContent{}, + }, + }, + }, + }, + devWorkspaceResources: map[string]v1.DevWorkspaceTemplate{}, + errors: map[string]string{ + name: "not found", + }, + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + testK8sClient := &testingutil.FakeK8sClient{ + DevWorkspaceResources: tt.devWorkspaceResources, + Errors: tt.errors, + } + tool := resolverTools{ + k8sClient: testK8sClient, + context: context.Background(), + } + err := parseParentAndPlugin(tt.mainDevfile, &resolutionContextTree{}, tool) + + // Unexpected error + if (err != nil) != tt.wantErr { + t.Errorf("parseParentAndPlugin() error = %v, wantErr %v", err, tt.wantErr) + return + } + + // Expected error and got an err + if tt.wantErr && err != nil { + return + } + + if !reflect.DeepEqual(tt.mainDevfile.Data, tt.wantDevFile.Data) { + t.Errorf("wanted: %v, got: %v, difference at %v", tt.wantDevFile.Data, tt.mainDevfile.Data, pretty.Compare(tt.mainDevfile.Data, tt.wantDevFile.Data)) + } + + }) + } +} + func Test_parseFromURI(t *testing.T) { const uri1 = "127.0.0.1:8080" const httpPrefix = "http://" diff --git a/pkg/devfile/parser/sourceAttribute.go b/pkg/devfile/parser/sourceAttribute.go new file mode 100644 index 00000000..a92d3093 --- /dev/null +++ b/pkg/devfile/parser/sourceAttribute.go @@ -0,0 +1,114 @@ +package parser + +import ( + "fmt" + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" +) + +const ( + importSourceAttribute = "library.devfile.io/imported-from" + parentOverrideAttribute = "library.devfile.io/parent-override-from" + pluginOverrideAttribute = "library.devfile.io/plugin-override-from" +) + +// addSourceAttributesForParentOverride adds an attribute 'library.devfile.io/imported-from=' +// to all elements of template spec content that support attributes. +func addSourceAttributesForTemplateSpecContent(sourceImportReference v1.ImportReference, template *v1.DevWorkspaceTemplateSpecContent) { + for idx, component := range template.Components { + if component.Attributes == nil { + template.Components[idx].Attributes = attributes.Attributes{} + } + template.Components[idx].Attributes.PutString(importSourceAttribute, resolveImportReference(sourceImportReference)) + } + for idx, command := range template.Commands { + if command.Attributes == nil { + template.Commands[idx].Attributes = attributes.Attributes{} + } + template.Commands[idx].Attributes.PutString(importSourceAttribute, resolveImportReference(sourceImportReference)) + } + for idx, project := range template.Projects { + if project.Attributes == nil { + template.Projects[idx].Attributes = attributes.Attributes{} + } + template.Projects[idx].Attributes.PutString(importSourceAttribute, resolveImportReference(sourceImportReference)) + } + for idx, project := range template.StarterProjects { + if project.Attributes == nil { + template.StarterProjects[idx].Attributes = attributes.Attributes{} + } + template.StarterProjects[idx].Attributes.PutString(importSourceAttribute, resolveImportReference(sourceImportReference)) + } +} + +// addSourceAttributesForParentOverride adds an attribute 'library.devfile.io/parent-override-from=' +// to all elements of parent override that support attributes. +func addSourceAttributesForParentOverride(sourceImportReference v1.ImportReference, parentOverrides *v1.ParentOverrides) { + for idx, component := range parentOverrides.Components { + if component.Attributes == nil { + parentOverrides.Components[idx].Attributes = attributes.Attributes{} + } + parentOverrides.Components[idx].Attributes.PutString(parentOverrideAttribute, resolveImportReference(sourceImportReference)) + } + for idx, command := range parentOverrides.Commands { + if command.Attributes == nil { + parentOverrides.Commands[idx].Attributes = attributes.Attributes{} + } + parentOverrides.Commands[idx].Attributes.PutString(parentOverrideAttribute, resolveImportReference(sourceImportReference)) + } + for idx, project := range parentOverrides.Projects { + if project.Attributes == nil { + parentOverrides.Projects[idx].Attributes = attributes.Attributes{} + } + parentOverrides.Projects[idx].Attributes.PutString(parentOverrideAttribute, resolveImportReference(sourceImportReference)) + } + for idx, project := range parentOverrides.StarterProjects { + if project.Attributes == nil { + parentOverrides.StarterProjects[idx].Attributes = attributes.Attributes{} + } + parentOverrides.StarterProjects[idx].Attributes.PutString(parentOverrideAttribute, resolveImportReference(sourceImportReference)) + } + +} + +// addSourceAttributesForPluginOverride adds an attribute 'library.devfile.io/plugin-override-from=' +// to all elements of plugin override that support attributes. +func addSourceAttributesForPluginOverride(sourceImportReference v1.ImportReference, pluginOverrides *v1.PluginOverrides) { + for idx, component := range pluginOverrides.Components { + if component.Attributes == nil { + pluginOverrides.Components[idx].Attributes = attributes.Attributes{} + } + pluginOverrides.Components[idx].Attributes.PutString(pluginOverrideAttribute, resolveImportReference(sourceImportReference)) + } + for idx, command := range pluginOverrides.Commands { + if command.Attributes == nil { + pluginOverrides.Commands[idx].Attributes = attributes.Attributes{} + } + pluginOverrides.Commands[idx].Attributes.PutString(pluginOverrideAttribute, resolveImportReference(sourceImportReference)) + } + +} + +// addSourceAttributesForOverrideAndMerge adds an attribute record the import reference to all elements of template that support attributes. +func addSourceAttributesForOverrideAndMerge(sourceImportReference v1.ImportReference, template interface{}) error { + if template == nil { + return fmt.Errorf("cannot add source attributes to nil") + } + + mainContent, isMainContent := template.(*v1.DevWorkspaceTemplateSpecContent) + parentOverride, isParentOverride := template.(*v1.ParentOverrides) + pluginOverride, isPluginOverride := template.(*v1.PluginOverrides) + + switch { + case isMainContent: + addSourceAttributesForTemplateSpecContent(sourceImportReference, mainContent) + case isParentOverride: + addSourceAttributesForParentOverride(sourceImportReference, parentOverride) + case isPluginOverride: + addSourceAttributesForPluginOverride(sourceImportReference, pluginOverride) + default: + return fmt.Errorf("unknown template type") + } + + return nil +} diff --git a/pkg/devfile/parser/sourceAttribute_test.go b/pkg/devfile/parser/sourceAttribute_test.go new file mode 100644 index 00000000..f85fd55d --- /dev/null +++ b/pkg/devfile/parser/sourceAttribute_test.go @@ -0,0 +1,162 @@ +package parser + +import ( + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/attributes" + "github.com/kylelemons/godebug/pretty" + "reflect" + "testing" +) + +func TestAddSourceAttributesForOverrideAndMerge(t *testing.T) { + importReference := v1.ImportReference{ + ImportReferenceUnion: v1.ImportReferenceUnion{ + Uri: "127.0.0.1:8080", + }, + } + uriImportAttribute := attributes.Attributes{}.PutString(importSourceAttribute, resolveImportReference(importReference)) + pluginOverrideImportAttribute := attributes.Attributes{}.PutString(pluginOverrideAttribute, "main devfile") + parentOverrideImportAttribute := attributes.Attributes{}.PutString(parentOverrideAttribute, "main devfile") + + tests := []struct { + name string + wantErr bool + importReference v1.ImportReference + template interface{} + wantResult interface{} + }{ + { + name: "should fail if template is nil", + template: nil, + wantErr: true, + }, + { + name: "should fail if template is a not support type", + template: "invalid template", + wantErr: true, + }, + { + name: "template is with type *DevWorkspaceTemplateSpecContent", + importReference: importReference, + template: &v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Name: "nodejs", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-10", + }, + }, + }, + }, + }, + }, + wantResult: &v1.DevWorkspaceTemplateSpecContent{ + Components: []v1.Component{ + { + Attributes: uriImportAttribute, + Name: "nodejs", + ComponentUnion: v1.ComponentUnion{ + Container: &v1.ContainerComponent{ + Container: v1.Container{ + Image: "quay.io/nodejs-10", + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "template is with type *PluginOverrides", + importReference: v1.ImportReference{}, + template: &v1.PluginOverrides{ + Components: []v1.ComponentPluginOverride{ + { + Name: "nodejs", + ComponentUnionPluginOverride: v1.ComponentUnionPluginOverride{ + Container: &v1.ContainerComponentPluginOverride{ + ContainerPluginOverride: v1.ContainerPluginOverride{ + Image: "quay.io/nodejs-10", + }, + }, + }, + }, + }, + }, + wantResult: &v1.PluginOverrides{ + Components: []v1.ComponentPluginOverride{ + { + Name: "nodejs", + Attributes: pluginOverrideImportAttribute, + ComponentUnionPluginOverride: v1.ComponentUnionPluginOverride{ + Container: &v1.ContainerComponentPluginOverride{ + ContainerPluginOverride: v1.ContainerPluginOverride{ + Image: "quay.io/nodejs-10", + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + { + name: "template is with type *ParentOverrides", + importReference: v1.ImportReference{}, + template: &v1.ParentOverrides{ + Components: []v1.ComponentParentOverride{ + { + Name: "nodejs", + ComponentUnionParentOverride: v1.ComponentUnionParentOverride{ + Container: &v1.ContainerComponentParentOverride{ + ContainerParentOverride: v1.ContainerParentOverride{ + Image: "quay.io/nodejs-10", + }, + }, + }, + }, + }, + }, + wantResult: &v1.ParentOverrides{ + Components: []v1.ComponentParentOverride{ + { + Name: "nodejs", + Attributes: parentOverrideImportAttribute, + ComponentUnionParentOverride: v1.ComponentUnionParentOverride{ + Container: &v1.ContainerComponentParentOverride{ + ContainerParentOverride: v1.ContainerParentOverride{ + Image: "quay.io/nodejs-10", + }, + }, + }, + }, + }, + }, + wantErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := addSourceAttributesForOverrideAndMerge(tt.importReference, tt.template) + + if tt.wantErr == (err == nil) { + t.Errorf("Test_AddSourceAttributesForOverrideAndMerge() error = %v, wantErr %v", err, tt.wantErr) + return + } + + if err != nil { + return + } + + if !reflect.DeepEqual(tt.template, tt.wantResult) { + t.Errorf("wanted: %v, got: %v, difference at %v", tt.wantResult, tt.template, pretty.Compare(tt.template, tt.wantResult)) + } + + }) + } + +}