diff --git a/pkg/deploy/deploy.go b/pkg/deploy/deploy.go index 489d3065a91..54aee991f17 100644 --- a/pkg/deploy/deploy.go +++ b/pkg/deploy/deploy.go @@ -59,7 +59,7 @@ func (o *deployHandler) ApplyImage(img v1alpha2.Component) error { func (o *deployHandler) ApplyKubernetes(kubernetes v1alpha2.Component) error { // Validate if the GVRs represented by Kubernetes inlined components are supported by the underlying cluster - _, err := service.ValidateResourceExist(o.kubeClient, kubernetes, o.path) + _, err := service.ValidateResourceExist(o.kubeClient, o.devfileObj, kubernetes, o.path) if err != nil { return err } @@ -75,7 +75,7 @@ func (o *deployHandler) ApplyKubernetes(kubernetes v1alpha2.Component) error { odolabels.SetProjectType(annotations, component.GetComponentTypeFromDevfileMetadata(o.devfileObj.Data.GetMetadata())) // Get the Kubernetes component - u, err := libdevfile.GetK8sComponentAsUnstructured(kubernetes.Kubernetes, o.path, devfilefs.DefaultFs{}) + u, err := libdevfile.GetK8sComponentAsUnstructured(o.devfileObj, kubernetes.Name, o.path, devfilefs.DefaultFs{}) if err != nil { return err } diff --git a/pkg/devfile/adapters/kubernetes/component/adapter.go b/pkg/devfile/adapters/kubernetes/component/adapter.go index cb4ef5c6def..69acebfc0b9 100644 --- a/pkg/devfile/adapters/kubernetes/component/adapter.go +++ b/pkg/devfile/adapters/kubernetes/component/adapter.go @@ -196,13 +196,13 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) { } // validate if the GVRs represented by Kubernetes inlined components are supported by the underlying cluster - err = service.ValidateResourcesExist(a.Client, k8sComponents, a.Context) + err = service.ValidateResourcesExist(a.Client, a.Devfile, k8sComponents, a.Context) if err != nil { return err } // create the Kubernetes objects from the manifest and delete the ones not in the devfile - err = service.PushKubernetesResources(a.Client, k8sComponents, labels, annotations, a.Context) + err = service.PushKubernetesResources(a.Client, a.Devfile, k8sComponents, labels, annotations, a.Context) if err != nil { return fmt.Errorf("failed to create service(s) associated with the component: %w", err) } @@ -246,14 +246,14 @@ func (a Adapter) Push(parameters common.PushParameters) (err error) { // Update all services with owner references err = a.Client.TryWithBlockOwnerDeletion(ownerReference, func(ownerRef metav1.OwnerReference) error { - return service.UpdateServicesWithOwnerReferences(a.Client, k8sComponents, ownerRef, a.Context) + return service.UpdateServicesWithOwnerReferences(a.Client, a.Devfile, k8sComponents, ownerRef, a.Context) }) if err != nil { return err } // create the Kubernetes objects from the manifest and delete the ones not in the devfile - needRestart, err := service.PushLinks(a.Client, k8sComponents, labels, a.deployment, a.Context) + needRestart, err := service.PushLinks(a.Client, a.Devfile, k8sComponents, labels, a.deployment, a.Context) if err != nil { return fmt.Errorf("failed to create service(s) associated with the component: %w", err) } diff --git a/pkg/libdevfile/command_apply.go b/pkg/libdevfile/command_apply.go index 4b702011e84..cfcfb83d547 100644 --- a/pkg/libdevfile/command_apply.go +++ b/pkg/libdevfile/command_apply.go @@ -37,7 +37,7 @@ func (o *applyCommand) Execute(handler Handler) error { } if len(devfileComponents) != 1 { - return NewComponentsWithSameNameError() + return NewComponentsWithSameNameError(o.command.Apply.Component) } component, err := newComponent(o.devfileObj, devfileComponents[0]) diff --git a/pkg/libdevfile/component_kubernetes_utils.go b/pkg/libdevfile/component_kubernetes_utils.go index 9fded29bf00..7a715545055 100644 --- a/pkg/libdevfile/component_kubernetes_utils.go +++ b/pkg/libdevfile/component_kubernetes_utils.go @@ -6,19 +6,16 @@ import ( "github.com/devfile/library/pkg/devfile/parser/data/v2/common" devfilefs "github.com/devfile/library/pkg/testingutil/filesystem" "github.com/ghodss/yaml" - "github.com/redhat-developer/odo/pkg/util" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) // GetK8sComponentAsUnstructured parses the Inlined/URI K8s of the devfile K8s component -func GetK8sComponentAsUnstructured(component *v1alpha2.KubernetesComponent, context string, fs devfilefs.Filesystem) (unstructured.Unstructured, error) { - strCRD := component.Inlined - var err error - if component.Uri != "" { - strCRD, err = util.GetDataFromURI(component.Uri, context, fs) - if err != nil { - return unstructured.Unstructured{}, err - } +func GetK8sComponentAsUnstructured(devfileObj parser.DevfileObj, componentName string, + context string, fs devfilefs.Filesystem) (unstructured.Unstructured, error) { + + strCRD, err := GetK8sManifestWithVariablesSubstituted(devfileObj, componentName, context, fs) + if err != nil { + return unstructured.Unstructured{}, err } // convert the YAML definition into map[string]interface{} since it's needed to create dynamic resource @@ -40,7 +37,7 @@ func ListKubernetesComponents(devfileObj parser.DevfileObj, path string) (list [ var u unstructured.Unstructured for _, kComponent := range components { if kComponent.Kubernetes != nil { - u, err = GetK8sComponentAsUnstructured(kComponent.Kubernetes, path, devfilefs.DefaultFs{}) + u, err = GetK8sComponentAsUnstructured(devfileObj, kComponent.Name, path, devfilefs.DefaultFs{}) if err != nil { return } diff --git a/pkg/libdevfile/errors.go b/pkg/libdevfile/errors.go index f1dd26d7073..753cac6629d 100644 --- a/pkg/libdevfile/errors.go +++ b/pkg/libdevfile/errors.go @@ -64,14 +64,18 @@ func (e ComponentNotExistError) Error() string { return fmt.Sprintf("component %q does not exists", e.name) } -type ComponentsWithSameNameError struct{} +type ComponentsWithSameNameError struct { + name string +} -func NewComponentsWithSameNameError() ComponentsWithSameNameError { - return ComponentsWithSameNameError{} +func NewComponentsWithSameNameError(name string) ComponentsWithSameNameError { + return ComponentsWithSameNameError{ + name: name, + } } func (e ComponentsWithSameNameError) Error() string { - return "more than one component with the same name, should not happen" + return fmt.Sprintf("more than one component with the same name %q, should not happen", e.name) } // ComponentTypeNotFoundError is returned when no component with the specified type has been found in Devfile diff --git a/pkg/libdevfile/libdevfile.go b/pkg/libdevfile/libdevfile.go index 42befb90065..9a3ff5ce0a7 100644 --- a/pkg/libdevfile/libdevfile.go +++ b/pkg/libdevfile/libdevfile.go @@ -2,11 +2,17 @@ package libdevfile import ( "fmt" + "regexp" + "strings" "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" + "github.com/devfile/api/v2/pkg/validation/variables" "github.com/devfile/library/pkg/devfile/parser" "github.com/devfile/library/pkg/devfile/parser/data" "github.com/devfile/library/pkg/devfile/parser/data/v2/common" + devfilefs "github.com/devfile/library/pkg/testingutil/filesystem" + + "github.com/redhat-developer/odo/pkg/util" ) type Handler interface { @@ -193,3 +199,87 @@ func GetEndpointsFromDevfile(devfileObj parser.DevfileObj, ignoreExposures []v1a } return endpoints, nil } + +// GetK8sManifestWithVariablesSubstituted 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, + context string, fs devfilefs.Filesystem) (string, error) { + + components, err := devfileObj.Data.GetComponents(common.DevfileOptions{FilterByName: devfileCmpName}) + if err != nil { + return "", err + } + + if len(components) == 0 { + return "", NewComponentNotExistError(devfileCmpName) + } + + if len(components) != 1 { + return "", NewComponentsWithSameNameError(devfileCmpName) + } + + devfileCmp := components[0] + componentType, err := common.GetComponentType(devfileCmp) + if err != nil { + return "", err + } + + var content, uri string + switch componentType { + case v1alpha2.KubernetesComponentType: + content = devfileCmp.Kubernetes.Inlined + if devfileCmp.Kubernetes.Uri != "" { + uri = devfileCmp.Kubernetes.Uri + } + + case v1alpha2.OpenshiftComponentType: + content = devfileCmp.Openshift.Inlined + if devfileCmp.Openshift.Uri != "" { + uri = devfileCmp.Openshift.Uri + } + + default: + return "", fmt.Errorf("unexpected component type %s", componentType) + } + + if uri != "" { + return loadResourceManifestFromUriAndResolveVariables(devfileObj, uri, context, fs) + } + return substituteVariables(devfileObj.Data.GetDevfileWorkspaceSpec().Variables, content) +} + +func loadResourceManifestFromUriAndResolveVariables(devfileObj parser.DevfileObj, uri string, + context string, fs devfilefs.Filesystem) (string, error) { + content, err := util.GetDataFromURI(uri, context, fs) + if err != nil { + return content, err + } + return substituteVariables(devfileObj.Data.GetDevfileWorkspaceSpec().Variables, content) +} + +// substituteVariables validates the string for a global variable in the given `devfileObj` and replaces it. +// An error is returned if the string references an invalid variable key not defined in the Devfile object. +// +//Inspired from variables.validateAndReplaceDataWithVariable, which is unfortunately not exported +func substituteVariables(devfileVars map[string]string, val string) (string, error) { + // example of the regex: {{variable}} / {{ variable }} + matches := regexp.MustCompile(`\{\{\s*(.*?)\s*\}\}`).FindAllStringSubmatch(val, -1) + var invalidKeys []string + for _, match := range matches { + varValue, ok := devfileVars[match[1]] + if !ok { + invalidKeys = append(invalidKeys, match[1]) + } else { + val = strings.Replace(val, match[0], varValue, -1) + } + } + + if len(invalidKeys) > 0 { + return val, &variables.InvalidKeysError{Keys: invalidKeys} + } + + return val, nil +} diff --git a/pkg/libdevfile/libdevfile_test.go b/pkg/libdevfile/libdevfile_test.go index f6cc1ac8308..436c797d2ad 100644 --- a/pkg/libdevfile/libdevfile_test.go +++ b/pkg/libdevfile/libdevfile_test.go @@ -1,12 +1,14 @@ package libdevfile import ( + "os" "reflect" "testing" "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/devfile/library/pkg/devfile/parser" "github.com/devfile/library/pkg/devfile/parser/data" + devfileFileSystem "github.com/devfile/library/pkg/testingutil/filesystem" "github.com/golang/mock/gomock" "github.com/redhat-developer/odo/pkg/libdevfile/generator" "k8s.io/utils/pointer" @@ -436,3 +438,335 @@ func TestGetEndpointsFromDevfile(t *testing.T) { }) } } + +func TestGetK8sManifestWithVariablesSubstituted(t *testing.T) { + fakeFs := devfileFileSystem.NewFakeFs() + cmpName := "my-cmp-1" + for _, tt := range []struct { + name string + setupFunc func() error + devfileObjFunc func() parser.DevfileObj + wantErr bool + want string + }{ + { + name: "Missing Component", + devfileObjFunc: func() parser.DevfileObj { + devfileData, _ := data.NewDevfileData(string(data.APISchemaVersion220)) + cmp := generator.GetContainerComponent(generator.ContainerComponentParams{ + Name: "a-different-component", + }) + s := v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Components: []v1alpha2.Component{cmp}, + }, + } + devfileData.SetDevfileWorkspaceSpec(s) + return parser.DevfileObj{ + Data: devfileData, + } + }, + wantErr: true, + }, + { + name: "Multiple Components with the same name", + devfileObjFunc: func() parser.DevfileObj { + devfileData, _ := data.NewDevfileData(string(data.APISchemaVersion220)) + cmp1 := generator.GetContainerComponent(generator.ContainerComponentParams{ + Name: cmpName, + }) + cmp2 := generator.GetImageComponent(generator.ImageComponentParams{ + Name: cmpName, + }) + s := v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Components: []v1alpha2.Component{cmp1, cmp2}, + }, + } + devfileData.SetDevfileWorkspaceSpec(s) + return parser.DevfileObj{ + Data: devfileData, + } + }, + wantErr: true, + }, + { + name: "Container Component", + devfileObjFunc: func() parser.DevfileObj { + devfileData, _ := data.NewDevfileData(string(data.APISchemaVersion220)) + cmp := generator.GetContainerComponent(generator.ContainerComponentParams{ + Name: cmpName, + }) + s := v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Components: []v1alpha2.Component{cmp}, + }, + } + devfileData.SetDevfileWorkspaceSpec(s) + return parser.DevfileObj{ + Data: devfileData, + } + }, + wantErr: true, + }, + { + name: "Image Component", + devfileObjFunc: func() parser.DevfileObj { + devfileData, _ := data.NewDevfileData(string(data.APISchemaVersion220)) + cmp := generator.GetImageComponent(generator.ImageComponentParams{ + Name: cmpName, + }) + s := v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Components: []v1alpha2.Component{cmp}, + }, + } + devfileData.SetDevfileWorkspaceSpec(s) + return parser.DevfileObj{ + Data: devfileData, + } + }, + wantErr: true, + }, + { + name: "Kubernetes Component - Inlined with no variables", + devfileObjFunc: func() parser.DevfileObj { + devfileData, _ := data.NewDevfileData(string(data.APISchemaVersion220)) + cmp := generator.GetKubernetesComponent(generator.KubernetesComponentParams{ + Name: cmpName, + Kubernetes: &v1alpha2.KubernetesComponent{ + K8sLikeComponent: v1alpha2.K8sLikeComponent{ + K8sLikeComponentLocation: v1alpha2.K8sLikeComponentLocation{ + Inlined: "some-text-inlined", + }, + }, + }, + }) + s := v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Components: []v1alpha2.Component{cmp}, + }, + } + devfileData.SetDevfileWorkspaceSpec(s) + return parser.DevfileObj{ + Data: devfileData, + } + }, + wantErr: false, + want: "some-text-inlined", + }, + { + name: "Kubernetes Component - Inlined with variables", + devfileObjFunc: func() parser.DevfileObj { + devfileData, _ := data.NewDevfileData(string(data.APISchemaVersion220)) + cmp := generator.GetKubernetesComponent(generator.KubernetesComponentParams{ + Name: cmpName, + Kubernetes: &v1alpha2.KubernetesComponent{ + K8sLikeComponent: v1alpha2.K8sLikeComponent{ + K8sLikeComponentLocation: v1alpha2.K8sLikeComponentLocation{ + Inlined: "image: {{MY_CONTAINER_IMAGE}}", + }, + }, + }, + }) + s := v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Variables: map[string]string{ + "MY_CONTAINER_IMAGE": "quay.io/unknown-account/my-image:1.2.3", + }, + Components: []v1alpha2.Component{cmp}, + }, + } + devfileData.SetDevfileWorkspaceSpec(s) + return parser.DevfileObj{ + Data: devfileData, + } + }, + wantErr: false, + want: "image: quay.io/unknown-account/my-image:1.2.3", + }, + { + name: "Kubernetes Component - Inlined with unknown variables", + devfileObjFunc: func() parser.DevfileObj { + devfileData, _ := data.NewDevfileData(string(data.APISchemaVersion220)) + cmp := generator.GetKubernetesComponent(generator.KubernetesComponentParams{ + Name: cmpName, + Kubernetes: &v1alpha2.KubernetesComponent{ + K8sLikeComponent: v1alpha2.K8sLikeComponent{ + K8sLikeComponentLocation: v1alpha2.K8sLikeComponentLocation{ + Inlined: "image: {{MY_CONTAINER_IMAGE}}:{{ MY_CONTAINER_IMAGE_VERSION_UNKNOWN }}", + }, + }, + }, + }) + s := v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Variables: map[string]string{ + "MY_CONTAINER_IMAGE": "quay.io/unknown-account/my-image", + }, + Components: []v1alpha2.Component{cmp}, + }, + } + devfileData.SetDevfileWorkspaceSpec(s) + return parser.DevfileObj{ + Data: devfileData, + } + }, + wantErr: true, + want: "image: quay.io/unknown-account/my-image:{{ MY_CONTAINER_IMAGE_VERSION_UNKNOWN }}", + }, + { + name: "Kubernetes Component - non-existing external file", + devfileObjFunc: func() parser.DevfileObj { + devfileData, _ := data.NewDevfileData(string(data.APISchemaVersion220)) + cmp := generator.GetKubernetesComponent(generator.KubernetesComponentParams{ + Name: cmpName, + Kubernetes: &v1alpha2.KubernetesComponent{ + K8sLikeComponent: v1alpha2.K8sLikeComponent{ + K8sLikeComponentLocation: v1alpha2.K8sLikeComponentLocation{ + Uri: "kubernetes/my-external-file-with-should-not-exist", + }, + }, + }, + }) + s := v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Components: []v1alpha2.Component{cmp}, + }, + } + devfileData.SetDevfileWorkspaceSpec(s) + return parser.DevfileObj{ + Data: devfileData, + } + }, + wantErr: true, + }, + { + name: "Kubernetes Component - URI with no variables", + setupFunc: func() error { + return fakeFs.WriteFile("kubernetes/my-external-file", + []byte("some-text-with-no-variables"), + os.ModePerm) + }, + devfileObjFunc: func() parser.DevfileObj { + devfileData, _ := data.NewDevfileData(string(data.APISchemaVersion220)) + cmp := generator.GetKubernetesComponent(generator.KubernetesComponentParams{ + Name: cmpName, + Kubernetes: &v1alpha2.KubernetesComponent{ + K8sLikeComponent: v1alpha2.K8sLikeComponent{ + K8sLikeComponentLocation: v1alpha2.K8sLikeComponentLocation{ + Uri: "kubernetes/my-external-file", + }, + }, + }, + }) + s := v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Components: []v1alpha2.Component{cmp}, + }, + } + devfileData.SetDevfileWorkspaceSpec(s) + return parser.DevfileObj{ + Data: devfileData, + } + }, + wantErr: false, + want: "some-text-with-no-variables", + }, + { + name: "Kubernetes Component - URI with variables", + setupFunc: func() error { + return fakeFs.WriteFile("kubernetes/my-deployment.yaml", + []byte("image: {{ MY_CONTAINER_IMAGE }}:{{MY_CONTAINER_IMAGE_VERSION}}"), + os.ModePerm) + }, + devfileObjFunc: func() parser.DevfileObj { + devfileData, _ := data.NewDevfileData(string(data.APISchemaVersion220)) + cmp := generator.GetKubernetesComponent(generator.KubernetesComponentParams{ + Name: cmpName, + Kubernetes: &v1alpha2.KubernetesComponent{ + K8sLikeComponent: v1alpha2.K8sLikeComponent{ + K8sLikeComponentLocation: v1alpha2.K8sLikeComponentLocation{ + Uri: "kubernetes/my-deployment.yaml", + }, + }, + }, + }) + s := v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Variables: map[string]string{ + "MY_CONTAINER_IMAGE": "quay.io/unknown-account/my-image", + "MY_CONTAINER_IMAGE_VERSION": "1.2.3", + }, + Components: []v1alpha2.Component{cmp}, + }, + } + devfileData.SetDevfileWorkspaceSpec(s) + return parser.DevfileObj{ + Data: devfileData, + } + }, + wantErr: false, + want: "image: quay.io/unknown-account/my-image:1.2.3", + }, + { + name: "Kubernetes Component - URI with unknown variables", + setupFunc: func() error { + return fakeFs.WriteFile("kubernetes/my-external-file.yaml", + []byte("image: {{MY_CONTAINER_IMAGE}}:{{ MY_CONTAINER_IMAGE_VERSION_UNKNOWN }}"), + os.ModePerm) + }, + devfileObjFunc: func() parser.DevfileObj { + devfileData, _ := data.NewDevfileData(string(data.APISchemaVersion220)) + cmp := generator.GetKubernetesComponent(generator.KubernetesComponentParams{ + Name: cmpName, + Kubernetes: &v1alpha2.KubernetesComponent{ + K8sLikeComponent: v1alpha2.K8sLikeComponent{ + K8sLikeComponentLocation: v1alpha2.K8sLikeComponentLocation{ + Uri: "kubernetes/my-external-file.yaml", + }, + }, + }, + }) + s := v1alpha2.DevWorkspaceTemplateSpec{ + DevWorkspaceTemplateSpecContent: v1alpha2.DevWorkspaceTemplateSpecContent{ + Variables: map[string]string{ + "MY_CONTAINER_IMAGE": "quay.io/unknown-account/my-image", + }, + Components: []v1alpha2.Component{cmp}, + }, + } + devfileData.SetDevfileWorkspaceSpec(s) + return parser.DevfileObj{ + Data: devfileData, + } + }, + wantErr: true, + want: "image: quay.io/unknown-account/my-image:{{ MY_CONTAINER_IMAGE_VERSION_UNKNOWN }}", + }, + } { + t.Run(tt.name, func(t *testing.T) { + if tt.setupFunc != nil { + if err := tt.setupFunc(); err != nil { + t.Errorf("setup function returned an error: %v", err) + return + } + } + if tt.devfileObjFunc == nil { + t.Error("devfileObjFunc function not defined for test case") + return + } + + got, err := GetK8sManifestWithVariablesSubstituted(tt.devfileObjFunc(), cmpName, "", fakeFs) + if (err != nil) != tt.wantErr { + t.Errorf("GetK8sManifestWithVariablesSubstituted() error = %v, wantErr %v", + err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("GetK8sManifestWithVariablesSubstituted() got = %v, want %v", + got, tt.want) + } + }) + } +} diff --git a/pkg/service/link.go b/pkg/service/link.go index 37007efa648..ab55a841669 100644 --- a/pkg/service/link.go +++ b/pkg/service/link.go @@ -5,7 +5,8 @@ import ( "fmt" "strings" - "github.com/redhat-developer/odo/pkg/util" + "github.com/devfile/library/pkg/devfile/parser" + "github.com/redhat-developer/odo/pkg/libdevfile" devfile "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2" "github.com/devfile/library/pkg/devfile/generator" @@ -32,7 +33,7 @@ import ( // PushLinks updates Link(s) from Kubernetes Inlined component in a devfile by creating new ones or removing old ones // returns true if the component needs to be restarted (when a link has been created or deleted) // if service binding operator is not present, it will call pushLinksWithoutOperator to create the links without it. -func PushLinks(client kclient.ClientInterface, k8sComponents []devfile.Component, labels map[string]string, deployment *v1.Deployment, context string) (bool, error) { +func PushLinks(client kclient.ClientInterface, devfileObj parser.DevfileObj, k8sComponents []devfile.Component, labels map[string]string, deployment *v1.Deployment, context string) (bool, error) { serviceBindingSupport, err := client.IsServiceBindingSupported() if err != nil { return false, err @@ -40,15 +41,15 @@ func PushLinks(client kclient.ClientInterface, k8sComponents []devfile.Component if !serviceBindingSupport { klog.V(4).Info("Service Binding Operator is not installed on cluster. Service Binding will be created by odo using SB library.") - return pushLinksWithoutOperator(client, k8sComponents, labels, deployment, context) + return pushLinksWithoutOperator(client, devfileObj, k8sComponents, labels, deployment, context) } - return pushLinksWithOperator(client, k8sComponents, labels, deployment, context) + return pushLinksWithOperator(client, devfileObj, k8sComponents, labels, deployment, context) } // pushLinksWithOperator creates links or deletes links (if service binding operator is installed) between components and services // returns true if the component needs to be restarted (a secret was generated and added to the deployment) -func pushLinksWithOperator(client kclient.ClientInterface, k8sComponents []devfile.Component, labels map[string]string, deployment *v1.Deployment, context string) (bool, error) { +func pushLinksWithOperator(client kclient.ClientInterface, devfileObj parser.DevfileObj, k8sComponents []devfile.Component, labels map[string]string, deployment *v1.Deployment, context string) (bool, error) { ownerReference := generator.GetOwnerReference(deployment) deployed, err := ListDeployedServices(client, labels) @@ -65,14 +66,12 @@ func pushLinksWithOperator(client kclient.ClientInterface, k8sComponents []devfi restartNeeded := false // create an object on the kubernetes cluster for all the Kubernetes Inlined components + var strCRD string for _, c := range k8sComponents { // get the string representation of the YAML definition of a CRD - strCRD := c.Kubernetes.Inlined - if c.Kubernetes.Uri != "" { - strCRD, err = util.GetDataFromURI(c.Kubernetes.Uri, context, devfilefs.DefaultFs{}) - if err != nil { - return false, err - } + strCRD, err = libdevfile.GetK8sManifestWithVariablesSubstituted(devfileObj, c.Name, context, devfilefs.DefaultFs{}) + if err != nil { + return false, err } // convert the YAML definition into map[string]interface{} since it's needed to create dynamic resource @@ -127,7 +126,7 @@ func pushLinksWithOperator(client kclient.ClientInterface, k8sComponents []devfi // pushLinksWithoutOperator creates links or deletes links (if service binding operator is not installed) between components and services // returns true if the component needs to be restarted (a secret was generated and added to the deployment) -func pushLinksWithoutOperator(client kclient.ClientInterface, k8sComponents []devfile.Component, labels map[string]string, deployment *v1.Deployment, context string) (bool, error) { +func pushLinksWithoutOperator(client kclient.ClientInterface, devfileObj parser.DevfileObj, k8sComponents []devfile.Component, labels map[string]string, deployment *v1.Deployment, context string) (bool, error) { // check csv support before proceeding csvSupport, err := client.IsCSVSupported() @@ -151,14 +150,12 @@ func pushLinksWithoutOperator(client kclient.ClientInterface, k8sComponents []de localLinksMap := make(map[string]string) // create an object on the kubernetes cluster for all the Kubernetes Inlined components + var strCRD string for _, c := range k8sComponents { // get the string representation of the YAML definition of a CRD - strCRD := c.Kubernetes.Inlined - if c.Kubernetes.Uri != "" { - strCRD, err = util.GetDataFromURI(c.Kubernetes.Uri, context, devfilefs.DefaultFs{}) - if err != nil { - return false, err - } + strCRD, err = libdevfile.GetK8sManifestWithVariablesSubstituted(devfileObj, c.Name, context, devfilefs.DefaultFs{}) + if err != nil { + return false, err } // convert the YAML definition into map[string]interface{} since it's needed to create dynamic resource diff --git a/pkg/service/service.go b/pkg/service/service.go index 2c5e3fb938b..8b6c864f158 100644 --- a/pkg/service/service.go +++ b/pkg/service/service.go @@ -154,7 +154,7 @@ func listDevfileLinks(devfileObj parser.DevfileObj, context string, fs devfilefs } var services []string for _, c := range components { - u, err := libdevfile.GetK8sComponentAsUnstructured(c.Kubernetes, context, fs) + u, err := libdevfile.GetK8sComponentAsUnstructured(devfileObj, c.Name, context, fs) if err != nil { return nil, err } @@ -185,7 +185,7 @@ func listDevfileLinks(devfileObj parser.DevfileObj, context string, fs devfilefs } // PushKubernetesResources updates service(s) from Kubernetes Inlined component in a devfile by creating new ones or removing old ones -func PushKubernetesResources(client kclient.ClientInterface, k8sComponents []devfile.Component, labels map[string]string, annotations map[string]string, context string) error { +func PushKubernetesResources(client kclient.ClientInterface, devfileObj parser.DevfileObj, k8sComponents []devfile.Component, labels map[string]string, annotations map[string]string, context string) error { // check csv support before proceeding csvSupported, err := client.IsCSVSupported() if err != nil { @@ -209,7 +209,7 @@ func PushKubernetesResources(client kclient.ClientInterface, k8sComponents []dev // create an object on the kubernetes cluster for all the Kubernetes Inlined components for _, c := range k8sComponents { - u, er := libdevfile.GetK8sComponentAsUnstructured(c.Kubernetes, context, devfilefs.DefaultFs{}) + u, er := libdevfile.GetK8sComponentAsUnstructured(devfileObj, c.Name, context, devfilefs.DefaultFs{}) if er != nil { return er } @@ -327,10 +327,10 @@ func ListDeployedServices(client kclient.ClientInterface, labels map[string]stri // UpdateServicesWithOwnerReferences adds an owner reference to an inlined Kubernetes resource (except service binding objects) // if not already present in the list of owner references -func UpdateServicesWithOwnerReferences(client kclient.ClientInterface, k8sComponents []devfile.Component, ownerReference metav1.OwnerReference, context string) error { +func UpdateServicesWithOwnerReferences(client kclient.ClientInterface, devfileObj parser.DevfileObj, k8sComponents []devfile.Component, ownerReference metav1.OwnerReference, context string) error { for _, c := range k8sComponents { // get the string representation of the YAML definition of a CRD - u, err := libdevfile.GetK8sComponentAsUnstructured(c.Kubernetes, context, devfilefs.DefaultFs{}) + u, err := libdevfile.GetK8sComponentAsUnstructured(devfileObj, c.Name, context, devfilefs.DefaultFs{}) if err != nil { return err } @@ -398,14 +398,14 @@ func createOperatorService(client kclient.ClientInterface, u unstructured.Unstru } // ValidateResourcesExist validates if the Kubernetes inlined components are installed on the cluster -func ValidateResourcesExist(client kclient.ClientInterface, k8sComponents []devfile.Component, context string) error { +func ValidateResourcesExist(client kclient.ClientInterface, devfileObj parser.DevfileObj, k8sComponents []devfile.Component, context string) error { if len(k8sComponents) == 0 { return nil } var unsupportedResources []string for _, c := range k8sComponents { - kindErr, err := ValidateResourceExist(client, c, context) + kindErr, err := ValidateResourceExist(client, devfileObj, c, context) if err != nil { if kindErr != "" { unsupportedResources = append(unsupportedResources, kindErr) @@ -422,9 +422,9 @@ func ValidateResourcesExist(client kclient.ClientInterface, k8sComponents []devf return nil } -func ValidateResourceExist(client kclient.ClientInterface, k8sComponent devfile.Component, context string) (kindErr string, err error) { +func ValidateResourceExist(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(k8sComponent.Kubernetes, context, devfilefs.DefaultFs{}) + u, err := libdevfile.GetK8sComponentAsUnstructured(devfileObj, k8sComponent.Name, context, devfilefs.DefaultFs{}) if err != nil { return "", err } diff --git a/tests/examples/source/devfiles/nodejs/devfile-deploy-with-k8s-uri.yaml b/tests/examples/source/devfiles/nodejs/devfile-deploy-with-k8s-uri.yaml new file mode 100644 index 00000000000..54eb4d90f59 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/devfile-deploy-with-k8s-uri.yaml @@ -0,0 +1,89 @@ +schemaVersion: 2.2.0 +metadata: + description: Stack with Node.js 16 + 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 + +starterProjects: + - git: + remotes: + origin: https://github.com/odo-devfiles/nodejs-ex.git + name: nodejs-starter + +variables: + CONTAINER_IMAGE: quay.io/unknown-account/myimage + +commands: +- exec: + commandLine: npm install + component: runtime + group: + isDefault: true + kind: build + workingDir: $PROJECT_SOURCE + id: install +- exec: + commandLine: npm start + component: runtime + group: + isDefault: true + kind: run + workingDir: $PROJECT_SOURCE + id: run +- exec: + commandLine: npm run debug + component: runtime + group: + isDefault: true + kind: debug + workingDir: $PROJECT_SOURCE + id: debug +- exec: + commandLine: npm test + component: runtime + group: + isDefault: true + kind: test + workingDir: $PROJECT_SOURCE + 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: +- name: runtime + container: + endpoints: + - name: http-3000 + targetPort: 3000 + image: registry.access.redhat.com/ubi8/nodejs-16-minimal:latest + memoryLimit: 1024Mi + mountSources: true + sourceMapping: $PROJECT_SOURCE +- name: outerloop-build + image: + imageName: "{{CONTAINER_IMAGE}}" + dockerfile: + uri: ./Dockerfile +- name: outerloop-deploy + kubernetes: + uri: 'kubernetes/outerloop-deployment.yaml' diff --git a/tests/examples/source/devfiles/nodejs/project/kubernetes/outerloop-deployment.yaml b/tests/examples/source/devfiles/nodejs/project/kubernetes/outerloop-deployment.yaml new file mode 100644 index 00000000000..2a155d1a780 --- /dev/null +++ b/tests/examples/source/devfiles/nodejs/project/kubernetes/outerloop-deployment.yaml @@ -0,0 +1,23 @@ +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 + # CONTAINER_IMAGE is a variable defined in Devfile, and it needs to be substituted accordingly. + # See https://github.com/redhat-developer/odo/issues/5451 + image: {{CONTAINER_IMAGE}} + resources: + limits: + memory: "128Mi" + cpu: "500m" diff --git a/tests/integration/devfile/cmd_devfile_deploy_test.go b/tests/integration/devfile/cmd_devfile_deploy_test.go index 4e6b38a90eb..5be778a2673 100644 --- a/tests/integration/devfile/cmd_devfile_deploy_test.go +++ b/tests/integration/devfile/cmd_devfile_deploy_test.go @@ -40,68 +40,97 @@ var _ = Describe("odo devfile deploy command tests", func() { }) }) - When("using a devfile.yaml containing a deploy command", func() { - // from devfile - cmpName := "nodejs-prj1-api-abhz" - deploymentName := "my-component" - BeforeEach(func() { - helper.CopyExample(filepath.Join("source", "nodejs"), commonVar.Context) - helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", "devfile-deploy.yaml"), path.Join(commonVar.Context, "devfile.yaml")) - }) + for _, ctx := range []struct { + title string + devfileName string + setupFunc func() + }{ + { + title: "using a devfile.yaml containing a deploy command", + devfileName: "devfile-deploy.yaml", + setupFunc: nil, + }, + { + title: "using a devfile.yaml containing an outer-loop Kubernetes component referenced via an URI", + devfileName: "devfile-deploy-with-k8s-uri.yaml", + setupFunc: func() { + helper.CopyExample(filepath.Join("source", "devfiles", "nodejs", "project", "kubernetes"), + filepath.Join(commonVar.Context, "kubernetes")) + }, + }, + } { - When("running odo deploy", func() { - var stdout string + When(ctx.title, func() { + // from devfile + cmpName := "nodejs-prj1-api-abhz" + deploymentName := "my-component" BeforeEach(func() { - stdout = helper.Cmd("odo", "deploy").AddEnv("PODMAN_CMD=echo").ShouldPass().Out() - // An ENV file should have been created indicating current namespace - Expect(helper.VerifyFileExists(".odo/env/env.yaml")).To(BeTrue()) - helper.FileShouldContainSubstring(".odo/env/env.yaml", "Project: "+commonVar.Project) + helper.CopyExample(filepath.Join("source", "nodejs"), commonVar.Context) + helper.CopyExampleDevFile(filepath.Join("source", "devfiles", "nodejs", ctx.devfileName), + path.Join(commonVar.Context, "devfile.yaml")) + if ctx.setupFunc != nil { + ctx.setupFunc() + } }) - It("should succeed", func() { - By("building and pushing image to registry", func() { - Expect(stdout).To(ContainSubstring("build -t quay.io/unknown-account/myimage -f " + filepath.Join(commonVar.Context, "Dockerfile ") + commonVar.Context)) - Expect(stdout).To(ContainSubstring("push quay.io/unknown-account/myimage")) + + When("running odo deploy", func() { + var stdout string + BeforeEach(func() { + stdout = helper.Cmd("odo", "deploy").AddEnv("PODMAN_CMD=echo").ShouldPass().Out() + // An ENV file should have been created indicating current namespace + Expect(helper.VerifyFileExists(".odo/env/env.yaml")).To(BeTrue()) + helper.FileShouldContainSubstring(".odo/env/env.yaml", "Project: "+commonVar.Project) }) - By("deploying a deployment with the built image", func() { - out := commonVar.CliRunner.Run("get", "deployment", deploymentName, "-n", commonVar.Project, "-o", `jsonpath="{.spec.template.spec.containers[0].image}"`).Wait().Out.Contents() - Expect(out).To(ContainSubstring("quay.io/unknown-account/myimage")) + It("should succeed", func() { + By("building and pushing image to registry", func() { + Expect(stdout).To(ContainSubstring("build -t quay.io/unknown-account/myimage -f " + + filepath.Join(commonVar.Context, "Dockerfile ") + commonVar.Context)) + Expect(stdout).To(ContainSubstring("push quay.io/unknown-account/myimage")) + }) + By("deploying a deployment with the built image", func() { + out := commonVar.CliRunner.Run("get", "deployment", deploymentName, "-n", + commonVar.Project, "-o", `jsonpath="{.spec.template.spec.containers[0].image}"`).Wait().Out.Contents() + Expect(out).To(ContainSubstring("quay.io/unknown-account/myimage")) + }) }) - }) - It("should run odo dev successfully", func() { - session, _, _, _, err := helper.StartDevMode() - Expect(err).ToNot(HaveOccurred()) - session.Kill() - session.WaitEnd() - }) + It("should run odo dev successfully", func() { + session, _, _, _, err := helper.StartDevMode() + Expect(err).ToNot(HaveOccurred()) + session.Kill() + session.WaitEnd() + }) - When("deleting previous deployment and switching kubeconfig to another namespace", func() { - var otherNS string - BeforeEach(func() { - helper.Cmd("odo", "delete", "component", "--name", cmpName, "-f").ShouldPass() - output := commonVar.CliRunner.Run("get", "deployment", "-n", commonVar.Project).Err.Contents() - Expect(string(output)).To(ContainSubstring("No resources found in " + commonVar.Project + " namespace.")) + When("deleting previous deployment and switching kubeconfig to another namespace", func() { + var otherNS string + BeforeEach(func() { + helper.Cmd("odo", "delete", "component", "--name", cmpName, "-f").ShouldPass() + output := commonVar.CliRunner.Run("get", "deployment", "-n", commonVar.Project).Err.Contents() + Expect(string(output)).To( + ContainSubstring("No resources found in " + commonVar.Project + " namespace.")) - otherNS = commonVar.CliRunner.CreateAndSetRandNamespaceProject() - }) + otherNS = commonVar.CliRunner.CreateAndSetRandNamespaceProject() + }) - AfterEach(func() { - commonVar.CliRunner.DeleteNamespaceProject(otherNS) - }) + AfterEach(func() { + commonVar.CliRunner.DeleteNamespaceProject(otherNS) + }) - It("should run odo deploy on initial namespace", func() { - helper.Cmd("odo", "deploy").AddEnv("PODMAN_CMD=echo").ShouldPass() + It("should run odo deploy on initial namespace", func() { + helper.Cmd("odo", "deploy").AddEnv("PODMAN_CMD=echo").ShouldPass() - output := commonVar.CliRunner.Run("get", "deployment").Err.Contents() - Expect(string(output)).To(ContainSubstring("No resources found in " + otherNS + " namespace.")) + output := commonVar.CliRunner.Run("get", "deployment").Err.Contents() + Expect(string(output)).To( + ContainSubstring("No resources found in " + otherNS + " namespace.")) - output = commonVar.CliRunner.Run("get", "deployment", "-n", commonVar.Project).Out.Contents() - Expect(string(output)).To(ContainSubstring(deploymentName)) - }) + output = commonVar.CliRunner.Run("get", "deployment", "-n", commonVar.Project).Out.Contents() + Expect(string(output)).To(ContainSubstring(deploymentName)) + }) + }) }) }) - }) + } When("using a devfile.yaml containing two deploy commands", func() { BeforeEach(func() {