Skip to content

Commit 5bccfb0

Browse files
AObuchowamisevsk
authored andcommitted
feat: Allow external DWOC to be merged with internal DWOC
Allow specifying an external DWOC that gets merged with the global DWOC for use by a workspace. During the merge, the fields which are set in the external DWOC will overwrite those existing in the internal/global DWOC, resulting in a merged DWOC to be used by the workspace. The internal/global DWOC will remain unchanged after the merge. The external DWOC's name and namespace are specified with the `controller.devfile.io/devworkspace-config` DevWorkspace attribute, which has the following structure: attributes: controller.devfile.io/devworkspace-config: name: <string> namespace: <string> Part of eclipse-che/che#21405 Signed-off-by: Andrew Obuchowicz <[email protected]>
1 parent 5513b67 commit 5bccfb0

File tree

5 files changed

+172
-2
lines changed

5 files changed

+172
-2
lines changed

controllers/workspace/devworkspace_controller.go

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,11 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request
123123
return reconcile.Result{}, err
124124
}
125125

126-
config := wkspConfig.GetGlobalConfig()
126+
config, err := wkspConfig.ResolveConfigForWorkspace(rawWorkspace, clusterAPI.Client)
127+
if err != nil {
128+
reqLogger.Error(err, "Error applying external DevWorkspace-Operator configuration")
129+
config = wkspConfig.GetGlobalConfig()
130+
}
127131
configString := wkspConfig.GetCurrentConfigString()
128132
workspace := &common.DevWorkspaceWithConfig{}
129133
workspace.DevWorkspace = rawWorkspace

pkg/config/common_test.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,11 @@ import (
3131
"github.com/devfile/devworkspace-operator/pkg/infrastructure"
3232
)
3333

34-
const testNamespace = "test-namespace"
34+
const (
35+
testNamespace = "test-namespace"
36+
externalConfigName = "external-config-name"
37+
externalConfigNamespace = "external-config-namespace"
38+
)
3539

3640
var (
3741
scheme = runtime.NewScheme()
@@ -68,3 +72,13 @@ func buildConfig(config *v1alpha1.OperatorConfiguration) *v1alpha1.DevWorkspaceO
6872
Config: config,
6973
}
7074
}
75+
76+
func buildExternalConfig(config *v1alpha1.OperatorConfiguration) *v1alpha1.DevWorkspaceOperatorConfig {
77+
return &v1alpha1.DevWorkspaceOperatorConfig{
78+
ObjectMeta: metav1.ObjectMeta{
79+
Name: externalConfigName,
80+
Namespace: externalConfigNamespace,
81+
},
82+
Config: config,
83+
}
84+
}

pkg/config/sync.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import (
2121
"strings"
2222
"sync"
2323

24+
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
2425
"github.com/devfile/devworkspace-operator/pkg/config/proxy"
2526
routeV1 "github.com/openshift/api/route/v1"
2627
k8sErrors "k8s.io/apimachinery/pkg/api/errors"
@@ -30,6 +31,7 @@ import (
3031
crclient "sigs.k8s.io/controller-runtime/pkg/client"
3132

3233
controller "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
34+
"github.com/devfile/devworkspace-operator/pkg/constants"
3335
"github.com/devfile/devworkspace-operator/pkg/infrastructure"
3436
)
3537

@@ -49,6 +51,38 @@ func GetGlobalConfig() *controller.OperatorConfiguration {
4951
return internalConfig.DeepCopy()
5052
}
5153

54+
// ResolveConfigForWorkspace returns the resulting config from merging the global DevWorkspaceOperatorConfig with the
55+
// DevWorkspaceOperatorConfig specified by the optional workspace attribute `controller.devfile.io/devworkspace-config`.
56+
// If the `controller.devfile.io/devworkspace-config` is not set, the global DevWorkspaceOperatorConfig is returned.
57+
// If the `controller.devfile.io/devworkspace-config` attribute is incorrectly set, or the specified DevWorkspaceOperatorConfig
58+
// does not exist on the cluster, an error is returned.
59+
func ResolveConfigForWorkspace(workspace *dw.DevWorkspace, client crclient.Client) (*controller.OperatorConfiguration, error) {
60+
if !workspace.Spec.Template.Attributes.Exists(constants.ExternalDevWorkspaceConfiguration) {
61+
return GetGlobalConfig(), nil
62+
}
63+
64+
namespacedName := types.NamespacedName{}
65+
err := workspace.Spec.Template.Attributes.GetInto(constants.ExternalDevWorkspaceConfiguration, &namespacedName)
66+
if err != nil {
67+
return nil, fmt.Errorf("failed to read attribute %s in DevWorkspace attributes: %w", constants.ExternalDevWorkspaceConfiguration, err)
68+
}
69+
70+
if namespacedName.Name == "" {
71+
return nil, fmt.Errorf("'name' must be set for attribute %s in DevWorkspace attributes", constants.ExternalDevWorkspaceConfiguration)
72+
}
73+
74+
if namespacedName.Namespace == "" {
75+
return nil, fmt.Errorf("'namespace' must be set for attribute %s in DevWorkspace attributes", constants.ExternalDevWorkspaceConfiguration)
76+
}
77+
78+
externalDWOC := &controller.DevWorkspaceOperatorConfig{}
79+
err = client.Get(context.TODO(), namespacedName, externalDWOC)
80+
if err != nil {
81+
return nil, fmt.Errorf("could not fetch external DWOC with name %s in namespace %s: %w", namespacedName.Name, namespacedName.Namespace, err)
82+
}
83+
return getMergedConfig(externalDWOC.Config, internalConfig), nil
84+
}
85+
5286
func GetConfigForTesting(customConfig *controller.OperatorConfiguration) *controller.OperatorConfiguration {
5387
configMutex.Lock()
5488
defer configMutex.Unlock()
@@ -121,6 +155,13 @@ func getClusterConfig(namespace string, client crclient.Client) (*controller.Dev
121155
return clusterConfig, nil
122156
}
123157

158+
func getMergedConfig(from, to *controller.OperatorConfiguration) *controller.OperatorConfiguration {
159+
mergedConfig := to.DeepCopy()
160+
fromCopy := from.DeepCopy()
161+
mergeConfig(fromCopy, mergedConfig)
162+
return mergedConfig
163+
}
164+
124165
func syncConfigFrom(newConfig *controller.DevWorkspaceOperatorConfig) {
125166
if newConfig == nil || newConfig.Name != OperatorConfigName || newConfig.Namespace != configNamespace {
126167
return

pkg/config/sync_test.go

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,18 +16,24 @@
1616
package config
1717

1818
import (
19+
"context"
1920
"fmt"
2021
"testing"
2122

23+
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
24+
attributes "github.com/devfile/api/v2/pkg/attributes"
2225
"github.com/google/go-cmp/cmp"
2326
fuzz "github.com/google/gofuzz"
2427
routev1 "github.com/openshift/api/route/v1"
2528
"github.com/stretchr/testify/assert"
29+
"k8s.io/apimachinery/pkg/api/resource"
2630
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
2731
"k8s.io/apimachinery/pkg/runtime"
32+
"k8s.io/apimachinery/pkg/types"
2833
"sigs.k8s.io/controller-runtime/pkg/client/fake"
2934

3035
"github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
36+
"github.com/devfile/devworkspace-operator/pkg/constants"
3137
"github.com/devfile/devworkspace-operator/pkg/infrastructure"
3238
)
3339

@@ -83,6 +89,96 @@ func TestSetupControllerMergesClusterConfig(t *testing.T) {
8389
assert.Equal(t, expectedConfig, internalConfig, fmt.Sprintf("Processed config should merge settings from cluster: %s", cmp.Diff(internalConfig, expectedConfig)))
8490
}
8591

92+
func TestCatchesNonExistentExternalDWOC(t *testing.T) {
93+
setupForTest(t)
94+
95+
workspace := &dw.DevWorkspace{}
96+
attributes := attributes.Attributes{}
97+
namespacedName := types.NamespacedName{
98+
Name: "external-config-name",
99+
Namespace: "external-config-namespace",
100+
}
101+
attributes.Put(constants.ExternalDevWorkspaceConfiguration, namespacedName, nil)
102+
workspace.Spec.Template.DevWorkspaceTemplateSpecContent = dw.DevWorkspaceTemplateSpecContent{
103+
Attributes: attributes,
104+
}
105+
client := fake.NewClientBuilder().WithScheme(scheme).Build()
106+
107+
resolvedConfig, err := ResolveConfigForWorkspace(workspace, client)
108+
if !assert.Error(t, err, "Error should be given if external DWOC specified in workspace spec does not exist") {
109+
return
110+
}
111+
assert.Equal(t, resolvedConfig, internalConfig, "Internal/Global DWOC should be used as fallback if external DWOC could not be resolved")
112+
}
113+
114+
func TestMergeExternalConfig(t *testing.T) {
115+
setupForTest(t)
116+
117+
workspace := &dw.DevWorkspace{}
118+
attributes := attributes.Attributes{}
119+
namespacedName := types.NamespacedName{
120+
Name: externalConfigName,
121+
Namespace: externalConfigNamespace,
122+
}
123+
attributes.Put(constants.ExternalDevWorkspaceConfiguration, namespacedName, nil)
124+
workspace.Spec.Template.DevWorkspaceTemplateSpecContent = dw.DevWorkspaceTemplateSpecContent{
125+
Attributes: attributes,
126+
}
127+
128+
// External config is based off of the default/internal config, with just a few changes made
129+
// So when the internal config is merged with the external one, they will become identical
130+
externalConfig := buildExternalConfig(defaultConfig.DeepCopy())
131+
externalConfig.Config.Workspace.ImagePullPolicy = "Always"
132+
externalConfig.Config.Workspace.PVCName = "test-PVC-name"
133+
storageSize := resource.MustParse("15Gi")
134+
externalConfig.Config.Workspace.DefaultStorageSize = &v1alpha1.StorageSizes{
135+
Common: &storageSize,
136+
PerWorkspace: &storageSize,
137+
}
138+
139+
clusterConfig := buildConfig(defaultConfig.DeepCopy())
140+
client := fake.NewClientBuilder().WithScheme(scheme).WithObjects(clusterConfig, externalConfig).Build()
141+
err := SetupControllerConfig(client)
142+
if !assert.NoError(t, err, "Should not return error") {
143+
return
144+
}
145+
146+
// Sanity check
147+
if !cmp.Equal(clusterConfig.Config, internalConfig) {
148+
t.Error("Internal config should be same as cluster config before starting:", cmp.Diff(clusterConfig.Config, internalConfig))
149+
}
150+
151+
resolvedConfig, err := ResolveConfigForWorkspace(workspace, client)
152+
if !assert.NoError(t, err, "Should not return error") {
153+
return
154+
}
155+
156+
// Compare the resolved config and external config
157+
if !cmp.Equal(resolvedConfig, externalConfig.Config) {
158+
t.Error("Resolved config and external config should match after merge:", cmp.Diff(resolvedConfig, externalConfig.Config))
159+
}
160+
161+
// Ensure the internal config was not affected by merge
162+
if !cmp.Equal(clusterConfig.Config, internalConfig) {
163+
t.Error("Internal config should remain the same after merge:", cmp.Diff(clusterConfig.Config, internalConfig))
164+
}
165+
166+
// Get the global config off cluster and ensure it hasn't changed
167+
retrievedClusterConfig := &v1alpha1.DevWorkspaceOperatorConfig{}
168+
namespacedName = types.NamespacedName{
169+
Name: OperatorConfigName,
170+
Namespace: testNamespace,
171+
}
172+
err = client.Get(context.TODO(), namespacedName, retrievedClusterConfig)
173+
if !assert.NoError(t, err, "Should not return error when fetching config from cluster") {
174+
return
175+
}
176+
177+
if !cmp.Equal(retrievedClusterConfig.Config, clusterConfig.Config) {
178+
t.Error("Config on cluster and global config should match after merge; global config should not have been modified from merge:", cmp.Diff(retrievedClusterConfig, clusterConfig.Config))
179+
}
180+
}
181+
86182
func TestSetupControllerAlwaysSetsDefaultClusterRoutingSuffix(t *testing.T) {
87183
setupForTest(t)
88184
infrastructure.InitializeForTesting(infrastructure.OpenShiftv4)

pkg/constants/attributes.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,21 @@ const (
2828
// stopped.
2929
DevWorkspaceStorageTypeAttribute = "controller.devfile.io/storage-type"
3030

31+
// ExternalDevWorkspaceConfiguration is an attribute that allows for specifying an (optional) external DevWorkspaceOperatorConfig
32+
// which will merged with the internal/global DevWorkspaceOperatorConfig. The DevWorkspaceOperatorConfig resulting from the merge will be used for the workspace.
33+
// The fields which are set in the external DevWorkspaceOperatorConfig will overwrite those existing in the
34+
// internal/global DevWorkspaceOperatorConfig during the merge.
35+
// The structure of the attribute value should contain two strings: name and namespace.
36+
// 'name' specifies the metadata.name of the external operator configuration.
37+
// 'namespace' specifies the metadata.namespace of the external operator configuration .
38+
// For example:
39+
//
40+
// attributes:
41+
// controller.devfile.io/devworkspace-config:
42+
// name: external-dwoc-name
43+
// namespace: some-namespace
44+
ExternalDevWorkspaceConfiguration = "controller.devfile.io/devworkspace-config"
45+
3146
// RuntimeClassNameAttribute is an attribute added to a DevWorkspace to specify a runtimeClassName for container
3247
// components in the DevWorkspace (pod.spec.runtimeClassName). If empty, no runtimeClassName is added.
3348
RuntimeClassNameAttribute = "controller.devfile.io/runtime-class"

0 commit comments

Comments
 (0)