Skip to content

Commit d7a9843

Browse files
committed
feat : support service annotations in basic solver
Signed-off-by: Rohan Kumar <rohaan@redhat.com>
1 parent 724d742 commit d7a9843

10 files changed

Lines changed: 325 additions & 40 deletions

File tree

controllers/controller/devworkspacerouting/devworkspacerouting_controller_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ var _ = Describe("DevWorkspaceRouting Controller", func() {
114114
err := k8sClient.Get(ctx, serviceNamespacedName, createdService)
115115
return err == nil
116116
}, timeout, interval).Should(BeTrue(), "Service should exist in cluster")
117+
Expect(createdService.ObjectMeta.Annotations).Should(HaveKeyWithValue(serviceAnnotationKey, serviceAnnotationValue), "Service should have annotation")
117118
Expect(createdService.Spec.Selector).Should(Equal(createdDWR.Spec.PodSelector), "Service should have pod selector from DevWorkspace metadata")
118119
Expect(createdService.Labels).Should(Equal(ExpectedLabels), "Service should contain DevWorkspace ID label")
119120
expectedOwnerReference := devWorkspaceRoutingOwnerRef(createdDWR)

controllers/controller/devworkspacerouting/solvers/basic_solver.go

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,12 +43,38 @@ var nginxIngressAnnotations = func(endpointName string, endpointAnnotations map[
4343
return annotations
4444
}
4545

46+
func serviceAnnotations(sourceAnnotations map[string]string, isDiscoverable bool, serviceRoutingConfig controllerv1alpha1.Service) map[string]string {
47+
annotations := make(map[string]string)
48+
if sourceAnnotations != nil && len(sourceAnnotations) > 0 {
49+
for k, v := range sourceAnnotations {
50+
annotations[k] = v
51+
}
52+
}
53+
if isDiscoverable {
54+
annotations[constants.DevWorkspaceDiscoverableServiceAnnotation] = "true"
55+
}
56+
if serviceRoutingConfig.Annotations != nil && len(serviceRoutingConfig.Annotations) > 0 {
57+
for k, v := range serviceRoutingConfig.Annotations {
58+
annotations[k] = v
59+
}
60+
}
61+
return annotations
62+
}
63+
4664
// Basic solver exposes endpoints without any authentication
4765
// According to the current cluster there is different behavior:
4866
// Kubernetes: use Ingresses without TLS
4967
// OpenShift: use Routes with TLS enabled
5068
type BasicSolver struct{}
5169

70+
var routingSuffixSupplier = func() string {
71+
return config.GetGlobalConfig().Routing.ClusterHostSuffix
72+
}
73+
74+
var isOpenShift = func() bool {
75+
return infrastructure.IsOpenShift()
76+
}
77+
5278
var _ RoutingSolver = (*BasicSolver)(nil)
5379

5480
func (s *BasicSolver) FinalizerRequired(*controllerv1alpha1.DevWorkspaceRouting) bool {
@@ -63,16 +89,16 @@ func (s *BasicSolver) GetSpecObjects(routing *controllerv1alpha1.DevWorkspaceRou
6389
routingObjects := RoutingObjects{}
6490

6591
// TODO: Use workspace-scoped ClusterHostSuffix to allow overriding
66-
routingSuffix := config.GetGlobalConfig().Routing.ClusterHostSuffix
92+
routingSuffix := routingSuffixSupplier()
6793
if routingSuffix == "" {
6894
return routingObjects, &RoutingInvalid{"basic routing requires .config.routing.clusterHostSuffix to be set in operator config"}
6995
}
7096

7197
spec := routing.Spec
72-
services := getServicesForEndpoints(spec.Endpoints, workspaceMeta)
73-
services = append(services, GetDiscoverableServicesForEndpoints(spec.Endpoints, workspaceMeta)...)
98+
services := getServicesForEndpoints(spec, workspaceMeta)
99+
services = append(services, GetDiscoverableServicesForEndpoints(spec, workspaceMeta)...)
74100
routingObjects.Services = services
75-
if infrastructure.IsOpenShift() {
101+
if isOpenShift() {
76102
routingObjects.Routes = getRoutesForSpec(routingSuffix, spec.Endpoints, workspaceMeta)
77103
} else {
78104
routingObjects.Ingresses = getIngressesForSpec(routingSuffix, spec.Endpoints, workspaceMeta)
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
package solvers
2+
3+
import (
4+
"testing"
5+
6+
"github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
7+
"github.com/stretchr/testify/assert"
8+
corev1 "k8s.io/api/core/v1"
9+
networkingv1 "k8s.io/api/networking/v1"
10+
apiext "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
11+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
12+
"k8s.io/apimachinery/pkg/util/intstr"
13+
)
14+
15+
func TestServiceAnnotations(t *testing.T) {
16+
tests := []struct {
17+
name string
18+
sourceAnnotations map[string]string
19+
isDiscoverable bool
20+
serviceRoutingConfig v1alpha1.Service
21+
expectedAnnotations map[string]string
22+
}{
23+
{
24+
name: "No annotations provided and discoverable disabled should return empty",
25+
sourceAnnotations: nil,
26+
isDiscoverable: false,
27+
serviceRoutingConfig: v1alpha1.Service{
28+
Annotations: nil,
29+
},
30+
expectedAnnotations: map[string]string{},
31+
},
32+
{
33+
name: "Source annotations present and discoverable enabled should return source annotations",
34+
sourceAnnotations: map[string]string{
35+
"key1": "value1",
36+
"key2": "value2",
37+
},
38+
isDiscoverable: false,
39+
serviceRoutingConfig: v1alpha1.Service{
40+
Annotations: nil,
41+
},
42+
expectedAnnotations: map[string]string{
43+
"key1": "value1",
44+
"key2": "value2",
45+
},
46+
},
47+
{
48+
name: "Discoverable annotation enabled should return discoverable annotation",
49+
sourceAnnotations: nil,
50+
isDiscoverable: true,
51+
serviceRoutingConfig: v1alpha1.Service{
52+
Annotations: nil,
53+
},
54+
expectedAnnotations: map[string]string{
55+
"controller.devfile.io/discoverable-service": "true",
56+
},
57+
},
58+
{
59+
name: "DevWorkspaceRouting Service routing config annotations merged with source annotations",
60+
sourceAnnotations: map[string]string{
61+
"key1": "value1",
62+
},
63+
isDiscoverable: false,
64+
serviceRoutingConfig: v1alpha1.Service{
65+
Annotations: map[string]string{
66+
"key3": "value3",
67+
},
68+
},
69+
expectedAnnotations: map[string]string{
70+
"key1": "value1",
71+
"key3": "value3",
72+
},
73+
},
74+
}
75+
76+
for _, tt := range tests {
77+
t.Run(tt.name, func(t *testing.T) {
78+
// Given + When
79+
result := serviceAnnotations(tt.sourceAnnotations, tt.isDiscoverable, tt.serviceRoutingConfig)
80+
// Then
81+
assert.Equal(t, tt.expectedAnnotations, result)
82+
})
83+
}
84+
}
85+
86+
var devWorkspaceRouting = v1alpha1.DevWorkspaceRouting{
87+
Spec: v1alpha1.DevWorkspaceRoutingSpec{
88+
DevWorkspaceId: "workspaceb978dc9bd4ba428b",
89+
RoutingClass: "basic",
90+
Endpoints: map[string]v1alpha1.EndpointList{
91+
"component1": []v1alpha1.Endpoint{
92+
{
93+
Name: "endpoint1",
94+
TargetPort: 8080,
95+
Exposure: "public",
96+
Protocol: "http",
97+
Secure: false,
98+
Path: "/test",
99+
Attributes: map[string]apiext.JSON{},
100+
Annotations: map[string]string{
101+
"endpoint-annotation-key1": "endpoint-annotation-value1",
102+
},
103+
},
104+
},
105+
},
106+
PodSelector: map[string]string{
107+
"controller.devfile.io/devworkspace_id": "workspaceb978dc9bd4ba428b",
108+
},
109+
Service: map[string]v1alpha1.Service{
110+
"component1": {
111+
Annotations: map[string]string{
112+
"service-annotation-key": "service-annotation-value",
113+
},
114+
},
115+
},
116+
},
117+
}
118+
119+
func TestGetSpecObjects_WhenValidDWRProvidedAndOpenShiftUnavailable_ThenGenerateRoutingObjectsServiceAndIngress(t *testing.T) {
120+
// Given
121+
basicSolver := &BasicSolver{}
122+
routingSuffixSupplier = func() string {
123+
return "test.routing"
124+
}
125+
isOpenShift = func() bool {
126+
return false
127+
}
128+
dwRouting := &devWorkspaceRouting
129+
workspaceMeta := DevWorkspaceMetadata{
130+
DevWorkspaceId: "workspaceb978dc9bd4ba428b",
131+
Namespace: "test",
132+
PodSelector: map[string]string{
133+
"controller.devfile.io/devworkspace_id": "workspaceb978dc9bd4ba428b",
134+
},
135+
}
136+
137+
// When
138+
routingObjects, err := basicSolver.GetSpecObjects(dwRouting, workspaceMeta)
139+
140+
// Then
141+
assert.NotNil(t, routingObjects)
142+
assert.NoError(t, err)
143+
assert.Len(t, routingObjects.Services, 1)
144+
assert.Equal(t, corev1.Service{
145+
ObjectMeta: metav1.ObjectMeta{
146+
Name: "workspaceb978dc9bd4ba428b-service",
147+
Namespace: "test",
148+
Labels: map[string]string{"controller.devfile.io/devworkspace_id": "workspaceb978dc9bd4ba428b"},
149+
Annotations: map[string]string{"service-annotation-key": "service-annotation-value"},
150+
},
151+
Spec: corev1.ServiceSpec{
152+
Type: corev1.ServiceTypeClusterIP,
153+
Ports: []corev1.ServicePort{
154+
{
155+
Name: "endpoint1",
156+
Protocol: corev1.ProtocolTCP,
157+
Port: 8080,
158+
TargetPort: intstr.IntOrString{IntVal: 8080},
159+
},
160+
},
161+
Selector: map[string]string{"controller.devfile.io/devworkspace_id": "workspaceb978dc9bd4ba428b"},
162+
},
163+
}, routingObjects.Services[0])
164+
assert.Len(t, routingObjects.Ingresses, 1)
165+
assert.Equal(t, metav1.ObjectMeta{
166+
Name: "workspaceb978dc9bd4ba428b-endpoint1",
167+
Namespace: "test",
168+
Labels: map[string]string{"controller.devfile.io/devworkspace_id": "workspaceb978dc9bd4ba428b"},
169+
Annotations: map[string]string{
170+
"controller.devfile.io/endpoint_name": "endpoint1",
171+
"endpoint-annotation-key1": "endpoint-annotation-value1",
172+
"nginx.ingress.kubernetes.io/rewrite-target": "/",
173+
"nginx.ingress.kubernetes.io/ssl-redirect": "false",
174+
},
175+
}, routingObjects.Ingresses[0].ObjectMeta)
176+
assert.Len(t, routingObjects.Ingresses[0].Spec.Rules, 1)
177+
assert.Equal(t, "workspaceb978dc9bd4ba428b-endpoint1-8080.test.routing", routingObjects.Ingresses[0].Spec.Rules[0].Host)
178+
assert.Len(t, routingObjects.Ingresses[0].Spec.Rules[0].HTTP.Paths, 1)
179+
assert.Equal(t, networkingv1.IngressBackend{
180+
Service: &networkingv1.IngressServiceBackend{
181+
Name: "workspaceb978dc9bd4ba428b-service",
182+
Port: networkingv1.ServiceBackendPort{Number: int32(8080)},
183+
},
184+
}, routingObjects.Ingresses[0].Spec.Rules[0].HTTP.Paths[0].Backend)
185+
assert.Len(t, routingObjects.Routes, 0)
186+
}
187+
188+
func TestGetSpecObjects_WhenValidDWRProvidedAndOpenShiftAvailable_ThenGenerateRoutingObjectsServiceAndRoute(t *testing.T) {
189+
// Given
190+
basicSolver := &BasicSolver{}
191+
routingSuffixSupplier = func() string {
192+
return "test.routing"
193+
}
194+
isOpenShift = func() bool {
195+
return true
196+
}
197+
dwRouting := &devWorkspaceRouting
198+
workspaceMeta := DevWorkspaceMetadata{
199+
DevWorkspaceId: "workspaceb978dc9bd4ba428b",
200+
Namespace: "test",
201+
PodSelector: map[string]string{
202+
"controller.devfile.io/devworkspace_id": "workspaceb978dc9bd4ba428b",
203+
},
204+
}
205+
206+
// When
207+
routingObjects, err := basicSolver.GetSpecObjects(dwRouting, workspaceMeta)
208+
209+
// Then
210+
assert.NotNil(t, routingObjects)
211+
assert.NoError(t, err)
212+
assert.Len(t, routingObjects.Services, 1)
213+
assert.Equal(t, corev1.Service{
214+
ObjectMeta: metav1.ObjectMeta{
215+
Name: "workspaceb978dc9bd4ba428b-service",
216+
Namespace: "test",
217+
Labels: map[string]string{"controller.devfile.io/devworkspace_id": "workspaceb978dc9bd4ba428b"},
218+
Annotations: map[string]string{"service-annotation-key": "service-annotation-value"},
219+
},
220+
Spec: corev1.ServiceSpec{
221+
Type: corev1.ServiceTypeClusterIP,
222+
Ports: []corev1.ServicePort{
223+
{
224+
Name: "endpoint1",
225+
Protocol: corev1.ProtocolTCP,
226+
Port: 8080,
227+
TargetPort: intstr.IntOrString{IntVal: 8080},
228+
},
229+
},
230+
Selector: map[string]string{"controller.devfile.io/devworkspace_id": "workspaceb978dc9bd4ba428b"},
231+
},
232+
}, routingObjects.Services[0])
233+
assert.Len(t, routingObjects.Ingresses, 0)
234+
assert.Len(t, routingObjects.Routes, 1)
235+
assert.Equal(t, metav1.ObjectMeta{
236+
Name: "workspaceb978dc9bd4ba428b-endpoint1",
237+
Namespace: "test",
238+
Labels: map[string]string{
239+
"controller.devfile.io/devworkspace_id": "workspaceb978dc9bd4ba428b",
240+
},
241+
Annotations: map[string]string{
242+
"controller.devfile.io/endpoint_name": "endpoint1",
243+
"endpoint-annotation-key1": "endpoint-annotation-value1",
244+
"haproxy.router.openshift.io/rewrite-target": "/",
245+
},
246+
}, routingObjects.Routes[0].ObjectMeta)
247+
assert.Equal(t, "workspaceb978dc9bd4ba428b.test.routing", routingObjects.Routes[0].Spec.Host)
248+
assert.Equal(t, "/endpoint1/", routingObjects.Routes[0].Spec.Path)
249+
assert.Equal(t, "Service", routingObjects.Routes[0].Spec.To.Kind)
250+
assert.Equal(t, "workspaceb978dc9bd4ba428b-service", routingObjects.Routes[0].Spec.To.Name)
251+
}

controllers/controller/devworkspacerouting/solvers/cluster_solver.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ func (s *ClusterSolver) Finalize(*controllerv1alpha1.DevWorkspaceRouting) error
4646

4747
func (s *ClusterSolver) GetSpecObjects(routing *controllerv1alpha1.DevWorkspaceRouting, workspaceMeta DevWorkspaceMetadata) (RoutingObjects, error) {
4848
spec := routing.Spec
49-
services := getServicesForEndpoints(spec.Endpoints, workspaceMeta)
49+
services := getServicesForEndpoints(spec, workspaceMeta)
5050
podAdditions := &controllerv1alpha1.PodAdditions{}
5151
if s.TLS {
5252
readOnlyMode := int32(420)

0 commit comments

Comments
 (0)