Skip to content
5 changes: 5 additions & 0 deletions apis/controller/v1alpha1/devworkspacerouting_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 7 additions & 0 deletions apis/controller/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -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,
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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{}
Expand Down Expand Up @@ -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")
Expand Down Expand Up @@ -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{}
Expand Down Expand Up @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down Expand Up @@ -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"),
Expand Down
24 changes: 15 additions & 9 deletions controllers/controller/devworkspacerouting/util_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ const (
discoverableTargetPort = 7979
nonExposedEndpointName = "non-exposed-endpoint"
nonExposedTargetPort = 8989

endpointAnnotationKey = "endpoint-annotation-key"
endpointAnnotationValue = "endpoint-annotation-value"
)

var (
Expand All @@ -59,26 +62,29 @@ 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,
Exposure: controllerv1alpha1.NoneEndpointExposure,
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,
Expand Down

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions deploy/deployment/kubernetes/combined.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions deploy/deployment/openshift/combined.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion docs/unsupported-devfile-api.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,6 @@ input:
annotation:
service:
key: value
endpoints:
- name: web
targetPort: 8080
exposure: public
annotation:
key: value
- name: projects
volume:
ephemeral: true
Expand All @@ -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
33 changes: 10 additions & 23 deletions webhook/workspace/handler/warning.go
Original file line number Diff line number Diff line change
Expand Up @@ -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),
}
}

Expand All @@ -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
}
Expand All @@ -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 ||
Expand All @@ -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)
Expand Down Expand Up @@ -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)
Expand Down