diff --git a/apis/controller/v1alpha1/devworkspacerouting_types.go b/apis/controller/v1alpha1/devworkspacerouting_types.go index a737393ac..4b364d203 100644 --- a/apis/controller/v1alpha1/devworkspacerouting_types.go +++ b/apis/controller/v1alpha1/devworkspacerouting_types.go @@ -175,6 +175,11 @@ type Endpoint struct { // +kubebuilder:pruning:PreserveUnknownFields // +kubebuilder:validation:Schemaless Attributes Attributes `json:"attributes,omitempty"` + // Map of annotations to be added to the Kubernetes Ingress or OpenShift Route associated with the endpoint. + // +optional + // +patchMergeKey=name + // +patchStrategy=merge + Annotations map[string]string `json:"annotations,omitempty"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/apis/controller/v1alpha1/zz_generated.deepcopy.go b/apis/controller/v1alpha1/zz_generated.deepcopy.go index d3ac6a6e0..c03da961a 100644 --- a/apis/controller/v1alpha1/zz_generated.deepcopy.go +++ b/apis/controller/v1alpha1/zz_generated.deepcopy.go @@ -269,6 +269,13 @@ func (in *Endpoint) DeepCopyInto(out *Endpoint) { (*out)[key] = *val.DeepCopy() } } + if in.Annotations != nil { + in, out := &in.Annotations, &out.Annotations + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Endpoint. diff --git a/controllers/controller/devworkspacerouting/conversion/conversion.go b/controllers/controller/devworkspacerouting/conversion/conversion.go index e0e668782..6c361f62f 100644 --- a/controllers/controller/devworkspacerouting/conversion/conversion.go +++ b/controllers/controller/devworkspacerouting/conversion/conversion.go @@ -46,13 +46,14 @@ func convertDevfileEndpoint(dwEndpoint dw.Endpoint) v1alpha1.Endpoint { } return v1alpha1.Endpoint{ - Name: dwEndpoint.Name, - TargetPort: dwEndpoint.TargetPort, - Exposure: endpointExposure, - Protocol: protocol, - Secure: convertSecure(dwEndpoint.Secure), - Path: dwEndpoint.Path, - Attributes: v1alpha1.Attributes(dwEndpoint.Attributes), + Name: dwEndpoint.Name, + TargetPort: dwEndpoint.TargetPort, + Exposure: endpointExposure, + Protocol: protocol, + Secure: convertSecure(dwEndpoint.Secure), + Path: dwEndpoint.Path, + Attributes: v1alpha1.Attributes(dwEndpoint.Attributes), + Annotations: dwEndpoint.Annotations, } } diff --git a/controllers/controller/devworkspacerouting/devworkspacerouting_controller_test.go b/controllers/controller/devworkspacerouting/devworkspacerouting_controller_test.go index 3853f02e3..8ec6125a5 100644 --- a/controllers/controller/devworkspacerouting/devworkspacerouting_controller_test.go +++ b/controllers/controller/devworkspacerouting/devworkspacerouting_controller_test.go @@ -174,6 +174,7 @@ var _ = Describe("DevWorkspaceRouting Controller", func() { expectedOwnerReference := devWorkspaceRoutingOwnerRef(createdDWR) Expect(createdIngress.OwnerReferences).Should(ContainElement(expectedOwnerReference), "Ingress should be owned by DevWorkspaceRouting") Expect(createdIngress.ObjectMeta.Annotations).Should(HaveKeyWithValue(constants.DevWorkspaceEndpointNameAnnotation, exposedEndPointName), "Ingress should have endpoint name annotation") + Expect(createdIngress.ObjectMeta.Annotations).Should(HaveKeyWithValue(endpointAnnotationKey, endpointAnnotationValue), "Ingress should have annotation from endpoint") By("Checking ingress points to service") createdService := &corev1.Service{} @@ -205,6 +206,7 @@ var _ = Describe("DevWorkspaceRouting Controller", func() { Expect(discoverableEndpointIngress.Labels).Should(Equal(ExpectedLabels), "Ingress should contain DevWorkspace ID label") Expect(discoverableEndpointIngress.OwnerReferences).Should(ContainElement(expectedOwnerReference), "Ingress should be owned by DevWorkspaceRouting") Expect(discoverableEndpointIngress.ObjectMeta.Annotations).Should(HaveKeyWithValue(constants.DevWorkspaceEndpointNameAnnotation, discoverableEndpointName), "Ingress should have endpoint name annotation") + Expect(discoverableEndpointIngress.ObjectMeta.Annotations).Should(HaveKeyWithValue(endpointAnnotationKey, endpointAnnotationValue), "Ingress should have annotation from endpoint") By("Checking ingress for discoverable endpoint points to service") Expect(len(discoverableEndpointIngress.Spec.Rules)).Should(Equal(1), "Expected only a single rule for the ingress") @@ -306,6 +308,7 @@ var _ = Describe("DevWorkspaceRouting Controller", func() { expectedOwnerReference := devWorkspaceRoutingOwnerRef(createdDWR) Expect(createdRoute.OwnerReferences).Should(ContainElement(expectedOwnerReference), "Route should be owned by DevWorkspaceRouting") Expect(createdRoute.ObjectMeta.Annotations).Should(HaveKeyWithValue(constants.DevWorkspaceEndpointNameAnnotation, exposedEndPointName), "Route should have endpoint name annotation") + Expect(createdRoute.ObjectMeta.Annotations).Should(HaveKeyWithValue(endpointAnnotationKey, endpointAnnotationValue), "Route should have annotation from endpoint") By("Checking route points to service") createdService := &corev1.Service{} @@ -335,6 +338,7 @@ var _ = Describe("DevWorkspaceRouting Controller", func() { Expect(discoverableRoute.Labels).Should(Equal(ExpectedLabels), "Route should contain DevWorkspace ID label") Expect(discoverableRoute.OwnerReferences).Should(ContainElement(expectedOwnerReference), "Route should be owned by DevWorkspaceRouting") Expect(discoverableRoute.ObjectMeta.Annotations).Should(HaveKeyWithValue(constants.DevWorkspaceEndpointNameAnnotation, discoverableEndpointName), "Route should have endpoint name annotation") + Expect(discoverableRoute.ObjectMeta.Annotations).Should(HaveKeyWithValue(endpointAnnotationKey, endpointAnnotationValue), "Route should have annotation from endpoint") By("Checking route for discoverable endpoint points to service") Expect(targetPorts).Should(ContainElement(discoverableRoute.Spec.Port.TargetPort), "Route target port should be in service target ports") diff --git a/controllers/controller/devworkspacerouting/solvers/basic_solver.go b/controllers/controller/devworkspacerouting/solvers/basic_solver.go index 2c897cbb3..9cbafc1f0 100644 --- a/controllers/controller/devworkspacerouting/solvers/basic_solver.go +++ b/controllers/controller/devworkspacerouting/solvers/basic_solver.go @@ -22,19 +22,25 @@ import ( "github.com/devfile/devworkspace-operator/pkg/infrastructure" ) -var routeAnnotations = func(endpointName string) map[string]string { - return map[string]string{ - "haproxy.router.openshift.io/rewrite-target": "/", - constants.DevWorkspaceEndpointNameAnnotation: endpointName, +var routeAnnotations = func(endpointName string, endpointAnnotations map[string]string) map[string]string { + annotations := make(map[string]string, len(endpointAnnotations)) + for k, v := range endpointAnnotations { + annotations[k] = v } + annotations["haproxy.router.openshift.io/rewrite-target"] = "/" + annotations[constants.DevWorkspaceEndpointNameAnnotation] = endpointName + return annotations } -var nginxIngressAnnotations = func(endpointName string) map[string]string { - return map[string]string{ - "nginx.ingress.kubernetes.io/rewrite-target": "/", - "nginx.ingress.kubernetes.io/ssl-redirect": "false", - constants.DevWorkspaceEndpointNameAnnotation: endpointName, +var nginxIngressAnnotations = func(endpointName string, endpointAnnotations map[string]string) map[string]string { + annotations := make(map[string]string, len(endpointAnnotations)) + for k, v := range endpointAnnotations { + annotations[k] = v } + annotations["nginx.ingress.kubernetes.io/rewrite-target"] = "/" + annotations["nginx.ingress.kubernetes.io/ssl-redirect"] = "false" + annotations[constants.DevWorkspaceEndpointNameAnnotation] = endpointName + return annotations } // Basic solver exposes endpoints without any authentication diff --git a/controllers/controller/devworkspacerouting/solvers/common.go b/controllers/controller/devworkspacerouting/solvers/common.go index 5d5a50378..b642874ec 100644 --- a/controllers/controller/devworkspacerouting/solvers/common.go +++ b/controllers/controller/devworkspacerouting/solvers/common.go @@ -190,7 +190,7 @@ func getRouteForEndpoint(routingSuffix string, endpoint controllerv1alpha1.Endpo Labels: map[string]string{ constants.DevWorkspaceIDLabel: meta.DevWorkspaceId, }, - Annotations: routeAnnotations(endpointName), + Annotations: routeAnnotations(endpointName, endpoint.Annotations), }, Spec: routeV1.RouteSpec{ Host: common.WorkspaceHostname(routingSuffix, meta.DevWorkspaceId), @@ -221,7 +221,7 @@ func getIngressForEndpoint(routingSuffix string, endpoint controllerv1alpha1.End Labels: map[string]string{ constants.DevWorkspaceIDLabel: meta.DevWorkspaceId, }, - Annotations: nginxIngressAnnotations(endpoint.Name), + Annotations: nginxIngressAnnotations(endpoint.Name, endpoint.Annotations), }, Spec: networkingv1.IngressSpec{ IngressClassName: pointer.String("nginx"), diff --git a/controllers/controller/devworkspacerouting/util_test.go b/controllers/controller/devworkspacerouting/util_test.go index c6e54887d..deb189afb 100644 --- a/controllers/controller/devworkspacerouting/util_test.go +++ b/controllers/controller/devworkspacerouting/util_test.go @@ -48,6 +48,9 @@ const ( discoverableTargetPort = 7979 nonExposedEndpointName = "non-exposed-endpoint" nonExposedTargetPort = 8989 + + endpointAnnotationKey = "endpoint-annotation-key" + endpointAnnotationValue = "endpoint-annotation-value" ) var ( @@ -59,12 +62,14 @@ func createDWR(workspaceID string, name string) *controllerv1alpha1.DevWorkspace mainAttributes.PutString("type", "main") discoverableAttributes := controllerv1alpha1.Attributes{} discoverableAttributes.PutBoolean(string(controllerv1alpha1.DiscoverableAttribute), true) + annotations := map[string]string{endpointAnnotationKey: endpointAnnotationValue} exposedEndpoint := controllerv1alpha1.Endpoint{ - Name: exposedEndPointName, - Exposure: controllerv1alpha1.PublicEndpointExposure, - Attributes: mainAttributes, - TargetPort: exposedTargetPort, + Name: exposedEndPointName, + Exposure: controllerv1alpha1.PublicEndpointExposure, + Attributes: mainAttributes, + TargetPort: exposedTargetPort, + Annotations: annotations, } nonExposedEndpoint := controllerv1alpha1.Endpoint{ Name: nonExposedEndpointName, @@ -72,13 +77,14 @@ func createDWR(workspaceID string, name string) *controllerv1alpha1.DevWorkspace TargetPort: nonExposedTargetPort, } discoverableEndpoint := controllerv1alpha1.Endpoint{ - Name: discoverableEndpointName, - Exposure: controllerv1alpha1.PublicEndpointExposure, - Attributes: discoverableAttributes, - TargetPort: discoverableTargetPort, + Name: discoverableEndpointName, + Exposure: controllerv1alpha1.PublicEndpointExposure, + Attributes: discoverableAttributes, + TargetPort: discoverableTargetPort, + Annotations: annotations, } machineEndpointsMap := map[string]controllerv1alpha1.EndpointList{ - "test-machine-name": { + testMachineName: { exposedEndpoint, nonExposedEndpoint, discoverableEndpoint, diff --git a/deploy/bundle/manifests/controller.devfile.io_devworkspaceroutings.yaml b/deploy/bundle/manifests/controller.devfile.io_devworkspaceroutings.yaml index df0e6e8f7..0acc7a82c 100644 --- a/deploy/bundle/manifests/controller.devfile.io_devworkspaceroutings.yaml +++ b/deploy/bundle/manifests/controller.devfile.io_devworkspaceroutings.yaml @@ -55,6 +55,11 @@ spec: additionalProperties: items: properties: + annotations: + additionalProperties: + type: string + description: Map of annotations to be added to the Kubernetes Ingress or OpenShift Route associated with the endpoint. + type: object attributes: description: "Map of implementation-dependant string-based free-form attributes. \n Examples of Che-specific attributes: \n - cookiesAuthEnabled: \"true\" / \"false\", \n - type: \"terminal\" / \"ide\" / \"ide-dev\"," type: object diff --git a/deploy/deployment/kubernetes/combined.yaml b/deploy/deployment/kubernetes/combined.yaml index bfb2feba2..5b4aacd14 100644 --- a/deploy/deployment/kubernetes/combined.yaml +++ b/deploy/deployment/kubernetes/combined.yaml @@ -3120,6 +3120,12 @@ spec: additionalProperties: items: properties: + annotations: + additionalProperties: + type: string + description: Map of annotations to be added to the Kubernetes + Ingress or OpenShift Route associated with the endpoint. + type: object attributes: description: "Map of implementation-dependant string-based free-form attributes. \n Examples of Che-specific attributes: diff --git a/deploy/deployment/kubernetes/objects/devworkspaceroutings.controller.devfile.io.CustomResourceDefinition.yaml b/deploy/deployment/kubernetes/objects/devworkspaceroutings.controller.devfile.io.CustomResourceDefinition.yaml index 3305b8d75..0afb95d78 100644 --- a/deploy/deployment/kubernetes/objects/devworkspaceroutings.controller.devfile.io.CustomResourceDefinition.yaml +++ b/deploy/deployment/kubernetes/objects/devworkspaceroutings.controller.devfile.io.CustomResourceDefinition.yaml @@ -60,6 +60,12 @@ spec: additionalProperties: items: properties: + annotations: + additionalProperties: + type: string + description: Map of annotations to be added to the Kubernetes + Ingress or OpenShift Route associated with the endpoint. + type: object attributes: description: "Map of implementation-dependant string-based free-form attributes. \n Examples of Che-specific attributes: diff --git a/deploy/deployment/openshift/combined.yaml b/deploy/deployment/openshift/combined.yaml index 918402d7c..88b87c841 100644 --- a/deploy/deployment/openshift/combined.yaml +++ b/deploy/deployment/openshift/combined.yaml @@ -3120,6 +3120,12 @@ spec: additionalProperties: items: properties: + annotations: + additionalProperties: + type: string + description: Map of annotations to be added to the Kubernetes + Ingress or OpenShift Route associated with the endpoint. + type: object attributes: description: "Map of implementation-dependant string-based free-form attributes. \n Examples of Che-specific attributes: diff --git a/deploy/deployment/openshift/objects/devworkspaceroutings.controller.devfile.io.CustomResourceDefinition.yaml b/deploy/deployment/openshift/objects/devworkspaceroutings.controller.devfile.io.CustomResourceDefinition.yaml index 3305b8d75..0afb95d78 100644 --- a/deploy/deployment/openshift/objects/devworkspaceroutings.controller.devfile.io.CustomResourceDefinition.yaml +++ b/deploy/deployment/openshift/objects/devworkspaceroutings.controller.devfile.io.CustomResourceDefinition.yaml @@ -60,6 +60,12 @@ spec: additionalProperties: items: properties: + annotations: + additionalProperties: + type: string + description: Map of annotations to be added to the Kubernetes + Ingress or OpenShift Route associated with the endpoint. + type: object attributes: description: "Map of implementation-dependant string-based free-form attributes. \n Examples of Che-specific attributes: diff --git a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceroutings.yaml b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceroutings.yaml index b2c6224ab..7023b99cd 100644 --- a/deploy/templates/crd/bases/controller.devfile.io_devworkspaceroutings.yaml +++ b/deploy/templates/crd/bases/controller.devfile.io_devworkspaceroutings.yaml @@ -58,6 +58,12 @@ spec: additionalProperties: items: properties: + annotations: + additionalProperties: + type: string + description: Map of annotations to be added to the Kubernetes + Ingress or OpenShift Route associated with the endpoint. + type: object attributes: description: "Map of implementation-dependant string-based\ \ free-form attributes. \n Examples of Che-specific attributes:\ diff --git a/docs/unsupported-devfile-api.adoc b/docs/unsupported-devfile-api.adoc index cdb5e915a..d23d5a3a0 100644 --- a/docs/unsupported-devfile-api.adoc +++ b/docs/unsupported-devfile-api.adoc @@ -6,7 +6,6 @@ The following features of the Devfile API that are not yet supported by the DevW |================================================================================================================================================================================================ | DevFile feature | Related issue | `components.container.annotation.service` | https://github.com/devfile/devworkspace-operator/issues/799[Support setting annotations on services/endpoints from DevWorkspace] -| `components.container.endpoints.annotations` | https://github.com/devfile/devworkspace-operator/issues/799[Support setting annotations on services/endpoints from DevWorkspace] | `components.container.dedicatedPod` | | `components.image` | https://github.com/eclipse/che/issues/21186[Support Devfile v2 outer loop components of type image and kubernetes] | `components.custom` | diff --git a/webhook/workspace/handler/testdata/warning/add-all-unsupported-features.yaml b/webhook/workspace/handler/testdata/warning/add-all-unsupported-features.yaml index 863e8fd4e..05c5614b0 100644 --- a/webhook/workspace/handler/testdata/warning/add-all-unsupported-features.yaml +++ b/webhook/workspace/handler/testdata/warning/add-all-unsupported-features.yaml @@ -19,12 +19,6 @@ input: annotation: service: key: value - endpoints: - - name: web - targetPort: 8080 - exposure: public - annotation: - key: value - name: projects volume: ephemeral: true @@ -43,15 +37,11 @@ input: custom: componentClass: "some-component-class" events: - preStop: - - eventA - - eventB - - eventC postStop: - eventD - eventE - eventF output: - expectedWarning: "Unsupported Devfile features are present in this workspace. The following features will have no effect: components[].container.annotation.service, used by components: testing-container-1; components[].container.endpoints[].annotations, used by components: testing-container-1; components[].container.dedicatedPod, used by components: testing-container-1; components[].image, used by components: image-component; components[].custom, used by components: custom-component; events.postStop: eventD, eventE, eventF" + expectedWarning: "Unsupported Devfile features are present in this workspace. The following features will have no effect: components[].container.annotation.service, used by components: testing-container-1; components[].container.dedicatedPod, used by components: testing-container-1; components[].image, used by components: image-component; components[].custom, used by components: custom-component; events.postStop: eventD, eventE, eventF" newWarningsPresent: true diff --git a/webhook/workspace/handler/warning.go b/webhook/workspace/handler/warning.go index a435f7796..d889b14e6 100644 --- a/webhook/workspace/handler/warning.go +++ b/webhook/workspace/handler/warning.go @@ -24,23 +24,21 @@ import ( ) type unsupportedWarnings struct { - serviceAnnotations map[string]bool - endpointAnnotations map[string]bool - dedicatedPod map[string]bool - imageComponent map[string]bool - customComponent map[string]bool - eventPostStop map[string]bool + serviceAnnotations map[string]bool + dedicatedPod map[string]bool + imageComponent map[string]bool + customComponent map[string]bool + eventPostStop map[string]bool } // Returns an initialized unsupportedWarnings struct func newUnsupportedWarnings() *unsupportedWarnings { return &unsupportedWarnings{ - serviceAnnotations: make(map[string]bool), - endpointAnnotations: make(map[string]bool), - dedicatedPod: make(map[string]bool), - imageComponent: make(map[string]bool), - customComponent: make(map[string]bool), - eventPostStop: make(map[string]bool), + serviceAnnotations: make(map[string]bool), + dedicatedPod: make(map[string]bool), + imageComponent: make(map[string]bool), + customComponent: make(map[string]bool), + eventPostStop: make(map[string]bool), } } @@ -51,11 +49,6 @@ func checkUnsupportedFeatures(devWorkspaceSpec dwv2.DevWorkspaceTemplateSpec) (w if component.Container.Annotation != nil && component.Container.Annotation.Service != nil { warnings.serviceAnnotations[component.Name] = true } - for _, endpoint := range component.Container.Endpoints { - if endpoint.Annotations != nil { - warnings.endpointAnnotations[component.Name] = true - } - } if component.Container.DedicatedPod != nil && *component.Container.DedicatedPod { warnings.dedicatedPod[component.Name] = true } @@ -80,7 +73,6 @@ func checkUnsupportedFeatures(devWorkspaceSpec dwv2.DevWorkspaceTemplateSpec) (w func unsupportedWarningsPresent(warnings *unsupportedWarnings) bool { return len(warnings.serviceAnnotations) > 0 || - len(warnings.endpointAnnotations) > 0 || len(warnings.dedicatedPod) > 0 || len(warnings.imageComponent) > 0 || len(warnings.customComponent) > 0 || @@ -104,10 +96,6 @@ func formatUnsupportedFeaturesWarning(warnings *unsupportedWarnings) string { serviceAnnotationsMsg := "components[].container.annotation.service, used by components: " + strings.Join(getWarningNames(warnings.serviceAnnotations), ", ") msg = append(msg, serviceAnnotationsMsg) } - if len(warnings.endpointAnnotations) > 0 { - endpointAnnotationsMsg := "components[].container.endpoints[].annotations, used by components: " + strings.Join(getWarningNames(warnings.endpointAnnotations), ", ") - msg = append(msg, endpointAnnotationsMsg) - } if len(warnings.dedicatedPod) > 0 { dedicatedPodMsg := "components[].container.dedicatedPod, used by components: " + strings.Join(getWarningNames(warnings.dedicatedPod), ", ") msg = append(msg, dedicatedPodMsg) @@ -145,7 +133,6 @@ func checkForAddedUnsupportedFeatures(oldWksp, newWksp *dwv2.DevWorkspace) *unsu } addedWarnings.serviceAnnotations = getAddedWarnings(oldWarnings.serviceAnnotations, newWarnings.serviceAnnotations) - addedWarnings.endpointAnnotations = getAddedWarnings(oldWarnings.endpointAnnotations, newWarnings.endpointAnnotations) addedWarnings.dedicatedPod = getAddedWarnings(oldWarnings.dedicatedPod, newWarnings.dedicatedPod) addedWarnings.imageComponent = getAddedWarnings(oldWarnings.imageComponent, newWarnings.imageComponent) addedWarnings.customComponent = getAddedWarnings(oldWarnings.customComponent, newWarnings.customComponent)