diff --git a/pkg/devfile/parser/parse_test.go b/pkg/devfile/parser/parse_test.go index 98dc63ba..9ddb9c3e 100644 --- a/pkg/devfile/parser/parse_test.go +++ b/pkg/devfile/parser/parse_test.go @@ -3988,7 +3988,7 @@ func Test_parseFromRegistry(t *testing.T) { ImportReferenceUnion: v1.ImportReferenceUnion{ Id: registryId, }, - Version: "2.0.1", + Version: "2.1.0", RegistryUrl: stagingRegistry, }, }, @@ -4381,7 +4381,7 @@ func getUnsetBooleanDevfileTestData(apiVersion string) (devfileData data.Devfile } -//getBooleanDevfileTestData returns a DevfileData object that contains set values for the boolean properties. If setDefault is true, an object with the default boolean values will be returned +// getBooleanDevfileTestData returns a DevfileData object that contains set values for the boolean properties. If setDefault is true, an object with the default boolean values will be returned func getBooleanDevfileTestData(apiVersion string, setDefault bool) (devfileData data.DevfileData, err error) { type boolValues struct { diff --git a/pkg/devfile/parser/reader.go b/pkg/devfile/parser/reader.go index 2ad85fbb..efd79a51 100644 --- a/pkg/devfile/parser/reader.go +++ b/pkg/devfile/parser/reader.go @@ -49,6 +49,7 @@ type KubernetesResources struct { Services []corev1.Service Routes []routev1.Route Ingresses []extensionsv1.Ingress + Others []interface{} } // ReadKubernetesYaml reads a yaml Kubernetes file from either the Path, URL or Data provided. @@ -105,12 +106,14 @@ func ParseKubernetesYaml(values []interface{}) (KubernetesResources, error) { var services []corev1.Service var routes []routev1.Route var ingresses []extensionsv1.Ingress + var otherResources []interface{} for _, value := range values { var deployment appsv1.Deployment var service corev1.Service var route routev1.Route var ingress extensionsv1.Ingress + var otherResource interface{} byteData, err := k8yaml.Marshal(value) if err != nil { @@ -133,6 +136,9 @@ func ParseKubernetesYaml(values []interface{}) (KubernetesResources, error) { case "Ingress": err = k8yaml.Unmarshal(byteData, &ingress) ingresses = append(ingresses, ingress) + default: + err = k8yaml.Unmarshal(byteData, &otherResource) + otherResources = append(otherResources, otherResource) } if err != nil { @@ -145,5 +151,6 @@ func ParseKubernetesYaml(values []interface{}) (KubernetesResources, error) { Services: services, Routes: routes, Ingresses: ingresses, + Others: otherResources, }, nil } diff --git a/pkg/devfile/parser/reader_test.go b/pkg/devfile/parser/reader_test.go index d8ae9765..414ca4eb 100644 --- a/pkg/devfile/parser/reader_test.go +++ b/pkg/devfile/parser/reader_test.go @@ -192,6 +192,7 @@ func TestReadAndParseKubernetesYaml(t *testing.T) { services := resources.Services routes := resources.Routes ingresses := resources.Ingresses + otherResources := resources.Others for _, deploy := range deployments { assert.Contains(t, tt.wantDeploymentNames, deploy.Name) @@ -205,6 +206,13 @@ func TestReadAndParseKubernetesYaml(t *testing.T) { for _, ingress := range ingresses { assert.Contains(t, tt.wantIngressNames, ingress.Name) } + for _, resource := range otherResources { + kubernetesMap := resource.(map[string]interface{}) + metadata := kubernetesMap["metadata"] + metadataMap := metadata.(map[string]interface{}) + name := metadataMap["name"] + assert.Contains(t, tt.wantOtherNames, name) + } } } }) diff --git a/pkg/devfile/parser/utils.go b/pkg/devfile/parser/utils.go new file mode 100644 index 00000000..6cba73c8 --- /dev/null +++ b/pkg/devfile/parser/utils.go @@ -0,0 +1,111 @@ +// +// Copyright 2022 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package parser + +import ( + "fmt" + "reflect" + + devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common" +) + +// GetDeployComponents gets the default deploy command associated components +func GetDeployComponents(devfileObj DevfileObj) (map[string]string, error) { + deployCommandFilter := common.DevfileOptions{ + CommandOptions: common.CommandOptions{ + CommandGroupKind: devfilev1.DeployCommandGroupKind, + }, + } + deployCommands, err := devfileObj.Data.GetCommands(deployCommandFilter) + if err != nil { + return nil, err + } + + deployAssociatedComponents := make(map[string]string) + var deployAssociatedSubCommands []string + + for _, command := range deployCommands { + if command.Apply != nil { + if len(deployCommands) > 1 && command.Apply.Group.IsDefault != nil && !*command.Apply.Group.IsDefault { + continue + } + deployAssociatedComponents[command.Apply.Component] = command.Apply.Component + } else if command.Composite != nil { + if len(deployCommands) > 1 && command.Composite.Group.IsDefault != nil && !*command.Composite.Group.IsDefault { + continue + } + deployAssociatedSubCommands = append(deployAssociatedSubCommands, command.Composite.Commands...) + } + } + + applyCommandFilter := common.DevfileOptions{ + CommandOptions: common.CommandOptions{ + CommandType: devfilev1.ApplyCommandType, + }, + } + applyCommands, err := devfileObj.Data.GetCommands(applyCommandFilter) + if err != nil { + return nil, err + } + + for _, command := range applyCommands { + if command.Apply != nil { + for _, deployCommand := range deployAssociatedSubCommands { + if deployCommand == command.Id { + deployAssociatedComponents[command.Apply.Component] = command.Apply.Component + } + } + + } + } + + return deployAssociatedComponents, nil +} + +// GetImageBuildComponent gets the image build component from the deploy associated components +func GetImageBuildComponent(devfileObj DevfileObj, deployAssociatedComponents map[string]string) (devfilev1.Component, error) { + imageComponentFilter := common.DevfileOptions{ + ComponentOptions: common.ComponentOptions{ + ComponentType: devfilev1.ImageComponentType, + }, + } + + imageComponents, err := devfileObj.Data.GetComponents(imageComponentFilter) + if err != nil { + return devfilev1.Component{}, err + } + + var imageBuildComponent devfilev1.Component + for _, component := range imageComponents { + if _, ok := deployAssociatedComponents[component.Name]; ok && component.Image != nil { + if reflect.DeepEqual(imageBuildComponent, devfilev1.Component{}) { + imageBuildComponent = component + } else { + errMsg := "expected to find one devfile image component with a deploy command for build. Currently there is more than one image component" + return devfilev1.Component{}, fmt.Errorf(errMsg) + } + } + } + + // If there is not one image component defined in the deploy command, err out + if reflect.DeepEqual(imageBuildComponent, devfilev1.Component{}) { + errMsg := "expected to find one devfile image component with a deploy command for build. Currently there is no image component" + return devfilev1.Component{}, fmt.Errorf(errMsg) + } + + return imageBuildComponent, nil +} diff --git a/pkg/devfile/parser/utils_test.go b/pkg/devfile/parser/utils_test.go new file mode 100644 index 00000000..5afd01d8 --- /dev/null +++ b/pkg/devfile/parser/utils_test.go @@ -0,0 +1,387 @@ +// +// Copyright 2022 Red Hat, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package parser + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + v1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/library/v2/pkg/devfile/parser/data" + "github.com/devfile/library/v2/pkg/devfile/parser/data/v2/common" + "github.com/golang/mock/gomock" +) + +func TestGetDeployComponents(t *testing.T) { + + componentNames := []string{"testcomp0", "testcomp1", "testcomp2"} + + expectedMockErr := "an expected error" + + isDefault := true + notDefault := false + + tests := []struct { + name string + applyCommands []v1.Command + deployCommands []v1.Command + wantDeployAssociatedComponents map[string]string + wantMockErr1 *string + wantMockErr2 *string + wantErr *string + }{ + { + name: "Correct deploy components", + applyCommands: []v1.Command{ + { + Id: "apply0", + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{ + Component: componentNames[0], + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.DeployCommandGroupKind, + IsDefault: &isDefault, + }, + }, + }, + }, + }, + }, + { + Id: "apply1", + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{ + Component: componentNames[1], + }, + }, + }, + { + Id: "apply2", + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{ + Component: componentNames[2], + }, + }, + }, + }, + deployCommands: []v1.Command{ + { + Id: "applynotdefault", + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{ + Component: componentNames[0], + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.DeployCommandGroupKind, + IsDefault: ¬Default, + }, + }, + }, + }, + }, + }, + { + Id: "apply0", + CommandUnion: v1.CommandUnion{ + Apply: &v1.ApplyCommand{ + Component: componentNames[0], + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.DeployCommandGroupKind, + }, + }, + }, + }, + }, + }, + { + Id: "composite1", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{ + Commands: []string{"apply0", "apply2"}, + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.DeployCommandGroupKind, + IsDefault: &isDefault, + }, + }, + }, + }, + }, + }, + { + Id: "compositenotdefault", + CommandUnion: v1.CommandUnion{ + Composite: &v1.CompositeCommand{ + Commands: []string{"apply0", "apply2"}, + LabeledCommand: v1.LabeledCommand{ + BaseCommand: v1.BaseCommand{ + Group: &v1.CommandGroup{ + Kind: v1.DeployCommandGroupKind, + IsDefault: ¬Default, + }, + }, + }, + }, + }, + }, + }, + wantDeployAssociatedComponents: map[string]string{ + componentNames[0]: componentNames[0], + componentNames[2]: componentNames[2], + }, + }, + { + name: "Simulating error case with deploy filter, check if error matches", + wantMockErr1: &expectedMockErr, + wantErr: &expectedMockErr, + }, + { + name: "Simulating error case with apply filter, check if error matches", + wantMockErr2: &expectedMockErr, + wantErr: &expectedMockErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockDevfileData := data.NewMockDevfileData(ctrl) + + // set up the mock data + deployCommandFilter := common.DevfileOptions{ + CommandOptions: common.CommandOptions{ + CommandGroupKind: v1.DeployCommandGroupKind, + }, + } + mockDeployCommands := mockDevfileData.EXPECT().GetCommands(deployCommandFilter) + mockDeployCommands.Return(tt.deployCommands, nil).AnyTimes() + if tt.wantMockErr1 != nil { + mockDeployCommands.Return(nil, fmt.Errorf(*tt.wantMockErr1)) + } + + applyCommandFilter := common.DevfileOptions{ + CommandOptions: common.CommandOptions{ + CommandType: v1.ApplyCommandType, + }, + } + mockApplyCommands := mockDevfileData.EXPECT().GetCommands(applyCommandFilter) + mockApplyCommands.Return(tt.applyCommands, nil).AnyTimes() + if tt.wantMockErr2 != nil { + mockApplyCommands.Return(nil, fmt.Errorf(*tt.wantMockErr2)) + } + + devObj := DevfileObj{ + Data: mockDevfileData, + } + + componentMap, err := GetDeployComponents(devObj) + // Unexpected error + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestGetDeployComponents() error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + assert.Equal(t, tt.wantDeployAssociatedComponents, componentMap, "received the wrong components") + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestGetDeployComponents(): Error message does not match") + } + }) + } + +} + +func TestGetImageBuildComponent(t *testing.T) { + + componentNames := []string{"testcomp1", "testcomp2", "testcomp3"} + + expectedMockErr := "an expected error" + multipleComponentsErr := "Currently there is more than one image component" + noComponentsErr := "Currently there is no image component" + + tests := []struct { + name string + components []v1.Component + deployAssociatedComponents map[string]string + wantComponentName string + wantMockErr *string + wantErr *string + }{ + { + name: "Correct Image component", + components: []v1.Component{ + { + Name: componentNames[0], + ComponentUnion: v1.ComponentUnion{ + Image: &v1.ImageComponent{ + Image: v1.Image{ + ImageUnion: v1.ImageUnion{ + Dockerfile: &v1.DockerfileImage{ + DockerfileSrc: v1.DockerfileSrc{ + Uri: "uri", + }, + }, + }, + }, + }, + }, + }, + { + Name: componentNames[1], + ComponentUnion: v1.ComponentUnion{ + Kubernetes: &v1.KubernetesComponent{ + K8sLikeComponent: v1.K8sLikeComponent{ + K8sLikeComponentLocation: v1.K8sLikeComponentLocation{ + Uri: "uri", + }, + }, + }, + }, + }, + { + Name: componentNames[2], + ComponentUnion: v1.ComponentUnion{ + Image: &v1.ImageComponent{ + Image: v1.Image{ + ImageUnion: v1.ImageUnion{ + Dockerfile: &v1.DockerfileImage{ + DockerfileSrc: v1.DockerfileSrc{ + Uri: "uri", + }, + }, + }, + }, + }, + }, + }, + }, + deployAssociatedComponents: map[string]string{ + componentNames[0]: componentNames[0], + }, + wantComponentName: componentNames[0], + }, + { + name: "Simulating error case, check if error matches", + wantMockErr: &expectedMockErr, + wantErr: &expectedMockErr, + }, + { + name: "Multiple Image component", + components: []v1.Component{ + { + Name: componentNames[0], + ComponentUnion: v1.ComponentUnion{ + Image: &v1.ImageComponent{ + Image: v1.Image{ + ImageUnion: v1.ImageUnion{ + Dockerfile: &v1.DockerfileImage{ + DockerfileSrc: v1.DockerfileSrc{ + Uri: "uri", + }, + }, + }, + }, + }, + }, + }, + { + Name: componentNames[1], + ComponentUnion: v1.ComponentUnion{ + Kubernetes: &v1.KubernetesComponent{ + K8sLikeComponent: v1.K8sLikeComponent{ + K8sLikeComponentLocation: v1.K8sLikeComponentLocation{ + Uri: "uri", + }, + }, + }, + }, + }, + { + Name: componentNames[2], + ComponentUnion: v1.ComponentUnion{ + Image: &v1.ImageComponent{ + Image: v1.Image{ + ImageUnion: v1.ImageUnion{ + Dockerfile: &v1.DockerfileImage{ + DockerfileSrc: v1.DockerfileSrc{ + Uri: "uri", + }, + }, + }, + }, + }, + }, + }, + }, + deployAssociatedComponents: map[string]string{ + componentNames[0]: componentNames[0], + componentNames[2]: componentNames[2], + }, + wantErr: &multipleComponentsErr, + }, + { + name: "No Image component", + components: []v1.Component{}, + deployAssociatedComponents: map[string]string{ + componentNames[0]: componentNames[0], + componentNames[2]: componentNames[2], + }, + wantErr: &noComponentsErr, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mockDevfileData := data.NewMockDevfileData(ctrl) + + imageComponentFilter := common.DevfileOptions{ + ComponentOptions: common.ComponentOptions{ + ComponentType: v1.ImageComponentType, + }, + } + mockGetComponents := mockDevfileData.EXPECT().GetComponents(imageComponentFilter) + + // set up the mock data + mockGetComponents.Return(tt.components, nil).AnyTimes() + if tt.wantMockErr != nil { + mockGetComponents.Return(nil, fmt.Errorf(*tt.wantMockErr)) + } + + devObj := DevfileObj{ + Data: mockDevfileData, + } + + component, err := GetImageBuildComponent(devObj, tt.deployAssociatedComponents) + // Unexpected error + if (err != nil) != (tt.wantErr != nil) { + t.Errorf("TestGetImageBuildComponent() error: %v, wantErr %v", err, tt.wantErr) + } else if err == nil { + assert.Equal(t, tt.wantComponentName, component.Name, "received the wrong component") + } else { + assert.Regexp(t, *tt.wantErr, err.Error(), "TestGetImageBuildComponent(): Error message does not match") + } + }) + } + +}