From a39931388e746eb984872713cd9ca8774cca165c Mon Sep 17 00:00:00 2001 From: Parthvi Vala Date: Wed, 7 Dec 2022 13:54:02 +0530 Subject: [PATCH] Add support for parsing multiple k8s definition in a single Devfile K8s component (#6372) * Rename GetK8sManifestWithVariablesSubstituted to GetK8sManifestsWithVariablesSubstituted * Add GetK8sComponentAsUnstructuredList; add support for multiple manifests in a single k8s component Signed-off-by: Parthvi Vala * Use GetK8sComponentAsUnstructuredList in binding/remove.go * Use GetK8sComponentAsUnstructuredList in component/component_test.go * Use GetK8sComponentAsUnstructuredList in libdevfile/component_kubernetes_utils.go * Use GetK8sComponentAsUnstructuredList in service/service.go * Use GetK8sComponentAsUnstructuredList in devfile/adapters/kubernetes/component/adapter.go * Add documentation for GetK8sComponentAsUnstructuredList and remove GetK8sComponentAsUnstructured * Fix createOrUpdateServiceForComponent to fetch only the service created for the component * Add integration test for odo deploy * Add integration test for odo dev * Attempt at fixing list/describe binding tests; fixes old mishap Signed-off-by: Parthvi Vala * Check for empty list of binding Signed-off-by: Parthvi Vala Signed-off-by: Parthvi Vala --- pkg/binding/binding.go | 2 +- pkg/binding/remove.go | 9 +- pkg/component/apply_kubernetes.go | 24 ++-- pkg/component/component_test.go | 8 +- pkg/component/validate.go | 23 ++-- .../adapters/kubernetes/component/adapter.go | 8 +- pkg/kclient/interface.go | 2 +- pkg/kclient/mock_Client.go | 12 +- pkg/kclient/services.go | 4 +- pkg/libdevfile/component_kubernetes_utils.go | 54 ++++++-- pkg/libdevfile/libdevfile.go | 6 +- pkg/libdevfile/libdevfile_test.go | 10 +- pkg/service/service.go | 41 +++--- ...ple-k8s-resources-in-single-component.yaml | 123 ++++++++++++++++++ ...ple-k8s-resources-in-single-component.yaml | 83 ++++++++++++ .../cmd_describe_list_binding_test.go | 35 +++-- tests/integration/cmd_dev_test.go | 62 ++++++--- tests/integration/cmd_devfile_deploy_test.go | 21 ++- 18 files changed, 404 insertions(+), 123 deletions(-) create mode 100644 tests/examples/source/devfiles/nodejs/devfile-deploy-multiple-k8s-resources-in-single-component.yaml create mode 100644 tests/examples/source/devfiles/nodejs/devfile-with-multiple-k8s-resources-in-single-component.yaml diff --git a/pkg/binding/binding.go b/pkg/binding/binding.go index 0c9d336612e..a31d9fbd5e9 100644 --- a/pkg/binding/binding.go +++ b/pkg/binding/binding.go @@ -126,7 +126,7 @@ func (o *BindingClient) GetBindingsFromDevfile(devfileObj parser.DevfileObj, con } for _, component := range kubeComponents { - strCRD, err := libdevfile.GetK8sManifestWithVariablesSubstituted(devfileObj, component.Name, context, devfilefs.DefaultFs{}) + strCRD, err := libdevfile.GetK8sManifestsWithVariablesSubstituted(devfileObj, component.Name, context, devfilefs.DefaultFs{}) if err != nil { return nil, err } diff --git a/pkg/binding/remove.go b/pkg/binding/remove.go index 47d3cf46428..53c8f92c557 100644 --- a/pkg/binding/remove.go +++ b/pkg/binding/remove.go @@ -36,12 +36,15 @@ func (o *BindingClient) RemoveBinding(servicebindingName string, obj parser.Devf return obj, err } for _, component := range components { - var unstructuredObj unstructured.Unstructured + var unstructuredObjs []unstructured.Unstructured // Parse the K8s manifest - unstructuredObj, err = libdevfile.GetK8sComponentAsUnstructured(obj, component.Name, filepath.Dir(obj.Ctx.GetAbsPath()), devfilefs.DefaultFs{}) - if err != nil { + unstructuredObjs, err = libdevfile.GetK8sComponentAsUnstructuredList(obj, component.Name, filepath.Dir(obj.Ctx.GetAbsPath()), devfilefs.DefaultFs{}) + if err != nil || len(unstructuredObjs) == 0 { continue } + // We default to the first object in the list because as far as ServiceBinding is concerned, + // we assume that only one resource will be defined for the Devfile K8s component; which is true + unstructuredObj := unstructuredObjs[0] if unstructuredObj.GetKind() == kclient.ServiceBindingKind { options = append(options, unstructuredObj.GetName()) if unstructuredObj.GetName() == servicebindingName { diff --git a/pkg/component/apply_kubernetes.go b/pkg/component/apply_kubernetes.go index 2c41cc33572..1d6a69b531d 100644 --- a/pkg/component/apply_kubernetes.go +++ b/pkg/component/apply_kubernetes.go @@ -6,12 +6,13 @@ import ( devfilev1 "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/devfile/library/pkg/devfile/parser" devfilefs "github.com/devfile/library/pkg/testingutil/filesystem" + "k8s.io/klog" + "github.com/redhat-developer/odo/pkg/kclient" odolabels "github.com/redhat-developer/odo/pkg/labels" "github.com/redhat-developer/odo/pkg/libdevfile" "github.com/redhat-developer/odo/pkg/log" "github.com/redhat-developer/odo/pkg/service" - "k8s.io/klog" ) // ApplyKubernetes contains the logic to create the k8s resources defined by the `apply` command @@ -30,10 +31,11 @@ func ApplyKubernetes( kubeClient kclient.ClientInterface, path string, ) error { + // TODO: Use GetK8sComponentAsUnstructured here and pass it to ValidateResourcesExistInK8sComponent // Validate if the GVRs represented by Kubernetes inlined components are supported by the underlying cluster - _, err := ValidateResourceExist(kubeClient, devfile, kubernetes, path) + kind, err := ValidateResourcesExistInK8sComponent(kubeClient, devfile, kubernetes, path) if err != nil { - return err + return fmt.Errorf("%s: %w", kind, err) } // Get the most common labels that's applicable to all resources being deployed. @@ -49,17 +51,17 @@ func ApplyKubernetes( odolabels.SetProjectType(annotations, GetComponentTypeFromDevfileMetadata(devfile.Data.GetMetadata())) // Get the Kubernetes component - u, err := libdevfile.GetK8sComponentAsUnstructured(devfile, kubernetes.Name, path, devfilefs.DefaultFs{}) + uList, err := libdevfile.GetK8sComponentAsUnstructuredList(devfile, kubernetes.Name, path, devfilefs.DefaultFs{}) if err != nil { return err } - - // Deploy the actual Kubernetes component and error out if there's an issue. - log.Sectionf("Deploying Kubernetes Component: %s", u.GetName()) - err = service.PushKubernetesResource(kubeClient, u, labels, annotations, mode) - if err != nil { - return fmt.Errorf("failed to create service(s) associated with the component: %w", err) + for _, u := range uList { + // Deploy the actual Kubernetes component and error out if there's an issue. + log.Sectionf("Deploying Kubernetes Component: %s", u.GetName()) + err = service.PushKubernetesResource(kubeClient, u, labels, annotations, mode) + if err != nil { + return fmt.Errorf("failed to create service(s) associated with the component: %w", err) + } } - return nil } diff --git a/pkg/component/component_test.go b/pkg/component/component_test.go index a88f616b4dd..fca0737b4c1 100644 --- a/pkg/component/component_test.go +++ b/pkg/component/component_test.go @@ -506,7 +506,9 @@ func TestListRoutesAndIngresses(t *testing.T) { ) createFakeIngressFromDevfile := func(devfileObj parser.DevfileObj, ingressComponentName string, label map[string]string) *v1.Ingress { ing := &v1.Ingress{} - u, _ := libdevfile.GetK8sComponentAsUnstructured(devfileObj, ingressComponentName, "", filesystem.DefaultFs{}) + uList, _ := libdevfile.GetK8sComponentAsUnstructuredList(devfileObj, ingressComponentName, "", filesystem.DefaultFs{}) + // We default to the first object in the list because it is safe to do so since we have only defined one K8s resource for the Devfile K8s component + u := uList[0] _ = runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), ing) ing.SetLabels(label) return ing @@ -514,7 +516,9 @@ func TestListRoutesAndIngresses(t *testing.T) { createFakeRouteFromDevfile := func(devfileObj parser.DevfileObj, routeComponentName string, label map[string]string) *v12.Route { route := &v12.Route{} - u, _ := libdevfile.GetK8sComponentAsUnstructured(devfileObj, routeComponentName, "", filesystem.DefaultFs{}) + uList, _ := libdevfile.GetK8sComponentAsUnstructuredList(devfileObj, routeComponentName, "", filesystem.DefaultFs{}) + // We default to the first object in the list because it is safe to do so since we have only defined one K8s resource for the Devfile K8s component + u := uList[0] _ = runtime.DefaultUnstructuredConverter.FromUnstructured(u.UnstructuredContent(), route) route.SetLabels(label) return route diff --git a/pkg/component/validate.go b/pkg/component/validate.go index e677a7209af..aa73a325233 100644 --- a/pkg/component/validate.go +++ b/pkg/component/validate.go @@ -21,7 +21,7 @@ func ValidateResourcesExist(client kclient.ClientInterface, devfileObj parser.De var unsupportedResources []string for _, c := range k8sComponents { - kindErr, err := ValidateResourceExist(client, devfileObj, c, context) + kindErr, err := ValidateResourcesExistInK8sComponent(client, devfileObj, c, context) if err != nil { if kindErr != "" { unsupportedResources = append(unsupportedResources, kindErr) @@ -38,20 +38,21 @@ func ValidateResourcesExist(client kclient.ClientInterface, devfileObj parser.De return nil } -// ValidateResourceExist validates if a Kubernetes inlined component is installed on the cluster -func ValidateResourceExist(client kclient.ClientInterface, devfileObj parser.DevfileObj, k8sComponent devfile.Component, context string) (kindErr string, err error) { +// ValidateResourcesExistInK8sComponent validates if resources defined inside a Kubernetes inlined component are installed on the cluster +func ValidateResourcesExistInK8sComponent(client kclient.ClientInterface, devfileObj parser.DevfileObj, k8sComponent devfile.Component, context string) (kindErr string, err error) { // get the string representation of the YAML definition of a CRD - u, err := libdevfile.GetK8sComponentAsUnstructured(devfileObj, k8sComponent.Name, context, devfilefs.DefaultFs{}) + uList, err := libdevfile.GetK8sComponentAsUnstructuredList(devfileObj, k8sComponent.Name, context, devfilefs.DefaultFs{}) if err != nil { return "", err } - - _, err = client.GetRestMappingFromUnstructured(u) - if err != nil && u.GetKind() != "ServiceBinding" { - // getting a RestMapping would fail if there are no matches for the Kind field on the cluster; - // but if it's a "ServiceBinding" resource, we don't add it to unsupported list because odo can create links - // without having SBO installed - return u.GetKind(), errors.New("resource not supported") + for _, u := range uList { + _, err = client.GetRestMappingFromUnstructured(u) + if err != nil && u.GetKind() != "ServiceBinding" { + // getting a RestMapping would fail if there are no matches for the Kind field on the cluster; + // but if it's a "ServiceBinding" resource, we don't add it to unsupported list because odo can create links + // without having SBO installed + return u.GetKind(), errors.New("resource not supported") + } } return "", nil } diff --git a/pkg/devfile/adapters/kubernetes/component/adapter.go b/pkg/devfile/adapters/kubernetes/component/adapter.go index 2837666bde8..d9ffb6d8a68 100644 --- a/pkg/devfile/adapters/kubernetes/component/adapter.go +++ b/pkg/devfile/adapters/kubernetes/component/adapter.go @@ -552,7 +552,7 @@ func (a *Adapter) createOrUpdateComponent( } func (a *Adapter) createOrUpdateServiceForComponent(svc *corev1.Service, componentName string, ownerReference metav1.OwnerReference) error { - oldSvc, err := a.kubeClient.GetOneService(a.ComponentName, a.AppName) + oldSvc, err := a.kubeClient.GetOneService(a.ComponentName, a.AppName, true) originOwnerReferences := svc.OwnerReferences if err != nil { // no old service was found, create a new one @@ -635,12 +635,12 @@ func (a Adapter) getRemoteResourcesNotPresentInDevfile(selector string) (objects // convert all devfileK8sResources to unstructured data var devfileK8sResourcesUnstructured []unstructured.Unstructured for _, devfileK := range devfileK8sResources { - var devfileKUnstructured unstructured.Unstructured - devfileKUnstructured, err = libdevfile.GetK8sComponentAsUnstructured(a.Devfile, devfileK.Name, a.Context, devfilefs.DefaultFs{}) + var devfileKUnstructuredList []unstructured.Unstructured + devfileKUnstructuredList, err = libdevfile.GetK8sComponentAsUnstructuredList(a.Devfile, devfileK.Name, a.Context, devfilefs.DefaultFs{}) if err != nil { return nil, nil, fmt.Errorf("unable to read the resource: %w", err) } - devfileK8sResourcesUnstructured = append(devfileK8sResourcesUnstructured, devfileKUnstructured) + devfileK8sResourcesUnstructured = append(devfileK8sResourcesUnstructured, devfileKUnstructuredList...) } isSBOSupported, err := a.kubeClient.IsServiceBindingSupported() diff --git a/pkg/kclient/interface.go b/pkg/kclient/interface.go index 09a25e7d61e..f99ccebdf31 100644 --- a/pkg/kclient/interface.go +++ b/pkg/kclient/interface.go @@ -148,7 +148,7 @@ type ClientInterface interface { UpdateService(svc corev1.Service) (*corev1.Service, error) ListServices(selector string) ([]corev1.Service, error) DeleteService(serviceName string) error - GetOneService(componentName, appName string) (*corev1.Service, error) + GetOneService(componentName, appName string, isPartOfComponent bool) (*corev1.Service, error) GetOneServiceFromSelector(selector string) (*corev1.Service, error) // user.go diff --git a/pkg/kclient/mock_Client.go b/pkg/kclient/mock_Client.go index 00316f54f2e..d0f2c4329c8 100644 --- a/pkg/kclient/mock_Client.go +++ b/pkg/kclient/mock_Client.go @@ -696,19 +696,19 @@ func (mr *MockClientInterfaceMockRecorder) GetOneDeploymentFromSelector(selector return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOneDeploymentFromSelector", reflect.TypeOf((*MockClientInterface)(nil).GetOneDeploymentFromSelector), selector) } -// GetOneService mocks base method. -func (m *MockClientInterface) GetOneService(componentName, appName string) (*v11.Service, error) { +// GetOneService mocks base method +func (m *MockClientInterface) GetOneService(componentName, appName string, isPartOfComponent bool) (*v11.Service, error) { m.ctrl.T.Helper() - ret := m.ctrl.Call(m, "GetOneService", componentName, appName) + ret := m.ctrl.Call(m, "GetOneService", componentName, appName, isPartOfComponent) ret0, _ := ret[0].(*v11.Service) ret1, _ := ret[1].(error) return ret0, ret1 } -// GetOneService indicates an expected call of GetOneService. -func (mr *MockClientInterfaceMockRecorder) GetOneService(componentName, appName interface{}) *gomock.Call { +// GetOneService indicates an expected call of GetOneService +func (mr *MockClientInterfaceMockRecorder) GetOneService(componentName, appName, isPartOfComponent interface{}) *gomock.Call { mr.mock.ctrl.T.Helper() - return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOneService", reflect.TypeOf((*MockClientInterface)(nil).GetOneService), componentName, appName) + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetOneService", reflect.TypeOf((*MockClientInterface)(nil).GetOneService), componentName, appName, isPartOfComponent) } // GetOneServiceFromSelector mocks base method. diff --git a/pkg/kclient/services.go b/pkg/kclient/services.go index 5e25e60a969..569060af61d 100644 --- a/pkg/kclient/services.go +++ b/pkg/kclient/services.go @@ -52,8 +52,8 @@ func (c *Client) DeleteService(serviceName string) error { // GetOneService retrieves the service with the given component and app name // An error is thrown when exactly one service is not found for the selector. -func (c *Client) GetOneService(componentName, appName string) (*corev1.Service, error) { - selector := odolabels.GetSelector(componentName, appName, odolabels.ComponentDevMode, false) +func (c *Client) GetOneService(componentName, appName string, isPartOfComponent bool) (*corev1.Service, error) { + selector := odolabels.GetSelector(componentName, appName, odolabels.ComponentDevMode, isPartOfComponent) return c.GetOneServiceFromSelector(selector) } diff --git a/pkg/libdevfile/component_kubernetes_utils.go b/pkg/libdevfile/component_kubernetes_utils.go index caf1e710882..2212f692874 100644 --- a/pkg/libdevfile/component_kubernetes_utils.go +++ b/pkg/libdevfile/component_kubernetes_utils.go @@ -1,29 +1,57 @@ package libdevfile import ( + "bytes" + "io" + "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/devfile/library/pkg/devfile/parser" "github.com/devfile/library/pkg/devfile/parser/data/v2/common" devfilefs "github.com/devfile/library/pkg/testingutil/filesystem" "github.com/ghodss/yaml" + yaml3 "gopkg.in/yaml.v3" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) -// GetK8sComponentAsUnstructured parses the Inlined/URI K8s of the devfile K8s component -func GetK8sComponentAsUnstructured(devfileObj parser.DevfileObj, componentName string, - context string, fs devfilefs.Filesystem) (unstructured.Unstructured, error) { +// GetK8sComponentAsUnstructuredList parses the Inlined/URI K8s of the Devfile K8s component and returns a list of unstructured.Unstructured objects; +// List is returned here because it is possible to define multiple K8s resources against a single Devfile K8s component +func GetK8sComponentAsUnstructuredList(devfileObj parser.DevfileObj, componentName string, + context string, fs devfilefs.Filesystem) ([]unstructured.Unstructured, error) { - strCRD, err := GetK8sManifestWithVariablesSubstituted(devfileObj, componentName, context, fs) + strCRD, err := GetK8sManifestsWithVariablesSubstituted(devfileObj, componentName, context, fs) if err != nil { - return unstructured.Unstructured{}, err + return nil, err } - // convert the YAML definition into map[string]interface{} since it's needed to create dynamic resource - u := unstructured.Unstructured{} - if err = yaml.Unmarshal([]byte(strCRD), &u.Object); err != nil { - return unstructured.Unstructured{}, err + var uList []unstructured.Unstructured + // Use the decoder to correctly read file with multiple manifests + decoder := yaml3.NewDecoder(bytes.NewBufferString(strCRD)) + for { + var decodeU unstructured.Unstructured + if err = decoder.Decode(&decodeU.Object); err != nil { + if err == io.EOF { + break + } + return nil, err + } + + // Marshal the object's data so that it can be unmarshalled again into unstructured.Unstructured object + // We do this again because yaml3 "gopkg.in/yaml.v3" pkg is unable to properly unmarshal the data into an unstructured object + rawData, err := yaml3.Marshal(decodeU.Object) + if err != nil { + return nil, err + } + + // Use "github.com/ghodss/yaml" pkg to correctly unmarshal the data into an unstructured object; + var u unstructured.Unstructured + if err = yaml.Unmarshal(rawData, &u.Object); err != nil { + return nil, err + } + + uList = append(uList, u) + } - return u, nil + return uList, nil } // ListKubernetesComponents lists all the kubernetes components from the devfile @@ -34,14 +62,14 @@ func ListKubernetesComponents(devfileObj parser.DevfileObj, path string) (list [ if err != nil { return } - var u unstructured.Unstructured + var u []unstructured.Unstructured for _, kComponent := range components { if kComponent.Kubernetes != nil { - u, err = GetK8sComponentAsUnstructured(devfileObj, kComponent.Name, path, devfilefs.DefaultFs{}) + u, err = GetK8sComponentAsUnstructuredList(devfileObj, kComponent.Name, path, devfilefs.DefaultFs{}) if err != nil { return } - list = append(list, u) + list = append(list, u...) } } return diff --git a/pkg/libdevfile/libdevfile.go b/pkg/libdevfile/libdevfile.go index d4294633e22..f95aa46d54e 100644 --- a/pkg/libdevfile/libdevfile.go +++ b/pkg/libdevfile/libdevfile.go @@ -356,7 +356,7 @@ func IsDebugPort(name string) bool { // GetContainerComponentsForCommand returns the list of container components that would get used if the specified command runs. func GetContainerComponentsForCommand(devfileObj parser.DevfileObj, cmd v1alpha2.Command) ([]string, error) { - //No error if cmd is empty + // No error if cmd is empty if reflect.DeepEqual(cmd, v1alpha2.Command{}) { return nil, nil } @@ -416,12 +416,12 @@ func GetContainerComponentsForCommand(devfileObj parser.DevfileObj, cmd v1alpha2 } } -// GetK8sManifestWithVariablesSubstituted returns the full content of either a Kubernetes or an Openshift +// GetK8sManifestsWithVariablesSubstituted returns the full content of either a Kubernetes or an Openshift // Devfile component, either Inlined or referenced via a URI. // No matter how the component is defined, it returns the content with all variables substituted // using the global variables map defined in `devfileObj`. // An error is returned if the content references an invalid variable key not defined in the Devfile object. -func GetK8sManifestWithVariablesSubstituted(devfileObj parser.DevfileObj, devfileCmpName string, +func GetK8sManifestsWithVariablesSubstituted(devfileObj parser.DevfileObj, devfileCmpName string, context string, fs devfilefs.Filesystem) (string, error) { components, err := devfileObj.Data.GetComponents(common.DevfileOptions{FilterByName: devfileCmpName}) diff --git a/pkg/libdevfile/libdevfile_test.go b/pkg/libdevfile/libdevfile_test.go index a66ace4ab3a..6e4de885b99 100644 --- a/pkg/libdevfile/libdevfile_test.go +++ b/pkg/libdevfile/libdevfile_test.go @@ -940,7 +940,7 @@ func TestGetDebugEndpointsForComponent(t *testing.T) { } } -func TestGetK8sManifestWithVariablesSubstituted(t *testing.T) { +func TestGetK8sManifestsWithVariablesSubstituted(t *testing.T) { fakeFs := devfileFileSystem.NewFakeFs() cmpName := "my-cmp-1" for _, tt := range []struct { @@ -1258,14 +1258,14 @@ func TestGetK8sManifestWithVariablesSubstituted(t *testing.T) { return } - got, err := GetK8sManifestWithVariablesSubstituted(tt.devfileObjFunc(), cmpName, "", fakeFs) + got, err := GetK8sManifestsWithVariablesSubstituted(tt.devfileObjFunc(), cmpName, "", fakeFs) if (err != nil) != tt.wantErr { - t.Errorf("GetK8sManifestWithVariablesSubstituted() error = %v, wantErr %v", + t.Errorf("GetK8sManifestsWithVariablesSubstituted() error = %v, wantErr %v", err, tt.wantErr) return } if got != tt.want { - t.Errorf("GetK8sManifestWithVariablesSubstituted() got = %v, want %v", + t.Errorf("GetK8sManifestsWithVariablesSubstituted() got = %v, want %v", got, tt.want) } }) @@ -1623,7 +1623,7 @@ func TestValidateAndGetPushCommands(t *testing.T) { buildCommand: emptyString, runCommand: "customcommand", execCommands: execCommands, - //only the specified run command is returned, because the build command is not marked as default + // only the specified run command is returned, because the build command is not marked as default numberOfCommands: 2, wantErr: false, }, diff --git a/pkg/service/service.go b/pkg/service/service.go index 32d4d79bdcc..fbee140444d 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -163,10 +163,11 @@ func listDevfileLinks(devfileObj parser.DevfileObj, context string, fs devfilefs } var services []string for _, c := range components { - u, err := libdevfile.GetK8sComponentAsUnstructured(devfileObj, c.Name, context, fs) + uList, err := libdevfile.GetK8sComponentAsUnstructuredList(devfileObj, c.Name, context, fs) if err != nil { return nil, err } + u := uList[0] if !isLinkResource(u.GetKind()) { continue } @@ -218,28 +219,30 @@ func PushKubernetesResources(client kclient.ClientInterface, devfileObj parser.D // create an object on the kubernetes cluster for all the Kubernetes Inlined components for _, c := range k8sComponents { - u, er := libdevfile.GetK8sComponentAsUnstructured(devfileObj, c.Name, context, devfilefs.DefaultFs{}) + uList, er := libdevfile.GetK8sComponentAsUnstructuredList(devfileObj, c.Name, context, devfilefs.DefaultFs{}) if er != nil { return er } - var found bool - currentOwnerReferences := u.GetOwnerReferences() - for _, ref := range currentOwnerReferences { - if ref.UID == reference.UID { - found = true - break + for _, u := range uList { + var found bool + currentOwnerReferences := u.GetOwnerReferences() + for _, ref := range currentOwnerReferences { + if ref.UID == reference.UID { + found = true + break + } + } + if !found { + currentOwnerReferences = append(currentOwnerReferences, reference) + u.SetOwnerReferences(currentOwnerReferences) + } + er = PushKubernetesResource(client, u, labels, annotations, mode) + if er != nil { + return er + } + if csvSupported { + delete(deployed, u.GetKind()+"/"+u.GetName()) } - } - if !found { - currentOwnerReferences = append(currentOwnerReferences, reference) - u.SetOwnerReferences(currentOwnerReferences) - } - er = PushKubernetesResource(client, u, labels, annotations, mode) - if er != nil { - return er - } - if csvSupported { - delete(deployed, u.GetKind()+"/"+u.GetName()) } } diff --git a/tests/examples/source/devfiles/nodejs/devfile-deploy-multiple-k8s-resources-in-single-component.yaml b/tests/examples/source/devfiles/nodejs/devfile-deploy-multiple-k8s-resources-in-single-component.yaml new file mode 100644 index 00000000000..5460a74d779 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-deploy-multiple-k8s-resources-in-single-component.yaml @@ -0,0 +1,123 @@ +commands: +- exec: + commandLine: npm install + component: runtime + group: + isDefault: true + kind: build + workingDir: /project + id: install +- exec: + commandLine: npm start + component: runtime + group: + isDefault: true + kind: run + workingDir: /project + id: run +- exec: + commandLine: npm run debug + component: runtime + group: + isDefault: true + kind: debug + workingDir: /project + id: debug +- exec: + commandLine: npm test + component: runtime + group: + isDefault: true + kind: test + workingDir: /project + id: test +- id: build-image + apply: + component: outerloop-build +- id: deployk8s + apply: + component: outerloop-deploy +- id: deploy + composite: + commands: + - build-image + - deployk8s + group: + kind: deploy + isDefault: true +components: +- container: + endpoints: + - name: http-3000 + targetPort: 3000 + image: registry.access.redhat.com/ubi8/nodejs-14:latest + memoryLimit: 1024Mi + mountSources: true + sourceMapping: /project + name: runtime +- name: outerloop-build + image: + imageName: "{{CONTAINER_IMAGE}}" + dockerfile: + uri: ./Dockerfile + buildContext: ${PROJECTS_ROOT} + rootRequired: false + +- name: outerloop-deploy + kubernetes: + inlined: | + kind: Deployment + apiVersion: apps/v1 + metadata: + name: my-component + spec: + replicas: 1 + selector: + matchLabels: + app: node-app + template: + metadata: + labels: + app: node-app + spec: + containers: + - name: main + image: {{CONTAINER_IMAGE}} + resources: + limits: + memory: "128Mi" + cpu: "500m" + --- + kind: Service + apiVersion: v1 + metadata: + name: my-component-svc + spec: + ports: + - name: http-8081 + port: 8081 + protocol: TCP + targetPort: 8081 + selector: + app: node-app + +metadata: + description: Stack with Node.js 14 + displayName: Node.js Runtime + icon: https://nodejs.org/static/images/logos/nodejs-new-pantone-black.svg + language: javascript + name: nodejs-prj1-api-abhz + projectType: nodejs + tags: + - NodeJS + - Express + - ubi8 + version: 1.0.1 +schemaVersion: 2.2.0 +starterProjects: +- git: + remotes: + origin: https://github.com/odo-devfiles/nodejs-ex.git + name: nodejs-starter +variables: + CONTAINER_IMAGE: quay.io/unknown-account/myimage diff --git a/tests/examples/source/devfiles/nodejs/devfile-with-multiple-k8s-resources-in-single-component.yaml b/tests/examples/source/devfiles/nodejs/devfile-with-multiple-k8s-resources-in-single-component.yaml new file mode 100644 index 00000000000..be909623fd1 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-with-multiple-k8s-resources-in-single-component.yaml @@ -0,0 +1,83 @@ +schemaVersion: 2.0.0 +metadata: + name: nodejs + projectType: nodejs + language: nodejs +starterProjects: + - name: nodejs-starter + git: + remotes: + origin: "https://github.com/odo-devfiles/nodejs-ex.git" +components: + - name: runtime + container: + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + memoryLimit: 1024Mi + endpoints: + - name: "3000-tcp" + targetPort: 3000 + mountSources: true + - name: deploy-k8s-resource + kubernetes: + inlined: | + kind: Deployment + apiVersion: apps/v1 + metadata: + name: my-component + spec: + replicas: 1 + selector: + matchLabels: + app: node-app + template: + metadata: + labels: + app: node-app + spec: + containers: + - name: main + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + resources: + limits: + memory: "128Mi" + cpu: "500m" + --- + kind: Deployment + apiVersion: apps/v1 + metadata: + name: my-component-2 + spec: + replicas: 1 + selector: + matchLabels: + app: node-app + template: + metadata: + labels: + app: node-app + spec: + containers: + - name: main + image: registry.access.redhat.com/ubi8/nodejs-12:1-36 + resources: + limits: + memory: "128Mi" + cpu: "500m" + +commands: + - id: build + exec: + component: runtime + commandLine: npm install + workingDir: ${PROJECTS_ROOT} + group: + kind: build + isDefault: true + - id: run + exec: + component: runtime + commandLine: npm start + workingDir: ${PROJECTS_ROOT} + group: + kind: run + isDefault: true diff --git a/tests/integration/cmd_describe_list_binding_test.go b/tests/integration/cmd_describe_list_binding_test.go index 22567c639b2..7358c553121 100644 --- a/tests/integration/cmd_describe_list_binding_test.go +++ b/tests/integration/cmd_describe_list_binding_test.go @@ -489,23 +489,6 @@ var _ = Describe("odo describe/list binding command tests", func() { cmpName := "my-nodejs-app" BeforeEach(func() { helper.Cmd("odo", "init", "--name", cmpName, "--devfile-path", ctx.devfile).ShouldPass() - - if ctx.isServiceNsSupported && ns != "" { - ns = commonVar.CliRunner.CreateAndSetRandNamespaceProject() - - commonVar.CliRunner.SetProject(commonVar.Project) - - helper.ReplaceString(filepath.Join(commonVar.Context, "devfile.yaml"), - "name: cluster-sample", - fmt.Sprintf(`name: cluster-sample - namespace: %s`, ns)) - } - }) - - AfterEach(func() { - if ctx.isServiceNsSupported && ns != "" { - commonVar.CliRunner.DeleteNamespaceProject(ns, false) - } }) When("Starting a Pg service", func() { @@ -517,11 +500,12 @@ var _ = Describe("odo describe/list binding command tests", func() { out, _ := commonVar.CliRunner.GetBindableKinds() return out }, 120, 3).Should(ContainSubstring("Cluster")) - addBindableKind := commonVar.CliRunner.Run("apply", "-f", helper.GetExamplePath("manifests", "bindablekind-instance.yaml")) - Expect(addBindableKind.ExitCode()).To(BeEquivalentTo(0)) - commonVar.CliRunner.EnsurePodIsUp(commonVar.Project, "cluster-sample-1") if ctx.isServiceNsSupported && ns != "" { + ns = commonVar.CliRunner.CreateAndSetRandNamespaceProject() + // Reset the original project + commonVar.CliRunner.SetProject(commonVar.Project) + addBindableKindInOtherNs := commonVar.CliRunner.Run("-n", ns, "apply", "-f", helper.GetExamplePath("manifests", "bindablekind-instance.yaml")) Expect(addBindableKindInOtherNs.ExitCode()).To(BeEquivalentTo(0)) @@ -530,6 +514,17 @@ var _ = Describe("odo describe/list binding command tests", func() { "name: cluster-sample", fmt.Sprintf(`name: cluster-sample namespace: %s`, ns)) + commonVar.CliRunner.EnsurePodIsUp(ns, "cluster-sample-1") + } else { + addBindableKind := commonVar.CliRunner.Run("apply", "-f", helper.GetExamplePath("manifests", "bindablekind-instance.yaml")) + Expect(addBindableKind.ExitCode()).To(BeEquivalentTo(0)) + commonVar.CliRunner.EnsurePodIsUp(commonVar.Project, "cluster-sample-1") + } + }) + + AfterEach(func() { + if ctx.isServiceNsSupported && ns != "" { + commonVar.CliRunner.DeleteNamespaceProject(ns, false) } }) diff --git a/tests/integration/cmd_dev_test.go b/tests/integration/cmd_dev_test.go index f2798cc3d85..792e850a55e 100644 --- a/tests/integration/cmd_dev_test.go +++ b/tests/integration/cmd_dev_test.go @@ -540,33 +540,53 @@ ComponentSettings: } }) - When("odo dev is executed to run a devfile containing a k8s resource ", func() { - var ( - devSession helper.DevSession - err error - getDeployArgs = []string{"get", "deployments", "-n", commonVar.Project} - ) - - const ( - deploymentName = "my-component" // hard-coded from the devfile-with-k8s-resource.yaml - ) + for _, ctx := range []struct { + title string + devfile string + matchResources []string + }{ + { + title: "odo dev is executed to run a devfile containing a k8s resource", + devfile: "devfile-with-k8s-resource.yaml", + matchResources: []string{"my-component"}, + }, + { + title: "odo dev is executed to run a devfile containing multiple k8s resource defined under a single Devfile component", + devfile: "devfile-with-multiple-k8s-resources-in-single-component.yaml", + matchResources: []string{"my-component", "my-component-2"}, + }, + } { + ctx := ctx + When(ctx.title, func() { + var ( + devSession helper.DevSession + out []byte + err error + getDeployArgs = []string{"get", "deployments", "-n", commonVar.Project} + ) - BeforeEach( - func() { + BeforeEach(func() { helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project"), commonVar.Context) - helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", "devfile-with-k8s-resource.yaml"), filepath.Join(commonVar.Context, "devfile.yaml")) - devSession, _, _, _, err = helper.StartDevMode(helper.DevSessionOpts{}) + helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", ctx.devfile), filepath.Join(commonVar.Context, "devfile.yaml")) + devSession, out, _, _, err = helper.StartDevMode(helper.DevSessionOpts{}) Expect(err).To(BeNil()) }) - AfterEach(func() { - devSession.Stop() - devSession.WaitEnd() - }) - It("should have created the k8s resource", func() { - Expect(commonVar.CliRunner.Run(getDeployArgs...).Out.Contents()).To(ContainSubstring(deploymentName)) + AfterEach(func() { + devSession.Stop() + devSession.WaitEnd() + }) + + It("should have created the necessary k8s resources", func() { + By("checking the output for the resources", func() { + helper.MatchAllInOutput(string(out), ctx.matchResources) + }) + By("fetching the resources from the cluster", func() { + helper.MatchAllInOutput(string(commonVar.CliRunner.Run(getDeployArgs...).Out.Contents()), ctx.matchResources) + }) + }) }) - }) + } for _, manual := range []bool{false, true} { for _, podman := range []bool{false, true} { diff --git a/tests/integration/cmd_devfile_deploy_test.go b/tests/integration/cmd_devfile_deploy_test.go index c9e0e8be166..fe57bc78e4b 100644 --- a/tests/integration/cmd_devfile_deploy_test.go +++ b/tests/integration/cmd_devfile_deploy_test.go @@ -259,7 +259,26 @@ ComponentSettings: }) } }) - + When("deploying a Devfile K8s component with multiple K8s resources defined", func() { + var out string + var resources []string + BeforeEach(func() { + helper.CopyExample(filepath.Join("source", "nodejs"), commonVar.Context) + helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", "devfile-deploy-multiple-k8s-resources-in-single-component.yaml"), filepath.Join(commonVar.Context, "devfile.yaml")) + out = helper.Cmd("odo", "deploy").AddEnv("PODMAN_CMD=echo").ShouldPass().Out() + resources = []string{"Deployment/my-component", "Service/my-component-svc"} + }) + It("should have created all the resources defined in the Devfile K8s component", func() { + By("checking the output", func() { + helper.MatchAllInOutput(out, resources) + }) + By("fetching the resources from the cluster", func() { + for _, resource := range resources { + Expect(commonVar.CliRunner.Run("get", resource).Out.Contents()).ToNot(BeEmpty()) + } + }) + }) + }) When("deploying a ServiceBinding k8s resource", func() { const serviceBindingName = "my-nodejs-app-cluster-sample" // hard-coded from devfile-deploy-with-SB.yaml BeforeEach(func() {