Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 6 additions & 65 deletions controllers/workspace/devworkspace_controller.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -700,69 +700,6 @@ func (r *DevWorkspaceReconciler) getWorkspaceId(ctx context.Context, workspace *
}
}

// Mapping the pod to the devworkspace
func dwRelatedPodsHandler(obj client.Object) []reconcile.Request {
labels := obj.GetLabels()
if _, ok := labels[constants.DevWorkspaceNameLabel]; !ok {
return []reconcile.Request{}
}

//If the dewworkspace label does not exist, do no reconcile
if _, ok := labels[constants.DevWorkspaceIDLabel]; !ok {
return []reconcile.Request{}
}

return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: labels[constants.DevWorkspaceNameLabel],
Namespace: obj.GetNamespace(),
},
},
}
}

func (r *DevWorkspaceReconciler) dwPVCHandler(obj client.Object) []reconcile.Request {
// Check if PVC is owned by a DevWorkspace (per-workspace storage case)
for _, ownerref := range obj.GetOwnerReferences() {
if ownerref.Kind != "DevWorkspace" {
continue
}
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: ownerref.Name,
Namespace: obj.GetNamespace(),
},
},
}
}

// TODO: Label PVCs used for workspace storage so that they can be cleaned up if non-default name is used.
// Otherwise, check if common PVC is deleted to make sure all DevWorkspaces see it happen
if obj.GetName() != wkspConfig.GetGlobalConfig().Workspace.PVCName || obj.GetDeletionTimestamp() == nil {
// We're looking for a deleted common PVC
return []reconcile.Request{}
}
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
storageType := workspace.Spec.Template.Attributes.GetString(constants.DevWorkspaceStorageTypeAttribute, nil)
if storageType == constants.CommonStorageClassType || storageType == constants.PerUserStorageClassType || storageType == "" {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}

func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
setupHttpClients()

Expand All@@ -775,7 +712,8 @@ func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
return []reconcile.Request{}
}

var configWatcher builder.WatchesOption = builder.WithPredicates(wkspConfig.Predicates())
configWatcher := builder.WithPredicates(wkspConfig.Predicates())
automountWatcher := builder.WithPredicates(automountPredicates)

// TODO: Set up indexing https://book.kubebuilder.io/cronjob-tutorial/controller-implementation.html#setup
return ctrl.NewControllerManagedBy(mgr).
Expand All@@ -793,8 +731,11 @@ func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
Owns(&corev1.ServiceAccount{}).
Watches(&source.Kind{Type: &corev1.Pod{}}, handler.EnqueueRequestsFromMapFunc(dwRelatedPodsHandler)).
Watches(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, handler.EnqueueRequestsFromMapFunc(r.dwPVCHandler)).
Watches(&source.Kind{Type: &corev1.Secret{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &corev1.ConfigMap{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &controllerv1alpha1.DevWorkspaceOperatorConfig{}}, handler.EnqueueRequestsFromMapFunc(emptyMapper), configWatcher).
WithEventFilter(predicates).
WithEventFilter(devworkspacePredicates).
WithEventFilter(podPredicates).
Complete(r)
}
44 changes: 43 additions & 1 deletion controllers/workspace/devworkspace_controller_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -353,7 +353,6 @@ var _ = Describe("DevWorkspace Controller", func() {
},
},
})
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
})

AfterEach(func() {
Expand All@@ -362,6 +361,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Mounts image pull secrets to the DevWorkspace Deployment", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -394,6 +394,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Manages git credentials for DevWorkspace", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -456,6 +457,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Automounts secrets and configmaps volumes", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -518,6 +520,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Automounts secrets and configmaps env vars", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -558,6 +561,45 @@ var _ = Describe("DevWorkspace Controller", func() {
Expect(container.EnvFrom).Should(ContainElements(expectedEnvFromSources), "Automounted env sources should be added to containers")
}
})

It("Detects changes to automount resources and reconciles", func() {
// NOTE: timeout for this test is reduced, as eventually DWO will reconcile the workspace by coincidence and notice
// the automount secret.
createStartedDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

mergedSecretNN := namespacedName(constants.GitCredentialsMergedSecretName, testNamespace)
mergedSecret := &corev1.Secret{}
Expect(k8sClient.Get(ctx, mergedSecretNN, mergedSecret)).Error()

By("Creating git-credential secret")
secret := generateSecret("git-credential-secret", corev1.SecretTypeOpaque)
secret.Labels[constants.DevWorkspaceGitCredentialLabel] = "true"
secret.Data["credentials"] = []byte("https://test:token@github.com")
createObject(secret)
defer deleteObject(secret)

By("Checking that merged credentials secret is created")
Eventually(func() error {
return k8sClient.Get(ctx, mergedSecretNN, mergedSecret)
}, 1*time.Second, interval).Should(Succeed(), "Merged credentials secret is created")

By("Checking that workspace deployment mounts merged credentials secret")
Eventually(func() error {
deploy := &appsv1.Deployment{}
deployNN := namespacedName(common.DeploymentName(workspaceID), testNamespace)
if err := k8sClient.Get(ctx, deployNN, deploy); err != nil {
return err
}
for _, volume := range deploy.Spec.Template.Spec.Volumes {
if volume.Secret != nil && volume.Secret.SecretName == constants.GitCredentialsMergedSecretName {
return nil
}
}
return fmt.Errorf("Secret not found in volumes")
}, 1*time.Second, interval).Should(Succeed(), "Merged credentials secret is added to deployment")
})
})

Context("Stopping DevWorkspaces", func() {
Expand Down
108 changes: 108 additions & 0 deletions controllers/workspace/eventhandlers.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
// Copyright (c) 2019-2023 Red Hat, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package controllers

import (
"context"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
wkspConfig "github.com/devfile/devworkspace-operator/pkg/config"
"github.com/devfile/devworkspace-operator/pkg/constants"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

// Mapping the pod to the devworkspace
func dwRelatedPodsHandler(obj client.Object) []reconcile.Request {
labels := obj.GetLabels()
if _, ok := labels[constants.DevWorkspaceNameLabel]; !ok {
return []reconcile.Request{}
}

//If the dewworkspace label does not exist, do no reconcile
if _, ok := labels[constants.DevWorkspaceIDLabel]; !ok {
return []reconcile.Request{}
}

return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: labels[constants.DevWorkspaceNameLabel],
Namespace: obj.GetNamespace(),
},
},
}
}

func (r *DevWorkspaceReconciler) dwPVCHandler(obj client.Object) []reconcile.Request {
// Check if PVC is owned by a DevWorkspace (per-workspace storage case)
for _, ownerref := range obj.GetOwnerReferences() {
if ownerref.Kind != "DevWorkspace" {
continue
}
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: ownerref.Name,
Namespace: obj.GetNamespace(),
},
},
}
}

// TODO: Label PVCs used for workspace storage so that they can be cleaned up if non-default name is used.
// Otherwise, check if common PVC is deleted to make sure all DevWorkspaces see it happen
if obj.GetName() != wkspConfig.GetGlobalConfig().Workspace.PVCName || obj.GetDeletionTimestamp() == nil {
// We're looking for a deleted common PVC
return []reconcile.Request{}
}
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
storageType := workspace.Spec.Template.Attributes.GetString(constants.DevWorkspaceStorageTypeAttribute, nil)
if storageType == constants.CommonStorageClassType || storageType == constants.PerUserStorageClassType || storageType == "" {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}

func (r *DevWorkspaceReconciler) runningWorkspacesHandler(obj client.Object) []reconcile.Request {
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
// Queue reconciles for any started workspaces to make sure they pick up new object
if workspace.Spec.Started {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}
32 changes: 30 additions & 2 deletions controllers/workspace/predicates.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,14 +21,15 @@ import (

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)

// predicates filters incoming events to avoid unnecessary reconciles to failed workspaces.
// devworkspacePredicates filters incoming events to avoid unnecessary reconciles to failed workspaces.
// If a workspace failed and its spec is changed, we trigger reconciles to allow for fixing
// issues in the workspace spec.
var predicates = predicate.Funcs{
var devworkspacePredicates = predicate.Funcs{
CreateFunc: func(_ event.CreateEvent) bool { return true },
DeleteFunc: func(_ event.DeleteEvent) bool { return true },
UpdateFunc: func(ev event.UpdateEvent) bool {
Expand DownExpand Up@@ -84,3 +85,30 @@ var podPredicates = predicate.Funcs{
return true
},
}

var automountPredicates = predicate.Funcs{
CreateFunc: func(ev event.CreateEvent) bool {
return objectIsAutomountResource(ev.Object)
},
DeleteFunc: func(ev event.DeleteEvent) bool {
return objectIsAutomountResource(ev.Object)
},
UpdateFunc: func(ev event.UpdateEvent) bool {
return objectIsAutomountResource(ev.ObjectNew)
},
GenericFunc: func(_ event.GenericEvent) bool { return false },
}

func objectIsAutomountResource(obj client.Object) bool {
labels := obj.GetLabels()
switch {
case labels[constants.DevWorkspaceMountLabel] == "true",
labels[constants.DevWorkspaceGitCredentialLabel] == "true",
labels[constants.DevWorkspaceGitTLSLabel] == "true",
labels[constants.DevWorkspacePullSecretLabel] == "true":
return true
default:
return false
}

}
9 changes: 9 additions & 0 deletions pkg/constants/metadata.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,15 @@ const (
// If the git host is not defined then the certificate will be used for all http repositories.
DevWorkspaceGitTLSLabel = "controller.devfile.io/git-tls-credential"

// GitCredentialsConfigMapName is the name used for the configmap that stores the Git configuration for workspaces
// in a given namespace. It is used when e.g. adding Git credentials via secret
GitCredentialsConfigMapName = "devworkspace-gitconfig"

// GitCredentialsMergedSecretName is the name for the merged Git credentials secret that is mounted to workspaces
// when Git credentials are defined. This secret combines the values of any secrets labelled
// "controller.devfile.io/git-credential"
GitCredentialsMergedSecretName = "devworkspace-merged-git-credentials"

// DevWorkspaceMountPathAnnotation is the annotation key to store the mount path for the secret or configmap.
// If no mount path is provided, configmaps will be mounted at /etc/config/<configmap-name>, secrets will
// be mounted at /etc/secret/<secret-name>, and persistent volume claims will be mounted to /tmp/<claim-name>
Expand Down
4 changes: 2 additions & 2 deletions pkg/provision/automount/gitconfig.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ func getGitResources(api sync.ClusterAPI, namespace string) (credentialSecrets [

func cleanupGitConfig(api sync.ClusterAPI, namespace string) error {
secretNN := types.NamespacedName{
Name: gitCredentialsSecretName,
Name: constants.GitCredentialsMergedSecretName,
Namespace: namespace,
}
tlsSecret := &corev1.Secret{}
Expand All@@ -134,7 +134,7 @@ func cleanupGitConfig(api sync.ClusterAPI, namespace string) error {
}

configmapNN := types.NamespacedName{
Name: gitCredentialsConfigMapName,
Name: constants.GitCredentialsConfigMapName,
Namespace: namespace,
}
credentialsConfigMap := &corev1.ConfigMap{}
Expand Down
6 changes: 2 additions & 4 deletions pkg/provision/automount/templates.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,10 +28,8 @@ const gitTLSCertificateKey = "certificate"

const gitConfigName = "gitconfig"
const gitConfigLocation = "/etc/" + gitConfigName
const gitCredentialsConfigMapName = "devworkspace-gitconfig"

const gitCredentialsSecretKey = "credentials"
const gitCredentialsSecretName = "devworkspace-merged-git-credentials"

// gitLFSConfig is the default configuration that gets provisioned when git-lfs
// is installed. It needs to be included in the overridden gitconfig to avoid
Expand DownExpand Up@@ -95,7 +93,7 @@ func constructGitConfig(namespace, credentialMountPath string, certificatesConfi

gitConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: gitCredentialsConfigMapName,
Name: constants.GitCredentialsConfigMapName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/defaultName": "git-config-secret",
Expand All@@ -122,7 +120,7 @@ func mergeGitCredentials(namespace string, credentialSecrets []corev1.Secret) (*
}
mergedCredentials := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: gitCredentialsSecretName,
Name: constants.GitCredentialsMergedSecretName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/defaultName": "git-config-secret",
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 6 additions & 65 deletions controllers/workspace/devworkspace_controller.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -700,69 +700,6 @@ func (r *DevWorkspaceReconciler) getWorkspaceId(ctx context.Context, workspace *
}
}

// Mapping the pod to the devworkspace
func dwRelatedPodsHandler(obj client.Object) []reconcile.Request {
labels := obj.GetLabels()
if _, ok := labels[constants.DevWorkspaceNameLabel]; !ok {
return []reconcile.Request{}
}

//If the dewworkspace label does not exist, do no reconcile
if _, ok := labels[constants.DevWorkspaceIDLabel]; !ok {
return []reconcile.Request{}
}

return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: labels[constants.DevWorkspaceNameLabel],
Namespace: obj.GetNamespace(),
},
},
}
}

func (r *DevWorkspaceReconciler) dwPVCHandler(obj client.Object) []reconcile.Request {
// Check if PVC is owned by a DevWorkspace (per-workspace storage case)
for _, ownerref := range obj.GetOwnerReferences() {
if ownerref.Kind != "DevWorkspace" {
continue
}
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: ownerref.Name,
Namespace: obj.GetNamespace(),
},
},
}
}

// TODO: Label PVCs used for workspace storage so that they can be cleaned up if non-default name is used.
// Otherwise, check if common PVC is deleted to make sure all DevWorkspaces see it happen
if obj.GetName() != wkspConfig.GetGlobalConfig().Workspace.PVCName || obj.GetDeletionTimestamp() == nil {
// We're looking for a deleted common PVC
return []reconcile.Request{}
}
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
storageType := workspace.Spec.Template.Attributes.GetString(constants.DevWorkspaceStorageTypeAttribute, nil)
if storageType == constants.CommonStorageClassType || storageType == constants.PerUserStorageClassType || storageType == "" {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}

func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
setupHttpClients()

Expand All@@ -775,7 +712,8 @@ func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
return []reconcile.Request{}
}

var configWatcher builder.WatchesOption = builder.WithPredicates(wkspConfig.Predicates())
configWatcher := builder.WithPredicates(wkspConfig.Predicates())
automountWatcher := builder.WithPredicates(automountPredicates)

// TODO: Set up indexing https://book.kubebuilder.io/cronjob-tutorial/controller-implementation.html#setup
return ctrl.NewControllerManagedBy(mgr).
Expand All@@ -793,8 +731,11 @@ func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
Owns(&corev1.ServiceAccount{}).
Watches(&source.Kind{Type: &corev1.Pod{}}, handler.EnqueueRequestsFromMapFunc(dwRelatedPodsHandler)).
Watches(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, handler.EnqueueRequestsFromMapFunc(r.dwPVCHandler)).
Watches(&source.Kind{Type: &corev1.Secret{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &corev1.ConfigMap{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &controllerv1alpha1.DevWorkspaceOperatorConfig{}}, handler.EnqueueRequestsFromMapFunc(emptyMapper), configWatcher).
WithEventFilter(predicates).
WithEventFilter(devworkspacePredicates).
WithEventFilter(podPredicates).
Complete(r)
}
44 changes: 43 additions & 1 deletion controllers/workspace/devworkspace_controller_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -353,7 +353,6 @@ var _ = Describe("DevWorkspace Controller", func() {
},
},
})
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
})

AfterEach(func() {
Expand All@@ -362,6 +361,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Mounts image pull secrets to the DevWorkspace Deployment", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -394,6 +394,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Manages git credentials for DevWorkspace", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -456,6 +457,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Automounts secrets and configmaps volumes", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -518,6 +520,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Automounts secrets and configmaps env vars", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -558,6 +561,45 @@ var _ = Describe("DevWorkspace Controller", func() {
Expect(container.EnvFrom).Should(ContainElements(expectedEnvFromSources), "Automounted env sources should be added to containers")
}
})

It("Detects changes to automount resources and reconciles", func() {
// NOTE: timeout for this test is reduced, as eventually DWO will reconcile the workspace by coincidence and notice
// the automount secret.
createStartedDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

mergedSecretNN := namespacedName(constants.GitCredentialsMergedSecretName, testNamespace)
mergedSecret := &corev1.Secret{}
Expect(k8sClient.Get(ctx, mergedSecretNN, mergedSecret)).Error()

By("Creating git-credential secret")
secret := generateSecret("git-credential-secret", corev1.SecretTypeOpaque)
secret.Labels[constants.DevWorkspaceGitCredentialLabel] = "true"
secret.Data["credentials"] = []byte("https://test:token@github.com")
createObject(secret)
defer deleteObject(secret)

By("Checking that merged credentials secret is created")
Eventually(func() error {
return k8sClient.Get(ctx, mergedSecretNN, mergedSecret)
}, 1*time.Second, interval).Should(Succeed(), "Merged credentials secret is created")

By("Checking that workspace deployment mounts merged credentials secret")
Eventually(func() error {
deploy := &appsv1.Deployment{}
deployNN := namespacedName(common.DeploymentName(workspaceID), testNamespace)
if err := k8sClient.Get(ctx, deployNN, deploy); err != nil {
return err
}
for _, volume := range deploy.Spec.Template.Spec.Volumes {
if volume.Secret != nil && volume.Secret.SecretName == constants.GitCredentialsMergedSecretName {
return nil
}
}
return fmt.Errorf("Secret not found in volumes")
}, 1*time.Second, interval).Should(Succeed(), "Merged credentials secret is added to deployment")
})
})

Context("Stopping DevWorkspaces", func() {
Expand Down
108 changes: 108 additions & 0 deletions controllers/workspace/eventhandlers.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
// Copyright (c) 2019-2023 Red Hat, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package controllers

import (
"context"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
wkspConfig "github.com/devfile/devworkspace-operator/pkg/config"
"github.com/devfile/devworkspace-operator/pkg/constants"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

// Mapping the pod to the devworkspace
func dwRelatedPodsHandler(obj client.Object) []reconcile.Request {
labels := obj.GetLabels()
if _, ok := labels[constants.DevWorkspaceNameLabel]; !ok {
return []reconcile.Request{}
}

//If the dewworkspace label does not exist, do no reconcile
if _, ok := labels[constants.DevWorkspaceIDLabel]; !ok {
return []reconcile.Request{}
}

return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: labels[constants.DevWorkspaceNameLabel],
Namespace: obj.GetNamespace(),
},
},
}
}

func (r *DevWorkspaceReconciler) dwPVCHandler(obj client.Object) []reconcile.Request {
// Check if PVC is owned by a DevWorkspace (per-workspace storage case)
for _, ownerref := range obj.GetOwnerReferences() {
if ownerref.Kind != "DevWorkspace" {
continue
}
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: ownerref.Name,
Namespace: obj.GetNamespace(),
},
},
}
}

// TODO: Label PVCs used for workspace storage so that they can be cleaned up if non-default name is used.
// Otherwise, check if common PVC is deleted to make sure all DevWorkspaces see it happen
if obj.GetName() != wkspConfig.GetGlobalConfig().Workspace.PVCName || obj.GetDeletionTimestamp() == nil {
// We're looking for a deleted common PVC
return []reconcile.Request{}
}
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
storageType := workspace.Spec.Template.Attributes.GetString(constants.DevWorkspaceStorageTypeAttribute, nil)
if storageType == constants.CommonStorageClassType || storageType == constants.PerUserStorageClassType || storageType == "" {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}

func (r *DevWorkspaceReconciler) runningWorkspacesHandler(obj client.Object) []reconcile.Request {
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
// Queue reconciles for any started workspaces to make sure they pick up new object
if workspace.Spec.Started {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}
32 changes: 30 additions & 2 deletions controllers/workspace/predicates.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,14 +21,15 @@ import (

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)

// predicates filters incoming events to avoid unnecessary reconciles to failed workspaces.
// devworkspacePredicates filters incoming events to avoid unnecessary reconciles to failed workspaces.
// If a workspace failed and its spec is changed, we trigger reconciles to allow for fixing
// issues in the workspace spec.
var predicates = predicate.Funcs{
var devworkspacePredicates = predicate.Funcs{
CreateFunc: func(_ event.CreateEvent) bool { return true },
DeleteFunc: func(_ event.DeleteEvent) bool { return true },
UpdateFunc: func(ev event.UpdateEvent) bool {
Expand DownExpand Up@@ -84,3 +85,30 @@ var podPredicates = predicate.Funcs{
return true
},
}

var automountPredicates = predicate.Funcs{
CreateFunc: func(ev event.CreateEvent) bool {
return objectIsAutomountResource(ev.Object)
},
DeleteFunc: func(ev event.DeleteEvent) bool {
return objectIsAutomountResource(ev.Object)
},
UpdateFunc: func(ev event.UpdateEvent) bool {
return objectIsAutomountResource(ev.ObjectNew)
},
GenericFunc: func(_ event.GenericEvent) bool { return false },
}

func objectIsAutomountResource(obj client.Object) bool {
labels := obj.GetLabels()
switch {
case labels[constants.DevWorkspaceMountLabel] == "true",
labels[constants.DevWorkspaceGitCredentialLabel] == "true",
labels[constants.DevWorkspaceGitTLSLabel] == "true",
labels[constants.DevWorkspacePullSecretLabel] == "true":
return true
default:
return false
}

}
9 changes: 9 additions & 0 deletions pkg/constants/metadata.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,15 @@ const (
// If the git host is not defined then the certificate will be used for all http repositories.
DevWorkspaceGitTLSLabel = "controller.devfile.io/git-tls-credential"

// GitCredentialsConfigMapName is the name used for the configmap that stores the Git configuration for workspaces
// in a given namespace. It is used when e.g. adding Git credentials via secret
GitCredentialsConfigMapName = "devworkspace-gitconfig"

// GitCredentialsMergedSecretName is the name for the merged Git credentials secret that is mounted to workspaces
// when Git credentials are defined. This secret combines the values of any secrets labelled
// "controller.devfile.io/git-credential"
GitCredentialsMergedSecretName = "devworkspace-merged-git-credentials"

// DevWorkspaceMountPathAnnotation is the annotation key to store the mount path for the secret or configmap.
// If no mount path is provided, configmaps will be mounted at /etc/config/<configmap-name>, secrets will
// be mounted at /etc/secret/<secret-name>, and persistent volume claims will be mounted to /tmp/<claim-name>
Expand Down
4 changes: 2 additions & 2 deletions pkg/provision/automount/gitconfig.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ func getGitResources(api sync.ClusterAPI, namespace string) (credentialSecrets [

func cleanupGitConfig(api sync.ClusterAPI, namespace string) error {
secretNN := types.NamespacedName{
Name: gitCredentialsSecretName,
Name: constants.GitCredentialsMergedSecretName,
Namespace: namespace,
}
tlsSecret := &corev1.Secret{}
Expand All@@ -134,7 +134,7 @@ func cleanupGitConfig(api sync.ClusterAPI, namespace string) error {
}

configmapNN := types.NamespacedName{
Name: gitCredentialsConfigMapName,
Name: constants.GitCredentialsConfigMapName,
Namespace: namespace,
}
credentialsConfigMap := &corev1.ConfigMap{}
Expand Down
6 changes: 2 additions & 4 deletions pkg/provision/automount/templates.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,10 +28,8 @@ const gitTLSCertificateKey = "certificate"

const gitConfigName = "gitconfig"
const gitConfigLocation = "/etc/" + gitConfigName
const gitCredentialsConfigMapName = "devworkspace-gitconfig"

const gitCredentialsSecretKey = "credentials"
const gitCredentialsSecretName = "devworkspace-merged-git-credentials"

// gitLFSConfig is the default configuration that gets provisioned when git-lfs
// is installed. It needs to be included in the overridden gitconfig to avoid
Expand DownExpand Up@@ -95,7 +93,7 @@ func constructGitConfig(namespace, credentialMountPath string, certificatesConfi

gitConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: gitCredentialsConfigMapName,
Name: constants.GitCredentialsConfigMapName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/defaultName": "git-config-secret",
Expand All@@ -122,7 +120,7 @@ func mergeGitCredentials(namespace string, credentialSecrets []corev1.Secret) (*
}
mergedCredentials := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: gitCredentialsSecretName,
Name: constants.GitCredentialsMergedSecretName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/defaultName": "git-config-secret",
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 6 additions & 65 deletions controllers/workspace/devworkspace_controller.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -700,69 +700,6 @@ func (r *DevWorkspaceReconciler) getWorkspaceId(ctx context.Context, workspace *
}
}

// Mapping the pod to the devworkspace
func dwRelatedPodsHandler(obj client.Object) []reconcile.Request {
labels := obj.GetLabels()
if _, ok := labels[constants.DevWorkspaceNameLabel]; !ok {
return []reconcile.Request{}
}

//If the dewworkspace label does not exist, do no reconcile
if _, ok := labels[constants.DevWorkspaceIDLabel]; !ok {
return []reconcile.Request{}
}

return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: labels[constants.DevWorkspaceNameLabel],
Namespace: obj.GetNamespace(),
},
},
}
}

func (r *DevWorkspaceReconciler) dwPVCHandler(obj client.Object) []reconcile.Request {
// Check if PVC is owned by a DevWorkspace (per-workspace storage case)
for _, ownerref := range obj.GetOwnerReferences() {
if ownerref.Kind != "DevWorkspace" {
continue
}
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: ownerref.Name,
Namespace: obj.GetNamespace(),
},
},
}
}

// TODO: Label PVCs used for workspace storage so that they can be cleaned up if non-default name is used.
// Otherwise, check if common PVC is deleted to make sure all DevWorkspaces see it happen
if obj.GetName() != wkspConfig.GetGlobalConfig().Workspace.PVCName || obj.GetDeletionTimestamp() == nil {
// We're looking for a deleted common PVC
return []reconcile.Request{}
}
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
storageType := workspace.Spec.Template.Attributes.GetString(constants.DevWorkspaceStorageTypeAttribute, nil)
if storageType == constants.CommonStorageClassType || storageType == constants.PerUserStorageClassType || storageType == "" {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}

func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
setupHttpClients()

Expand All@@ -775,7 +712,8 @@ func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
return []reconcile.Request{}
}

var configWatcher builder.WatchesOption = builder.WithPredicates(wkspConfig.Predicates())
configWatcher := builder.WithPredicates(wkspConfig.Predicates())
automountWatcher := builder.WithPredicates(automountPredicates)

// TODO: Set up indexing https://book.kubebuilder.io/cronjob-tutorial/controller-implementation.html#setup
return ctrl.NewControllerManagedBy(mgr).
Expand All@@ -793,8 +731,11 @@ func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
Owns(&corev1.ServiceAccount{}).
Watches(&source.Kind{Type: &corev1.Pod{}}, handler.EnqueueRequestsFromMapFunc(dwRelatedPodsHandler)).
Watches(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, handler.EnqueueRequestsFromMapFunc(r.dwPVCHandler)).
Watches(&source.Kind{Type: &corev1.Secret{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &corev1.ConfigMap{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &controllerv1alpha1.DevWorkspaceOperatorConfig{}}, handler.EnqueueRequestsFromMapFunc(emptyMapper), configWatcher).
WithEventFilter(predicates).
WithEventFilter(devworkspacePredicates).
WithEventFilter(podPredicates).
Complete(r)
}
44 changes: 43 additions & 1 deletion controllers/workspace/devworkspace_controller_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -353,7 +353,6 @@ var _ = Describe("DevWorkspace Controller", func() {
},
},
})
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
})

AfterEach(func() {
Expand All@@ -362,6 +361,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Mounts image pull secrets to the DevWorkspace Deployment", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -394,6 +394,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Manages git credentials for DevWorkspace", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -456,6 +457,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Automounts secrets and configmaps volumes", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -518,6 +520,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Automounts secrets and configmaps env vars", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -558,6 +561,45 @@ var _ = Describe("DevWorkspace Controller", func() {
Expect(container.EnvFrom).Should(ContainElements(expectedEnvFromSources), "Automounted env sources should be added to containers")
}
})

It("Detects changes to automount resources and reconciles", func() {
// NOTE: timeout for this test is reduced, as eventually DWO will reconcile the workspace by coincidence and notice
// the automount secret.
createStartedDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

mergedSecretNN := namespacedName(constants.GitCredentialsMergedSecretName, testNamespace)
mergedSecret := &corev1.Secret{}
Expect(k8sClient.Get(ctx, mergedSecretNN, mergedSecret)).Error()

By("Creating git-credential secret")
secret := generateSecret("git-credential-secret", corev1.SecretTypeOpaque)
secret.Labels[constants.DevWorkspaceGitCredentialLabel] = "true"
secret.Data["credentials"] = []byte("https://test:token@github.com")
createObject(secret)
defer deleteObject(secret)

By("Checking that merged credentials secret is created")
Eventually(func() error {
return k8sClient.Get(ctx, mergedSecretNN, mergedSecret)
}, 1*time.Second, interval).Should(Succeed(), "Merged credentials secret is created")

By("Checking that workspace deployment mounts merged credentials secret")
Eventually(func() error {
deploy := &appsv1.Deployment{}
deployNN := namespacedName(common.DeploymentName(workspaceID), testNamespace)
if err := k8sClient.Get(ctx, deployNN, deploy); err != nil {
return err
}
for _, volume := range deploy.Spec.Template.Spec.Volumes {
if volume.Secret != nil && volume.Secret.SecretName == constants.GitCredentialsMergedSecretName {
return nil
}
}
return fmt.Errorf("Secret not found in volumes")
}, 1*time.Second, interval).Should(Succeed(), "Merged credentials secret is added to deployment")
})
})

Context("Stopping DevWorkspaces", func() {
Expand Down
108 changes: 108 additions & 0 deletions controllers/workspace/eventhandlers.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
// Copyright (c) 2019-2023 Red Hat, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package controllers

import (
"context"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
wkspConfig "github.com/devfile/devworkspace-operator/pkg/config"
"github.com/devfile/devworkspace-operator/pkg/constants"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

// Mapping the pod to the devworkspace
func dwRelatedPodsHandler(obj client.Object) []reconcile.Request {
labels := obj.GetLabels()
if _, ok := labels[constants.DevWorkspaceNameLabel]; !ok {
return []reconcile.Request{}
}

//If the dewworkspace label does not exist, do no reconcile
if _, ok := labels[constants.DevWorkspaceIDLabel]; !ok {
return []reconcile.Request{}
}

return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: labels[constants.DevWorkspaceNameLabel],
Namespace: obj.GetNamespace(),
},
},
}
}

func (r *DevWorkspaceReconciler) dwPVCHandler(obj client.Object) []reconcile.Request {
// Check if PVC is owned by a DevWorkspace (per-workspace storage case)
for _, ownerref := range obj.GetOwnerReferences() {
if ownerref.Kind != "DevWorkspace" {
continue
}
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: ownerref.Name,
Namespace: obj.GetNamespace(),
},
},
}
}

// TODO: Label PVCs used for workspace storage so that they can be cleaned up if non-default name is used.
// Otherwise, check if common PVC is deleted to make sure all DevWorkspaces see it happen
if obj.GetName() != wkspConfig.GetGlobalConfig().Workspace.PVCName || obj.GetDeletionTimestamp() == nil {
// We're looking for a deleted common PVC
return []reconcile.Request{}
}
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
storageType := workspace.Spec.Template.Attributes.GetString(constants.DevWorkspaceStorageTypeAttribute, nil)
if storageType == constants.CommonStorageClassType || storageType == constants.PerUserStorageClassType || storageType == "" {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}

func (r *DevWorkspaceReconciler) runningWorkspacesHandler(obj client.Object) []reconcile.Request {
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
// Queue reconciles for any started workspaces to make sure they pick up new object
if workspace.Spec.Started {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}
32 changes: 30 additions & 2 deletions controllers/workspace/predicates.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,14 +21,15 @@ import (

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)

// predicates filters incoming events to avoid unnecessary reconciles to failed workspaces.
// devworkspacePredicates filters incoming events to avoid unnecessary reconciles to failed workspaces.
// If a workspace failed and its spec is changed, we trigger reconciles to allow for fixing
// issues in the workspace spec.
var predicates = predicate.Funcs{
var devworkspacePredicates = predicate.Funcs{
CreateFunc: func(_ event.CreateEvent) bool { return true },
DeleteFunc: func(_ event.DeleteEvent) bool { return true },
UpdateFunc: func(ev event.UpdateEvent) bool {
Expand DownExpand Up@@ -84,3 +85,30 @@ var podPredicates = predicate.Funcs{
return true
},
}

var automountPredicates = predicate.Funcs{
CreateFunc: func(ev event.CreateEvent) bool {
return objectIsAutomountResource(ev.Object)
},
DeleteFunc: func(ev event.DeleteEvent) bool {
return objectIsAutomountResource(ev.Object)
},
UpdateFunc: func(ev event.UpdateEvent) bool {
return objectIsAutomountResource(ev.ObjectNew)
},
GenericFunc: func(_ event.GenericEvent) bool { return false },
}

func objectIsAutomountResource(obj client.Object) bool {
labels := obj.GetLabels()
switch {
case labels[constants.DevWorkspaceMountLabel] == "true",
labels[constants.DevWorkspaceGitCredentialLabel] == "true",
labels[constants.DevWorkspaceGitTLSLabel] == "true",
labels[constants.DevWorkspacePullSecretLabel] == "true":
return true
default:
return false
}

}
9 changes: 9 additions & 0 deletions pkg/constants/metadata.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,15 @@ const (
// If the git host is not defined then the certificate will be used for all http repositories.
DevWorkspaceGitTLSLabel = "controller.devfile.io/git-tls-credential"

// GitCredentialsConfigMapName is the name used for the configmap that stores the Git configuration for workspaces
// in a given namespace. It is used when e.g. adding Git credentials via secret
GitCredentialsConfigMapName = "devworkspace-gitconfig"

// GitCredentialsMergedSecretName is the name for the merged Git credentials secret that is mounted to workspaces
// when Git credentials are defined. This secret combines the values of any secrets labelled
// "controller.devfile.io/git-credential"
GitCredentialsMergedSecretName = "devworkspace-merged-git-credentials"

// DevWorkspaceMountPathAnnotation is the annotation key to store the mount path for the secret or configmap.
// If no mount path is provided, configmaps will be mounted at /etc/config/<configmap-name>, secrets will
// be mounted at /etc/secret/<secret-name>, and persistent volume claims will be mounted to /tmp/<claim-name>
Expand Down
4 changes: 2 additions & 2 deletions pkg/provision/automount/gitconfig.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ func getGitResources(api sync.ClusterAPI, namespace string) (credentialSecrets [

func cleanupGitConfig(api sync.ClusterAPI, namespace string) error {
secretNN := types.NamespacedName{
Name: gitCredentialsSecretName,
Name: constants.GitCredentialsMergedSecretName,
Namespace: namespace,
}
tlsSecret := &corev1.Secret{}
Expand All@@ -134,7 +134,7 @@ func cleanupGitConfig(api sync.ClusterAPI, namespace string) error {
}

configmapNN := types.NamespacedName{
Name: gitCredentialsConfigMapName,
Name: constants.GitCredentialsConfigMapName,
Namespace: namespace,
}
credentialsConfigMap := &corev1.ConfigMap{}
Expand Down
6 changes: 2 additions & 4 deletions pkg/provision/automount/templates.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,10 +28,8 @@ const gitTLSCertificateKey = "certificate"

const gitConfigName = "gitconfig"
const gitConfigLocation = "/etc/" + gitConfigName
const gitCredentialsConfigMapName = "devworkspace-gitconfig"

const gitCredentialsSecretKey = "credentials"
const gitCredentialsSecretName = "devworkspace-merged-git-credentials"

// gitLFSConfig is the default configuration that gets provisioned when git-lfs
// is installed. It needs to be included in the overridden gitconfig to avoid
Expand DownExpand Up@@ -95,7 +93,7 @@ func constructGitConfig(namespace, credentialMountPath string, certificatesConfi

gitConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: gitCredentialsConfigMapName,
Name: constants.GitCredentialsConfigMapName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/defaultName": "git-config-secret",
Expand All@@ -122,7 +120,7 @@ func mergeGitCredentials(namespace string, credentialSecrets []corev1.Secret) (*
}
mergedCredentials := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: gitCredentialsSecretName,
Name: constants.GitCredentialsMergedSecretName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/defaultName": "git-config-secret",
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 6 additions & 65 deletions controllers/workspace/devworkspace_controller.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -700,69 +700,6 @@ func (r *DevWorkspaceReconciler) getWorkspaceId(ctx context.Context, workspace *
}
}

// Mapping the pod to the devworkspace
func dwRelatedPodsHandler(obj client.Object) []reconcile.Request {
labels := obj.GetLabels()
if _, ok := labels[constants.DevWorkspaceNameLabel]; !ok {
return []reconcile.Request{}
}

//If the dewworkspace label does not exist, do no reconcile
if _, ok := labels[constants.DevWorkspaceIDLabel]; !ok {
return []reconcile.Request{}
}

return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: labels[constants.DevWorkspaceNameLabel],
Namespace: obj.GetNamespace(),
},
},
}
}

func (r *DevWorkspaceReconciler) dwPVCHandler(obj client.Object) []reconcile.Request {
// Check if PVC is owned by a DevWorkspace (per-workspace storage case)
for _, ownerref := range obj.GetOwnerReferences() {
if ownerref.Kind != "DevWorkspace" {
continue
}
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: ownerref.Name,
Namespace: obj.GetNamespace(),
},
},
}
}

// TODO: Label PVCs used for workspace storage so that they can be cleaned up if non-default name is used.
// Otherwise, check if common PVC is deleted to make sure all DevWorkspaces see it happen
if obj.GetName() != wkspConfig.GetGlobalConfig().Workspace.PVCName || obj.GetDeletionTimestamp() == nil {
// We're looking for a deleted common PVC
return []reconcile.Request{}
}
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
storageType := workspace.Spec.Template.Attributes.GetString(constants.DevWorkspaceStorageTypeAttribute, nil)
if storageType == constants.CommonStorageClassType || storageType == constants.PerUserStorageClassType || storageType == "" {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}

func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
setupHttpClients()

Expand All@@ -775,7 +712,8 @@ func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
return []reconcile.Request{}
}

var configWatcher builder.WatchesOption = builder.WithPredicates(wkspConfig.Predicates())
configWatcher := builder.WithPredicates(wkspConfig.Predicates())
automountWatcher := builder.WithPredicates(automountPredicates)

// TODO: Set up indexing https://book.kubebuilder.io/cronjob-tutorial/controller-implementation.html#setup
return ctrl.NewControllerManagedBy(mgr).
Expand All@@ -793,8 +731,11 @@ func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
Owns(&corev1.ServiceAccount{}).
Watches(&source.Kind{Type: &corev1.Pod{}}, handler.EnqueueRequestsFromMapFunc(dwRelatedPodsHandler)).
Watches(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, handler.EnqueueRequestsFromMapFunc(r.dwPVCHandler)).
Watches(&source.Kind{Type: &corev1.Secret{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &corev1.ConfigMap{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &controllerv1alpha1.DevWorkspaceOperatorConfig{}}, handler.EnqueueRequestsFromMapFunc(emptyMapper), configWatcher).
WithEventFilter(predicates).
WithEventFilter(devworkspacePredicates).
WithEventFilter(podPredicates).
Complete(r)
}
44 changes: 43 additions & 1 deletion controllers/workspace/devworkspace_controller_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -353,7 +353,6 @@ var _ = Describe("DevWorkspace Controller", func() {
},
},
})
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
})

AfterEach(func() {
Expand All@@ -362,6 +361,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Mounts image pull secrets to the DevWorkspace Deployment", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -394,6 +394,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Manages git credentials for DevWorkspace", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -456,6 +457,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Automounts secrets and configmaps volumes", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -518,6 +520,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Automounts secrets and configmaps env vars", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -558,6 +561,45 @@ var _ = Describe("DevWorkspace Controller", func() {
Expect(container.EnvFrom).Should(ContainElements(expectedEnvFromSources), "Automounted env sources should be added to containers")
}
})

It("Detects changes to automount resources and reconciles", func() {
// NOTE: timeout for this test is reduced, as eventually DWO will reconcile the workspace by coincidence and notice
// the automount secret.
createStartedDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

mergedSecretNN := namespacedName(constants.GitCredentialsMergedSecretName, testNamespace)
mergedSecret := &corev1.Secret{}
Expect(k8sClient.Get(ctx, mergedSecretNN, mergedSecret)).Error()

By("Creating git-credential secret")
secret := generateSecret("git-credential-secret", corev1.SecretTypeOpaque)
secret.Labels[constants.DevWorkspaceGitCredentialLabel] = "true"
secret.Data["credentials"] = []byte("https://test:token@github.com")
createObject(secret)
defer deleteObject(secret)

By("Checking that merged credentials secret is created")
Eventually(func() error {
return k8sClient.Get(ctx, mergedSecretNN, mergedSecret)
}, 1*time.Second, interval).Should(Succeed(), "Merged credentials secret is created")

By("Checking that workspace deployment mounts merged credentials secret")
Eventually(func() error {
deploy := &appsv1.Deployment{}
deployNN := namespacedName(common.DeploymentName(workspaceID), testNamespace)
if err := k8sClient.Get(ctx, deployNN, deploy); err != nil {
return err
}
for _, volume := range deploy.Spec.Template.Spec.Volumes {
if volume.Secret != nil && volume.Secret.SecretName == constants.GitCredentialsMergedSecretName {
return nil
}
}
return fmt.Errorf("Secret not found in volumes")
}, 1*time.Second, interval).Should(Succeed(), "Merged credentials secret is added to deployment")
})
})

Context("Stopping DevWorkspaces", func() {
Expand Down
108 changes: 108 additions & 0 deletions controllers/workspace/eventhandlers.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
// Copyright (c) 2019-2023 Red Hat, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package controllers

import (
"context"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
wkspConfig "github.com/devfile/devworkspace-operator/pkg/config"
"github.com/devfile/devworkspace-operator/pkg/constants"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

// Mapping the pod to the devworkspace
func dwRelatedPodsHandler(obj client.Object) []reconcile.Request {
labels := obj.GetLabels()
if _, ok := labels[constants.DevWorkspaceNameLabel]; !ok {
return []reconcile.Request{}
}

//If the dewworkspace label does not exist, do no reconcile
if _, ok := labels[constants.DevWorkspaceIDLabel]; !ok {
return []reconcile.Request{}
}

return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: labels[constants.DevWorkspaceNameLabel],
Namespace: obj.GetNamespace(),
},
},
}
}

func (r *DevWorkspaceReconciler) dwPVCHandler(obj client.Object) []reconcile.Request {
// Check if PVC is owned by a DevWorkspace (per-workspace storage case)
for _, ownerref := range obj.GetOwnerReferences() {
if ownerref.Kind != "DevWorkspace" {
continue
}
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: ownerref.Name,
Namespace: obj.GetNamespace(),
},
},
}
}

// TODO: Label PVCs used for workspace storage so that they can be cleaned up if non-default name is used.
// Otherwise, check if common PVC is deleted to make sure all DevWorkspaces see it happen
if obj.GetName() != wkspConfig.GetGlobalConfig().Workspace.PVCName || obj.GetDeletionTimestamp() == nil {
// We're looking for a deleted common PVC
return []reconcile.Request{}
}
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
storageType := workspace.Spec.Template.Attributes.GetString(constants.DevWorkspaceStorageTypeAttribute, nil)
if storageType == constants.CommonStorageClassType || storageType == constants.PerUserStorageClassType || storageType == "" {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}

func (r *DevWorkspaceReconciler) runningWorkspacesHandler(obj client.Object) []reconcile.Request {
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
// Queue reconciles for any started workspaces to make sure they pick up new object
if workspace.Spec.Started {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}
32 changes: 30 additions & 2 deletions controllers/workspace/predicates.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,14 +21,15 @@ import (

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)

// predicates filters incoming events to avoid unnecessary reconciles to failed workspaces.
// devworkspacePredicates filters incoming events to avoid unnecessary reconciles to failed workspaces.
// If a workspace failed and its spec is changed, we trigger reconciles to allow for fixing
// issues in the workspace spec.
var predicates = predicate.Funcs{
var devworkspacePredicates = predicate.Funcs{
CreateFunc: func(_ event.CreateEvent) bool { return true },
DeleteFunc: func(_ event.DeleteEvent) bool { return true },
UpdateFunc: func(ev event.UpdateEvent) bool {
Expand DownExpand Up@@ -84,3 +85,30 @@ var podPredicates = predicate.Funcs{
return true
},
}

var automountPredicates = predicate.Funcs{
CreateFunc: func(ev event.CreateEvent) bool {
return objectIsAutomountResource(ev.Object)
},
DeleteFunc: func(ev event.DeleteEvent) bool {
return objectIsAutomountResource(ev.Object)
},
UpdateFunc: func(ev event.UpdateEvent) bool {
return objectIsAutomountResource(ev.ObjectNew)
},
GenericFunc: func(_ event.GenericEvent) bool { return false },
}

func objectIsAutomountResource(obj client.Object) bool {
labels := obj.GetLabels()
switch {
case labels[constants.DevWorkspaceMountLabel] == "true",
labels[constants.DevWorkspaceGitCredentialLabel] == "true",
labels[constants.DevWorkspaceGitTLSLabel] == "true",
labels[constants.DevWorkspacePullSecretLabel] == "true":
return true
default:
return false
}

}
9 changes: 9 additions & 0 deletions pkg/constants/metadata.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,15 @@ const (
// If the git host is not defined then the certificate will be used for all http repositories.
DevWorkspaceGitTLSLabel = "controller.devfile.io/git-tls-credential"

// GitCredentialsConfigMapName is the name used for the configmap that stores the Git configuration for workspaces
// in a given namespace. It is used when e.g. adding Git credentials via secret
GitCredentialsConfigMapName = "devworkspace-gitconfig"

// GitCredentialsMergedSecretName is the name for the merged Git credentials secret that is mounted to workspaces
// when Git credentials are defined. This secret combines the values of any secrets labelled
// "controller.devfile.io/git-credential"
GitCredentialsMergedSecretName = "devworkspace-merged-git-credentials"

// DevWorkspaceMountPathAnnotation is the annotation key to store the mount path for the secret or configmap.
// If no mount path is provided, configmaps will be mounted at /etc/config/<configmap-name>, secrets will
// be mounted at /etc/secret/<secret-name>, and persistent volume claims will be mounted to /tmp/<claim-name>
Expand Down
4 changes: 2 additions & 2 deletions pkg/provision/automount/gitconfig.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ func getGitResources(api sync.ClusterAPI, namespace string) (credentialSecrets [

func cleanupGitConfig(api sync.ClusterAPI, namespace string) error {
secretNN := types.NamespacedName{
Name: gitCredentialsSecretName,
Name: constants.GitCredentialsMergedSecretName,
Namespace: namespace,
}
tlsSecret := &corev1.Secret{}
Expand All@@ -134,7 +134,7 @@ func cleanupGitConfig(api sync.ClusterAPI, namespace string) error {
}

configmapNN := types.NamespacedName{
Name: gitCredentialsConfigMapName,
Name: constants.GitCredentialsConfigMapName,
Namespace: namespace,
}
credentialsConfigMap := &corev1.ConfigMap{}
Expand Down
6 changes: 2 additions & 4 deletions pkg/provision/automount/templates.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,10 +28,8 @@ const gitTLSCertificateKey = "certificate"

const gitConfigName = "gitconfig"
const gitConfigLocation = "/etc/" + gitConfigName
const gitCredentialsConfigMapName = "devworkspace-gitconfig"

const gitCredentialsSecretKey = "credentials"
const gitCredentialsSecretName = "devworkspace-merged-git-credentials"

// gitLFSConfig is the default configuration that gets provisioned when git-lfs
// is installed. It needs to be included in the overridden gitconfig to avoid
Expand DownExpand Up@@ -95,7 +93,7 @@ func constructGitConfig(namespace, credentialMountPath string, certificatesConfi

gitConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: gitCredentialsConfigMapName,
Name: constants.GitCredentialsConfigMapName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/defaultName": "git-config-secret",
Expand All@@ -122,7 +120,7 @@ func mergeGitCredentials(namespace string, credentialSecrets []corev1.Secret) (*
}
mergedCredentials := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: gitCredentialsSecretName,
Name: constants.GitCredentialsMergedSecretName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/defaultName": "git-config-secret",
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 6 additions & 65 deletions controllers/workspace/devworkspace_controller.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -700,69 +700,6 @@ func (r *DevWorkspaceReconciler) getWorkspaceId(ctx context.Context, workspace *
}
}

// Mapping the pod to the devworkspace
func dwRelatedPodsHandler(obj client.Object) []reconcile.Request {
labels := obj.GetLabels()
if _, ok := labels[constants.DevWorkspaceNameLabel]; !ok {
return []reconcile.Request{}
}

//If the dewworkspace label does not exist, do no reconcile
if _, ok := labels[constants.DevWorkspaceIDLabel]; !ok {
return []reconcile.Request{}
}

return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: labels[constants.DevWorkspaceNameLabel],
Namespace: obj.GetNamespace(),
},
},
}
}

func (r *DevWorkspaceReconciler) dwPVCHandler(obj client.Object) []reconcile.Request {
// Check if PVC is owned by a DevWorkspace (per-workspace storage case)
for _, ownerref := range obj.GetOwnerReferences() {
if ownerref.Kind != "DevWorkspace" {
continue
}
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: ownerref.Name,
Namespace: obj.GetNamespace(),
},
},
}
}

// TODO: Label PVCs used for workspace storage so that they can be cleaned up if non-default name is used.
// Otherwise, check if common PVC is deleted to make sure all DevWorkspaces see it happen
if obj.GetName() != wkspConfig.GetGlobalConfig().Workspace.PVCName || obj.GetDeletionTimestamp() == nil {
// We're looking for a deleted common PVC
return []reconcile.Request{}
}
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
storageType := workspace.Spec.Template.Attributes.GetString(constants.DevWorkspaceStorageTypeAttribute, nil)
if storageType == constants.CommonStorageClassType || storageType == constants.PerUserStorageClassType || storageType == "" {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}

func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
setupHttpClients()

Expand All@@ -775,7 +712,8 @@ func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
return []reconcile.Request{}
}

var configWatcher builder.WatchesOption = builder.WithPredicates(wkspConfig.Predicates())
configWatcher := builder.WithPredicates(wkspConfig.Predicates())
automountWatcher := builder.WithPredicates(automountPredicates)

// TODO: Set up indexing https://book.kubebuilder.io/cronjob-tutorial/controller-implementation.html#setup
return ctrl.NewControllerManagedBy(mgr).
Expand All@@ -793,8 +731,11 @@ func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
Owns(&corev1.ServiceAccount{}).
Watches(&source.Kind{Type: &corev1.Pod{}}, handler.EnqueueRequestsFromMapFunc(dwRelatedPodsHandler)).
Watches(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, handler.EnqueueRequestsFromMapFunc(r.dwPVCHandler)).
Watches(&source.Kind{Type: &corev1.Secret{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &corev1.ConfigMap{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &controllerv1alpha1.DevWorkspaceOperatorConfig{}}, handler.EnqueueRequestsFromMapFunc(emptyMapper), configWatcher).
WithEventFilter(predicates).
WithEventFilter(devworkspacePredicates).
WithEventFilter(podPredicates).
Complete(r)
}
44 changes: 43 additions & 1 deletion controllers/workspace/devworkspace_controller_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -353,7 +353,6 @@ var _ = Describe("DevWorkspace Controller", func() {
},
},
})
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
})

AfterEach(func() {
Expand All@@ -362,6 +361,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Mounts image pull secrets to the DevWorkspace Deployment", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -394,6 +394,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Manages git credentials for DevWorkspace", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -456,6 +457,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Automounts secrets and configmaps volumes", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -518,6 +520,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Automounts secrets and configmaps env vars", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -558,6 +561,45 @@ var _ = Describe("DevWorkspace Controller", func() {
Expect(container.EnvFrom).Should(ContainElements(expectedEnvFromSources), "Automounted env sources should be added to containers")
}
})

It("Detects changes to automount resources and reconciles", func() {
// NOTE: timeout for this test is reduced, as eventually DWO will reconcile the workspace by coincidence and notice
// the automount secret.
createStartedDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

mergedSecretNN := namespacedName(constants.GitCredentialsMergedSecretName, testNamespace)
mergedSecret := &corev1.Secret{}
Expect(k8sClient.Get(ctx, mergedSecretNN, mergedSecret)).Error()

By("Creating git-credential secret")
secret := generateSecret("git-credential-secret", corev1.SecretTypeOpaque)
secret.Labels[constants.DevWorkspaceGitCredentialLabel] = "true"
secret.Data["credentials"] = []byte("https://test:token@github.com")
createObject(secret)
defer deleteObject(secret)

By("Checking that merged credentials secret is created")
Eventually(func() error {
return k8sClient.Get(ctx, mergedSecretNN, mergedSecret)
}, 1*time.Second, interval).Should(Succeed(), "Merged credentials secret is created")

By("Checking that workspace deployment mounts merged credentials secret")
Eventually(func() error {
deploy := &appsv1.Deployment{}
deployNN := namespacedName(common.DeploymentName(workspaceID), testNamespace)
if err := k8sClient.Get(ctx, deployNN, deploy); err != nil {
return err
}
for _, volume := range deploy.Spec.Template.Spec.Volumes {
if volume.Secret != nil && volume.Secret.SecretName == constants.GitCredentialsMergedSecretName {
return nil
}
}
return fmt.Errorf("Secret not found in volumes")
}, 1*time.Second, interval).Should(Succeed(), "Merged credentials secret is added to deployment")
})
})

Context("Stopping DevWorkspaces", func() {
Expand Down
108 changes: 108 additions & 0 deletions controllers/workspace/eventhandlers.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
// Copyright (c) 2019-2023 Red Hat, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package controllers

import (
"context"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
wkspConfig "github.com/devfile/devworkspace-operator/pkg/config"
"github.com/devfile/devworkspace-operator/pkg/constants"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

// Mapping the pod to the devworkspace
func dwRelatedPodsHandler(obj client.Object) []reconcile.Request {
labels := obj.GetLabels()
if _, ok := labels[constants.DevWorkspaceNameLabel]; !ok {
return []reconcile.Request{}
}

//If the dewworkspace label does not exist, do no reconcile
if _, ok := labels[constants.DevWorkspaceIDLabel]; !ok {
return []reconcile.Request{}
}

return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: labels[constants.DevWorkspaceNameLabel],
Namespace: obj.GetNamespace(),
},
},
}
}

func (r *DevWorkspaceReconciler) dwPVCHandler(obj client.Object) []reconcile.Request {
// Check if PVC is owned by a DevWorkspace (per-workspace storage case)
for _, ownerref := range obj.GetOwnerReferences() {
if ownerref.Kind != "DevWorkspace" {
continue
}
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: ownerref.Name,
Namespace: obj.GetNamespace(),
},
},
}
}

// TODO: Label PVCs used for workspace storage so that they can be cleaned up if non-default name is used.
// Otherwise, check if common PVC is deleted to make sure all DevWorkspaces see it happen
if obj.GetName() != wkspConfig.GetGlobalConfig().Workspace.PVCName || obj.GetDeletionTimestamp() == nil {
// We're looking for a deleted common PVC
return []reconcile.Request{}
}
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
storageType := workspace.Spec.Template.Attributes.GetString(constants.DevWorkspaceStorageTypeAttribute, nil)
if storageType == constants.CommonStorageClassType || storageType == constants.PerUserStorageClassType || storageType == "" {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}

func (r *DevWorkspaceReconciler) runningWorkspacesHandler(obj client.Object) []reconcile.Request {
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
// Queue reconciles for any started workspaces to make sure they pick up new object
if workspace.Spec.Started {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}
32 changes: 30 additions & 2 deletions controllers/workspace/predicates.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,14 +21,15 @@ import (

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)

// predicates filters incoming events to avoid unnecessary reconciles to failed workspaces.
// devworkspacePredicates filters incoming events to avoid unnecessary reconciles to failed workspaces.
// If a workspace failed and its spec is changed, we trigger reconciles to allow for fixing
// issues in the workspace spec.
var predicates = predicate.Funcs{
var devworkspacePredicates = predicate.Funcs{
CreateFunc: func(_ event.CreateEvent) bool { return true },
DeleteFunc: func(_ event.DeleteEvent) bool { return true },
UpdateFunc: func(ev event.UpdateEvent) bool {
Expand DownExpand Up@@ -84,3 +85,30 @@ var podPredicates = predicate.Funcs{
return true
},
}

var automountPredicates = predicate.Funcs{
CreateFunc: func(ev event.CreateEvent) bool {
return objectIsAutomountResource(ev.Object)
},
DeleteFunc: func(ev event.DeleteEvent) bool {
return objectIsAutomountResource(ev.Object)
},
UpdateFunc: func(ev event.UpdateEvent) bool {
return objectIsAutomountResource(ev.ObjectNew)
},
GenericFunc: func(_ event.GenericEvent) bool { return false },
}

func objectIsAutomountResource(obj client.Object) bool {
labels := obj.GetLabels()
switch {
case labels[constants.DevWorkspaceMountLabel] == "true",
labels[constants.DevWorkspaceGitCredentialLabel] == "true",
labels[constants.DevWorkspaceGitTLSLabel] == "true",
labels[constants.DevWorkspacePullSecretLabel] == "true":
return true
default:
return false
}

}
9 changes: 9 additions & 0 deletions pkg/constants/metadata.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,15 @@ const (
// If the git host is not defined then the certificate will be used for all http repositories.
DevWorkspaceGitTLSLabel = "controller.devfile.io/git-tls-credential"

// GitCredentialsConfigMapName is the name used for the configmap that stores the Git configuration for workspaces
// in a given namespace. It is used when e.g. adding Git credentials via secret
GitCredentialsConfigMapName = "devworkspace-gitconfig"

// GitCredentialsMergedSecretName is the name for the merged Git credentials secret that is mounted to workspaces
// when Git credentials are defined. This secret combines the values of any secrets labelled
// "controller.devfile.io/git-credential"
GitCredentialsMergedSecretName = "devworkspace-merged-git-credentials"

// DevWorkspaceMountPathAnnotation is the annotation key to store the mount path for the secret or configmap.
// If no mount path is provided, configmaps will be mounted at /etc/config/<configmap-name>, secrets will
// be mounted at /etc/secret/<secret-name>, and persistent volume claims will be mounted to /tmp/<claim-name>
Expand Down
4 changes: 2 additions & 2 deletions pkg/provision/automount/gitconfig.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ func getGitResources(api sync.ClusterAPI, namespace string) (credentialSecrets [

func cleanupGitConfig(api sync.ClusterAPI, namespace string) error {
secretNN := types.NamespacedName{
Name: gitCredentialsSecretName,
Name: constants.GitCredentialsMergedSecretName,
Namespace: namespace,
}
tlsSecret := &corev1.Secret{}
Expand All@@ -134,7 +134,7 @@ func cleanupGitConfig(api sync.ClusterAPI, namespace string) error {
}

configmapNN := types.NamespacedName{
Name: gitCredentialsConfigMapName,
Name: constants.GitCredentialsConfigMapName,
Namespace: namespace,
}
credentialsConfigMap := &corev1.ConfigMap{}
Expand Down
6 changes: 2 additions & 4 deletions pkg/provision/automount/templates.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,10 +28,8 @@ const gitTLSCertificateKey = "certificate"

const gitConfigName = "gitconfig"
const gitConfigLocation = "/etc/" + gitConfigName
const gitCredentialsConfigMapName = "devworkspace-gitconfig"

const gitCredentialsSecretKey = "credentials"
const gitCredentialsSecretName = "devworkspace-merged-git-credentials"

// gitLFSConfig is the default configuration that gets provisioned when git-lfs
// is installed. It needs to be included in the overridden gitconfig to avoid
Expand DownExpand Up@@ -95,7 +93,7 @@ func constructGitConfig(namespace, credentialMountPath string, certificatesConfi

gitConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: gitCredentialsConfigMapName,
Name: constants.GitCredentialsConfigMapName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/defaultName": "git-config-secret",
Expand All@@ -122,7 +120,7 @@ func mergeGitCredentials(namespace string, credentialSecrets []corev1.Secret) (*
}
mergedCredentials := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: gitCredentialsSecretName,
Name: constants.GitCredentialsMergedSecretName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/defaultName": "git-config-secret",
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 6 additions & 65 deletions controllers/workspace/devworkspace_controller.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -700,69 +700,6 @@ func (r *DevWorkspaceReconciler) getWorkspaceId(ctx context.Context, workspace *
}
}

// Mapping the pod to the devworkspace
func dwRelatedPodsHandler(obj client.Object) []reconcile.Request {
labels := obj.GetLabels()
if _, ok := labels[constants.DevWorkspaceNameLabel]; !ok {
return []reconcile.Request{}
}

//If the dewworkspace label does not exist, do no reconcile
if _, ok := labels[constants.DevWorkspaceIDLabel]; !ok {
return []reconcile.Request{}
}

return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: labels[constants.DevWorkspaceNameLabel],
Namespace: obj.GetNamespace(),
},
},
}
}

func (r *DevWorkspaceReconciler) dwPVCHandler(obj client.Object) []reconcile.Request {
// Check if PVC is owned by a DevWorkspace (per-workspace storage case)
for _, ownerref := range obj.GetOwnerReferences() {
if ownerref.Kind != "DevWorkspace" {
continue
}
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: ownerref.Name,
Namespace: obj.GetNamespace(),
},
},
}
}

// TODO: Label PVCs used for workspace storage so that they can be cleaned up if non-default name is used.
// Otherwise, check if common PVC is deleted to make sure all DevWorkspaces see it happen
if obj.GetName() != wkspConfig.GetGlobalConfig().Workspace.PVCName || obj.GetDeletionTimestamp() == nil {
// We're looking for a deleted common PVC
return []reconcile.Request{}
}
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
storageType := workspace.Spec.Template.Attributes.GetString(constants.DevWorkspaceStorageTypeAttribute, nil)
if storageType == constants.CommonStorageClassType || storageType == constants.PerUserStorageClassType || storageType == "" {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}

func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
setupHttpClients()

Expand All@@ -775,7 +712,8 @@ func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
return []reconcile.Request{}
}

var configWatcher builder.WatchesOption = builder.WithPredicates(wkspConfig.Predicates())
configWatcher := builder.WithPredicates(wkspConfig.Predicates())
automountWatcher := builder.WithPredicates(automountPredicates)

// TODO: Set up indexing https://book.kubebuilder.io/cronjob-tutorial/controller-implementation.html#setup
return ctrl.NewControllerManagedBy(mgr).
Expand All@@ -793,8 +731,11 @@ func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
Owns(&corev1.ServiceAccount{}).
Watches(&source.Kind{Type: &corev1.Pod{}}, handler.EnqueueRequestsFromMapFunc(dwRelatedPodsHandler)).
Watches(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, handler.EnqueueRequestsFromMapFunc(r.dwPVCHandler)).
Watches(&source.Kind{Type: &corev1.Secret{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &corev1.ConfigMap{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &controllerv1alpha1.DevWorkspaceOperatorConfig{}}, handler.EnqueueRequestsFromMapFunc(emptyMapper), configWatcher).
WithEventFilter(predicates).
WithEventFilter(devworkspacePredicates).
WithEventFilter(podPredicates).
Complete(r)
}
44 changes: 43 additions & 1 deletion controllers/workspace/devworkspace_controller_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -353,7 +353,6 @@ var _ = Describe("DevWorkspace Controller", func() {
},
},
})
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
})

AfterEach(func() {
Expand All@@ -362,6 +361,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Mounts image pull secrets to the DevWorkspace Deployment", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -394,6 +394,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Manages git credentials for DevWorkspace", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -456,6 +457,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Automounts secrets and configmaps volumes", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -518,6 +520,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Automounts secrets and configmaps env vars", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -558,6 +561,45 @@ var _ = Describe("DevWorkspace Controller", func() {
Expect(container.EnvFrom).Should(ContainElements(expectedEnvFromSources), "Automounted env sources should be added to containers")
}
})

It("Detects changes to automount resources and reconciles", func() {
// NOTE: timeout for this test is reduced, as eventually DWO will reconcile the workspace by coincidence and notice
// the automount secret.
createStartedDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

mergedSecretNN := namespacedName(constants.GitCredentialsMergedSecretName, testNamespace)
mergedSecret := &corev1.Secret{}
Expect(k8sClient.Get(ctx, mergedSecretNN, mergedSecret)).Error()

By("Creating git-credential secret")
secret := generateSecret("git-credential-secret", corev1.SecretTypeOpaque)
secret.Labels[constants.DevWorkspaceGitCredentialLabel] = "true"
secret.Data["credentials"] = []byte("https://test:token@github.com")
createObject(secret)
defer deleteObject(secret)

By("Checking that merged credentials secret is created")
Eventually(func() error {
return k8sClient.Get(ctx, mergedSecretNN, mergedSecret)
}, 1*time.Second, interval).Should(Succeed(), "Merged credentials secret is created")

By("Checking that workspace deployment mounts merged credentials secret")
Eventually(func() error {
deploy := &appsv1.Deployment{}
deployNN := namespacedName(common.DeploymentName(workspaceID), testNamespace)
if err := k8sClient.Get(ctx, deployNN, deploy); err != nil {
return err
}
for _, volume := range deploy.Spec.Template.Spec.Volumes {
if volume.Secret != nil && volume.Secret.SecretName == constants.GitCredentialsMergedSecretName {
return nil
}
}
return fmt.Errorf("Secret not found in volumes")
}, 1*time.Second, interval).Should(Succeed(), "Merged credentials secret is added to deployment")
})
})

Context("Stopping DevWorkspaces", func() {
Expand Down
108 changes: 108 additions & 0 deletions controllers/workspace/eventhandlers.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
// Copyright (c) 2019-2023 Red Hat, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package controllers

import (
"context"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
wkspConfig "github.com/devfile/devworkspace-operator/pkg/config"
"github.com/devfile/devworkspace-operator/pkg/constants"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

// Mapping the pod to the devworkspace
func dwRelatedPodsHandler(obj client.Object) []reconcile.Request {
labels := obj.GetLabels()
if _, ok := labels[constants.DevWorkspaceNameLabel]; !ok {
return []reconcile.Request{}
}

//If the dewworkspace label does not exist, do no reconcile
if _, ok := labels[constants.DevWorkspaceIDLabel]; !ok {
return []reconcile.Request{}
}

return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: labels[constants.DevWorkspaceNameLabel],
Namespace: obj.GetNamespace(),
},
},
}
}

func (r *DevWorkspaceReconciler) dwPVCHandler(obj client.Object) []reconcile.Request {
// Check if PVC is owned by a DevWorkspace (per-workspace storage case)
for _, ownerref := range obj.GetOwnerReferences() {
if ownerref.Kind != "DevWorkspace" {
continue
}
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: ownerref.Name,
Namespace: obj.GetNamespace(),
},
},
}
}

// TODO: Label PVCs used for workspace storage so that they can be cleaned up if non-default name is used.
// Otherwise, check if common PVC is deleted to make sure all DevWorkspaces see it happen
if obj.GetName() != wkspConfig.GetGlobalConfig().Workspace.PVCName || obj.GetDeletionTimestamp() == nil {
// We're looking for a deleted common PVC
return []reconcile.Request{}
}
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
storageType := workspace.Spec.Template.Attributes.GetString(constants.DevWorkspaceStorageTypeAttribute, nil)
if storageType == constants.CommonStorageClassType || storageType == constants.PerUserStorageClassType || storageType == "" {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}

func (r *DevWorkspaceReconciler) runningWorkspacesHandler(obj client.Object) []reconcile.Request {
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
// Queue reconciles for any started workspaces to make sure they pick up new object
if workspace.Spec.Started {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}
32 changes: 30 additions & 2 deletions controllers/workspace/predicates.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,14 +21,15 @@ import (

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)

// predicates filters incoming events to avoid unnecessary reconciles to failed workspaces.
// devworkspacePredicates filters incoming events to avoid unnecessary reconciles to failed workspaces.
// If a workspace failed and its spec is changed, we trigger reconciles to allow for fixing
// issues in the workspace spec.
var predicates = predicate.Funcs{
var devworkspacePredicates = predicate.Funcs{
CreateFunc: func(_ event.CreateEvent) bool { return true },
DeleteFunc: func(_ event.DeleteEvent) bool { return true },
UpdateFunc: func(ev event.UpdateEvent) bool {
Expand DownExpand Up@@ -84,3 +85,30 @@ var podPredicates = predicate.Funcs{
return true
},
}

var automountPredicates = predicate.Funcs{
CreateFunc: func(ev event.CreateEvent) bool {
return objectIsAutomountResource(ev.Object)
},
DeleteFunc: func(ev event.DeleteEvent) bool {
return objectIsAutomountResource(ev.Object)
},
UpdateFunc: func(ev event.UpdateEvent) bool {
return objectIsAutomountResource(ev.ObjectNew)
},
GenericFunc: func(_ event.GenericEvent) bool { return false },
}

func objectIsAutomountResource(obj client.Object) bool {
labels := obj.GetLabels()
switch {
case labels[constants.DevWorkspaceMountLabel] == "true",
labels[constants.DevWorkspaceGitCredentialLabel] == "true",
labels[constants.DevWorkspaceGitTLSLabel] == "true",
labels[constants.DevWorkspacePullSecretLabel] == "true":
return true
default:
return false
}

}
9 changes: 9 additions & 0 deletions pkg/constants/metadata.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,15 @@ const (
// If the git host is not defined then the certificate will be used for all http repositories.
DevWorkspaceGitTLSLabel = "controller.devfile.io/git-tls-credential"

// GitCredentialsConfigMapName is the name used for the configmap that stores the Git configuration for workspaces
// in a given namespace. It is used when e.g. adding Git credentials via secret
GitCredentialsConfigMapName = "devworkspace-gitconfig"

// GitCredentialsMergedSecretName is the name for the merged Git credentials secret that is mounted to workspaces
// when Git credentials are defined. This secret combines the values of any secrets labelled
// "controller.devfile.io/git-credential"
GitCredentialsMergedSecretName = "devworkspace-merged-git-credentials"

// DevWorkspaceMountPathAnnotation is the annotation key to store the mount path for the secret or configmap.
// If no mount path is provided, configmaps will be mounted at /etc/config/<configmap-name>, secrets will
// be mounted at /etc/secret/<secret-name>, and persistent volume claims will be mounted to /tmp/<claim-name>
Expand Down
4 changes: 2 additions & 2 deletions pkg/provision/automount/gitconfig.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ func getGitResources(api sync.ClusterAPI, namespace string) (credentialSecrets [

func cleanupGitConfig(api sync.ClusterAPI, namespace string) error {
secretNN := types.NamespacedName{
Name: gitCredentialsSecretName,
Name: constants.GitCredentialsMergedSecretName,
Namespace: namespace,
}
tlsSecret := &corev1.Secret{}
Expand All@@ -134,7 +134,7 @@ func cleanupGitConfig(api sync.ClusterAPI, namespace string) error {
}

configmapNN := types.NamespacedName{
Name: gitCredentialsConfigMapName,
Name: constants.GitCredentialsConfigMapName,
Namespace: namespace,
}
credentialsConfigMap := &corev1.ConfigMap{}
Expand Down
6 changes: 2 additions & 4 deletions pkg/provision/automount/templates.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,10 +28,8 @@ const gitTLSCertificateKey = "certificate"

const gitConfigName = "gitconfig"
const gitConfigLocation = "/etc/" + gitConfigName
const gitCredentialsConfigMapName = "devworkspace-gitconfig"

const gitCredentialsSecretKey = "credentials"
const gitCredentialsSecretName = "devworkspace-merged-git-credentials"

// gitLFSConfig is the default configuration that gets provisioned when git-lfs
// is installed. It needs to be included in the overridden gitconfig to avoid
Expand DownExpand Up@@ -95,7 +93,7 @@ func constructGitConfig(namespace, credentialMountPath string, certificatesConfi

gitConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: gitCredentialsConfigMapName,
Name: constants.GitCredentialsConfigMapName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/defaultName": "git-config-secret",
Expand All@@ -122,7 +120,7 @@ func mergeGitCredentials(namespace string, credentialSecrets []corev1.Secret) (*
}
mergedCredentials := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: gitCredentialsSecretName,
Name: constants.GitCredentialsMergedSecretName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/defaultName": "git-config-secret",
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 6 additions & 65 deletions controllers/workspace/devworkspace_controller.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -700,69 +700,6 @@ func (r *DevWorkspaceReconciler) getWorkspaceId(ctx context.Context, workspace *
}
}

// Mapping the pod to the devworkspace
func dwRelatedPodsHandler(obj client.Object) []reconcile.Request {
labels := obj.GetLabels()
if _, ok := labels[constants.DevWorkspaceNameLabel]; !ok {
return []reconcile.Request{}
}

//If the dewworkspace label does not exist, do no reconcile
if _, ok := labels[constants.DevWorkspaceIDLabel]; !ok {
return []reconcile.Request{}
}

return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: labels[constants.DevWorkspaceNameLabel],
Namespace: obj.GetNamespace(),
},
},
}
}

func (r *DevWorkspaceReconciler) dwPVCHandler(obj client.Object) []reconcile.Request {
// Check if PVC is owned by a DevWorkspace (per-workspace storage case)
for _, ownerref := range obj.GetOwnerReferences() {
if ownerref.Kind != "DevWorkspace" {
continue
}
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: ownerref.Name,
Namespace: obj.GetNamespace(),
},
},
}
}

// TODO: Label PVCs used for workspace storage so that they can be cleaned up if non-default name is used.
// Otherwise, check if common PVC is deleted to make sure all DevWorkspaces see it happen
if obj.GetName() != wkspConfig.GetGlobalConfig().Workspace.PVCName || obj.GetDeletionTimestamp() == nil {
// We're looking for a deleted common PVC
return []reconcile.Request{}
}
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
storageType := workspace.Spec.Template.Attributes.GetString(constants.DevWorkspaceStorageTypeAttribute, nil)
if storageType == constants.CommonStorageClassType || storageType == constants.PerUserStorageClassType || storageType == "" {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}

func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
setupHttpClients()

Expand All@@ -775,7 +712,8 @@ func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
return []reconcile.Request{}
}

var configWatcher builder.WatchesOption = builder.WithPredicates(wkspConfig.Predicates())
configWatcher := builder.WithPredicates(wkspConfig.Predicates())
automountWatcher := builder.WithPredicates(automountPredicates)

// TODO: Set up indexing https://book.kubebuilder.io/cronjob-tutorial/controller-implementation.html#setup
return ctrl.NewControllerManagedBy(mgr).
Expand All@@ -793,8 +731,11 @@ func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
Owns(&corev1.ServiceAccount{}).
Watches(&source.Kind{Type: &corev1.Pod{}}, handler.EnqueueRequestsFromMapFunc(dwRelatedPodsHandler)).
Watches(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, handler.EnqueueRequestsFromMapFunc(r.dwPVCHandler)).
Watches(&source.Kind{Type: &corev1.Secret{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &corev1.ConfigMap{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &controllerv1alpha1.DevWorkspaceOperatorConfig{}}, handler.EnqueueRequestsFromMapFunc(emptyMapper), configWatcher).
WithEventFilter(predicates).
WithEventFilter(devworkspacePredicates).
WithEventFilter(podPredicates).
Complete(r)
}
44 changes: 43 additions & 1 deletion controllers/workspace/devworkspace_controller_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -353,7 +353,6 @@ var _ = Describe("DevWorkspace Controller", func() {
},
},
})
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
})

AfterEach(func() {
Expand All@@ -362,6 +361,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Mounts image pull secrets to the DevWorkspace Deployment", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -394,6 +394,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Manages git credentials for DevWorkspace", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -456,6 +457,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Automounts secrets and configmaps volumes", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -518,6 +520,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Automounts secrets and configmaps env vars", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -558,6 +561,45 @@ var _ = Describe("DevWorkspace Controller", func() {
Expect(container.EnvFrom).Should(ContainElements(expectedEnvFromSources), "Automounted env sources should be added to containers")
}
})

It("Detects changes to automount resources and reconciles", func() {
// NOTE: timeout for this test is reduced, as eventually DWO will reconcile the workspace by coincidence and notice
// the automount secret.
createStartedDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

mergedSecretNN := namespacedName(constants.GitCredentialsMergedSecretName, testNamespace)
mergedSecret := &corev1.Secret{}
Expect(k8sClient.Get(ctx, mergedSecretNN, mergedSecret)).Error()

By("Creating git-credential secret")
secret := generateSecret("git-credential-secret", corev1.SecretTypeOpaque)
secret.Labels[constants.DevWorkspaceGitCredentialLabel] = "true"
secret.Data["credentials"] = []byte("https://test:token@github.com")
createObject(secret)
defer deleteObject(secret)

By("Checking that merged credentials secret is created")
Eventually(func() error {
return k8sClient.Get(ctx, mergedSecretNN, mergedSecret)
}, 1*time.Second, interval).Should(Succeed(), "Merged credentials secret is created")

By("Checking that workspace deployment mounts merged credentials secret")
Eventually(func() error {
deploy := &appsv1.Deployment{}
deployNN := namespacedName(common.DeploymentName(workspaceID), testNamespace)
if err := k8sClient.Get(ctx, deployNN, deploy); err != nil {
return err
}
for _, volume := range deploy.Spec.Template.Spec.Volumes {
if volume.Secret != nil && volume.Secret.SecretName == constants.GitCredentialsMergedSecretName {
return nil
}
}
return fmt.Errorf("Secret not found in volumes")
}, 1*time.Second, interval).Should(Succeed(), "Merged credentials secret is added to deployment")
})
})

Context("Stopping DevWorkspaces", func() {
Expand Down
108 changes: 108 additions & 0 deletions controllers/workspace/eventhandlers.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
// Copyright (c) 2019-2023 Red Hat, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package controllers

import (
"context"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
wkspConfig "github.com/devfile/devworkspace-operator/pkg/config"
"github.com/devfile/devworkspace-operator/pkg/constants"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

// Mapping the pod to the devworkspace
func dwRelatedPodsHandler(obj client.Object) []reconcile.Request {
labels := obj.GetLabels()
if _, ok := labels[constants.DevWorkspaceNameLabel]; !ok {
return []reconcile.Request{}
}

//If the dewworkspace label does not exist, do no reconcile
if _, ok := labels[constants.DevWorkspaceIDLabel]; !ok {
return []reconcile.Request{}
}

return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: labels[constants.DevWorkspaceNameLabel],
Namespace: obj.GetNamespace(),
},
},
}
}

func (r *DevWorkspaceReconciler) dwPVCHandler(obj client.Object) []reconcile.Request {
// Check if PVC is owned by a DevWorkspace (per-workspace storage case)
for _, ownerref := range obj.GetOwnerReferences() {
if ownerref.Kind != "DevWorkspace" {
continue
}
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: ownerref.Name,
Namespace: obj.GetNamespace(),
},
},
}
}

// TODO: Label PVCs used for workspace storage so that they can be cleaned up if non-default name is used.
// Otherwise, check if common PVC is deleted to make sure all DevWorkspaces see it happen
if obj.GetName() != wkspConfig.GetGlobalConfig().Workspace.PVCName || obj.GetDeletionTimestamp() == nil {
// We're looking for a deleted common PVC
return []reconcile.Request{}
}
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
storageType := workspace.Spec.Template.Attributes.GetString(constants.DevWorkspaceStorageTypeAttribute, nil)
if storageType == constants.CommonStorageClassType || storageType == constants.PerUserStorageClassType || storageType == "" {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}

func (r *DevWorkspaceReconciler) runningWorkspacesHandler(obj client.Object) []reconcile.Request {
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
// Queue reconciles for any started workspaces to make sure they pick up new object
if workspace.Spec.Started {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}
32 changes: 30 additions & 2 deletions controllers/workspace/predicates.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,14 +21,15 @@ import (

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)

// predicates filters incoming events to avoid unnecessary reconciles to failed workspaces.
// devworkspacePredicates filters incoming events to avoid unnecessary reconciles to failed workspaces.
// If a workspace failed and its spec is changed, we trigger reconciles to allow for fixing
// issues in the workspace spec.
var predicates = predicate.Funcs{
var devworkspacePredicates = predicate.Funcs{
CreateFunc: func(_ event.CreateEvent) bool { return true },
DeleteFunc: func(_ event.DeleteEvent) bool { return true },
UpdateFunc: func(ev event.UpdateEvent) bool {
Expand DownExpand Up@@ -84,3 +85,30 @@ var podPredicates = predicate.Funcs{
return true
},
}

var automountPredicates = predicate.Funcs{
CreateFunc: func(ev event.CreateEvent) bool {
return objectIsAutomountResource(ev.Object)
},
DeleteFunc: func(ev event.DeleteEvent) bool {
return objectIsAutomountResource(ev.Object)
},
UpdateFunc: func(ev event.UpdateEvent) bool {
return objectIsAutomountResource(ev.ObjectNew)
},
GenericFunc: func(_ event.GenericEvent) bool { return false },
}

func objectIsAutomountResource(obj client.Object) bool {
labels := obj.GetLabels()
switch {
case labels[constants.DevWorkspaceMountLabel] == "true",
labels[constants.DevWorkspaceGitCredentialLabel] == "true",
labels[constants.DevWorkspaceGitTLSLabel] == "true",
labels[constants.DevWorkspacePullSecretLabel] == "true":
return true
default:
return false
}

}
9 changes: 9 additions & 0 deletions pkg/constants/metadata.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,15 @@ const (
// If the git host is not defined then the certificate will be used for all http repositories.
DevWorkspaceGitTLSLabel = "controller.devfile.io/git-tls-credential"

// GitCredentialsConfigMapName is the name used for the configmap that stores the Git configuration for workspaces
// in a given namespace. It is used when e.g. adding Git credentials via secret
GitCredentialsConfigMapName = "devworkspace-gitconfig"

// GitCredentialsMergedSecretName is the name for the merged Git credentials secret that is mounted to workspaces
// when Git credentials are defined. This secret combines the values of any secrets labelled
// "controller.devfile.io/git-credential"
GitCredentialsMergedSecretName = "devworkspace-merged-git-credentials"

// DevWorkspaceMountPathAnnotation is the annotation key to store the mount path for the secret or configmap.
// If no mount path is provided, configmaps will be mounted at /etc/config/<configmap-name>, secrets will
// be mounted at /etc/secret/<secret-name>, and persistent volume claims will be mounted to /tmp/<claim-name>
Expand Down
4 changes: 2 additions & 2 deletions pkg/provision/automount/gitconfig.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ func getGitResources(api sync.ClusterAPI, namespace string) (credentialSecrets [

func cleanupGitConfig(api sync.ClusterAPI, namespace string) error {
secretNN := types.NamespacedName{
Name: gitCredentialsSecretName,
Name: constants.GitCredentialsMergedSecretName,
Namespace: namespace,
}
tlsSecret := &corev1.Secret{}
Expand All@@ -134,7 +134,7 @@ func cleanupGitConfig(api sync.ClusterAPI, namespace string) error {
}

configmapNN := types.NamespacedName{
Name: gitCredentialsConfigMapName,
Name: constants.GitCredentialsConfigMapName,
Namespace: namespace,
}
credentialsConfigMap := &corev1.ConfigMap{}
Expand Down
6 changes: 2 additions & 4 deletions pkg/provision/automount/templates.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,10 +28,8 @@ const gitTLSCertificateKey = "certificate"

const gitConfigName = "gitconfig"
const gitConfigLocation = "/etc/" + gitConfigName
const gitCredentialsConfigMapName = "devworkspace-gitconfig"

const gitCredentialsSecretKey = "credentials"
const gitCredentialsSecretName = "devworkspace-merged-git-credentials"

// gitLFSConfig is the default configuration that gets provisioned when git-lfs
// is installed. It needs to be included in the overridden gitconfig to avoid
Expand DownExpand Up@@ -95,7 +93,7 @@ func constructGitConfig(namespace, credentialMountPath string, certificatesConfi

gitConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: gitCredentialsConfigMapName,
Name: constants.GitCredentialsConfigMapName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/defaultName": "git-config-secret",
Expand All@@ -122,7 +120,7 @@ func mergeGitCredentials(namespace string, credentialSecrets []corev1.Secret) (*
}
mergedCredentials := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: gitCredentialsSecretName,
Name: constants.GitCredentialsMergedSecretName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/defaultName": "git-config-secret",
Expand Down
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 6 additions & 65 deletions controllers/workspace/devworkspace_controller.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -700,69 +700,6 @@ func (r *DevWorkspaceReconciler) getWorkspaceId(ctx context.Context, workspace *
}
}

// Mapping the pod to the devworkspace
func dwRelatedPodsHandler(obj client.Object) []reconcile.Request {
labels := obj.GetLabels()
if _, ok := labels[constants.DevWorkspaceNameLabel]; !ok {
return []reconcile.Request{}
}

//If the dewworkspace label does not exist, do no reconcile
if _, ok := labels[constants.DevWorkspaceIDLabel]; !ok {
return []reconcile.Request{}
}

return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: labels[constants.DevWorkspaceNameLabel],
Namespace: obj.GetNamespace(),
},
},
}
}

func (r *DevWorkspaceReconciler) dwPVCHandler(obj client.Object) []reconcile.Request {
// Check if PVC is owned by a DevWorkspace (per-workspace storage case)
for _, ownerref := range obj.GetOwnerReferences() {
if ownerref.Kind != "DevWorkspace" {
continue
}
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: ownerref.Name,
Namespace: obj.GetNamespace(),
},
},
}
}

// TODO: Label PVCs used for workspace storage so that they can be cleaned up if non-default name is used.
// Otherwise, check if common PVC is deleted to make sure all DevWorkspaces see it happen
if obj.GetName() != wkspConfig.GetGlobalConfig().Workspace.PVCName || obj.GetDeletionTimestamp() == nil {
// We're looking for a deleted common PVC
return []reconcile.Request{}
}
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
storageType := workspace.Spec.Template.Attributes.GetString(constants.DevWorkspaceStorageTypeAttribute, nil)
if storageType == constants.CommonStorageClassType || storageType == constants.PerUserStorageClassType || storageType == "" {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}

func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
setupHttpClients()

Expand All@@ -775,7 +712,8 @@ func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
return []reconcile.Request{}
}

var configWatcher builder.WatchesOption = builder.WithPredicates(wkspConfig.Predicates())
configWatcher := builder.WithPredicates(wkspConfig.Predicates())
automountWatcher := builder.WithPredicates(automountPredicates)

// TODO: Set up indexing https://book.kubebuilder.io/cronjob-tutorial/controller-implementation.html#setup
return ctrl.NewControllerManagedBy(mgr).
Expand All@@ -793,8 +731,11 @@ func (r *DevWorkspaceReconciler) SetupWithManager(mgr ctrl.Manager) error {
Owns(&corev1.ServiceAccount{}).
Watches(&source.Kind{Type: &corev1.Pod{}}, handler.EnqueueRequestsFromMapFunc(dwRelatedPodsHandler)).
Watches(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, handler.EnqueueRequestsFromMapFunc(r.dwPVCHandler)).
Watches(&source.Kind{Type: &corev1.Secret{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &corev1.ConfigMap{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &corev1.PersistentVolumeClaim{}}, handler.EnqueueRequestsFromMapFunc(r.runningWorkspacesHandler), automountWatcher).
Watches(&source.Kind{Type: &controllerv1alpha1.DevWorkspaceOperatorConfig{}}, handler.EnqueueRequestsFromMapFunc(emptyMapper), configWatcher).
WithEventFilter(predicates).
WithEventFilter(devworkspacePredicates).
WithEventFilter(podPredicates).
Complete(r)
}
44 changes: 43 additions & 1 deletion controllers/workspace/devworkspace_controller_test.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -353,7 +353,6 @@ var _ = Describe("DevWorkspace Controller", func() {
},
},
})
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
})

AfterEach(func() {
Expand All@@ -362,6 +361,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Mounts image pull secrets to the DevWorkspace Deployment", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -394,6 +394,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Manages git credentials for DevWorkspace", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -456,6 +457,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Automounts secrets and configmaps volumes", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -518,6 +520,7 @@ var _ = Describe("DevWorkspace Controller", func() {
})

It("Automounts secrets and configmaps env vars", func() {
createDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

Expand DownExpand Up@@ -558,6 +561,45 @@ var _ = Describe("DevWorkspace Controller", func() {
Expect(container.EnvFrom).Should(ContainElements(expectedEnvFromSources), "Automounted env sources should be added to containers")
}
})

It("Detects changes to automount resources and reconciles", func() {
// NOTE: timeout for this test is reduced, as eventually DWO will reconcile the workspace by coincidence and notice
// the automount secret.
createStartedDevWorkspace(devWorkspaceName, "test-devworkspace.yaml")
devworkspace := getExistingDevWorkspace(devWorkspaceName)
workspaceID := devworkspace.Status.DevWorkspaceId

mergedSecretNN := namespacedName(constants.GitCredentialsMergedSecretName, testNamespace)
mergedSecret := &corev1.Secret{}
Expect(k8sClient.Get(ctx, mergedSecretNN, mergedSecret)).Error()

By("Creating git-credential secret")
secret := generateSecret("git-credential-secret", corev1.SecretTypeOpaque)
secret.Labels[constants.DevWorkspaceGitCredentialLabel] = "true"
secret.Data["credentials"] = []byte("https://test:token@github.com")
createObject(secret)
defer deleteObject(secret)

By("Checking that merged credentials secret is created")
Eventually(func() error {
return k8sClient.Get(ctx, mergedSecretNN, mergedSecret)
}, 1*time.Second, interval).Should(Succeed(), "Merged credentials secret is created")

By("Checking that workspace deployment mounts merged credentials secret")
Eventually(func() error {
deploy := &appsv1.Deployment{}
deployNN := namespacedName(common.DeploymentName(workspaceID), testNamespace)
if err := k8sClient.Get(ctx, deployNN, deploy); err != nil {
return err
}
for _, volume := range deploy.Spec.Template.Spec.Volumes {
if volume.Secret != nil && volume.Secret.SecretName == constants.GitCredentialsMergedSecretName {
return nil
}
}
return fmt.Errorf("Secret not found in volumes")
}, 1*time.Second, interval).Should(Succeed(), "Merged credentials secret is added to deployment")
})
})

Context("Stopping DevWorkspaces", func() {
Expand Down
108 changes: 108 additions & 0 deletions controllers/workspace/eventhandlers.go
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
// Copyright (c) 2019-2023 Red Hat, Inc.
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package controllers

import (
"context"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
wkspConfig "github.com/devfile/devworkspace-operator/pkg/config"
"github.com/devfile/devworkspace-operator/pkg/constants"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)

// Mapping the pod to the devworkspace
func dwRelatedPodsHandler(obj client.Object) []reconcile.Request {
labels := obj.GetLabels()
if _, ok := labels[constants.DevWorkspaceNameLabel]; !ok {
return []reconcile.Request{}
}

//If the dewworkspace label does not exist, do no reconcile
if _, ok := labels[constants.DevWorkspaceIDLabel]; !ok {
return []reconcile.Request{}
}

return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: labels[constants.DevWorkspaceNameLabel],
Namespace: obj.GetNamespace(),
},
},
}
}

func (r *DevWorkspaceReconciler) dwPVCHandler(obj client.Object) []reconcile.Request {
// Check if PVC is owned by a DevWorkspace (per-workspace storage case)
for _, ownerref := range obj.GetOwnerReferences() {
if ownerref.Kind != "DevWorkspace" {
continue
}
return []reconcile.Request{
{
NamespacedName: types.NamespacedName{
Name: ownerref.Name,
Namespace: obj.GetNamespace(),
},
},
}
}

// TODO: Label PVCs used for workspace storage so that they can be cleaned up if non-default name is used.
// Otherwise, check if common PVC is deleted to make sure all DevWorkspaces see it happen
if obj.GetName() != wkspConfig.GetGlobalConfig().Workspace.PVCName || obj.GetDeletionTimestamp() == nil {
// We're looking for a deleted common PVC
return []reconcile.Request{}
}
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
storageType := workspace.Spec.Template.Attributes.GetString(constants.DevWorkspaceStorageTypeAttribute, nil)
if storageType == constants.CommonStorageClassType || storageType == constants.PerUserStorageClassType || storageType == "" {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}

func (r *DevWorkspaceReconciler) runningWorkspacesHandler(obj client.Object) []reconcile.Request {
dwList := &dw.DevWorkspaceList{}
if err := r.Client.List(context.Background(), dwList, &client.ListOptions{Namespace: obj.GetNamespace()}); err != nil {
return []reconcile.Request{}
}
var reconciles []reconcile.Request
for _, workspace := range dwList.Items {
// Queue reconciles for any started workspaces to make sure they pick up new object
if workspace.Spec.Started {
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
}
return reconciles
}
32 changes: 30 additions & 2 deletions controllers/workspace/predicates.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,14 +21,15 @@ import (

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
"sigs.k8s.io/controller-runtime/pkg/predicate"
)

// predicates filters incoming events to avoid unnecessary reconciles to failed workspaces.
// devworkspacePredicates filters incoming events to avoid unnecessary reconciles to failed workspaces.
// If a workspace failed and its spec is changed, we trigger reconciles to allow for fixing
// issues in the workspace spec.
var predicates = predicate.Funcs{
var devworkspacePredicates = predicate.Funcs{
CreateFunc: func(_ event.CreateEvent) bool { return true },
DeleteFunc: func(_ event.DeleteEvent) bool { return true },
UpdateFunc: func(ev event.UpdateEvent) bool {
Expand DownExpand Up@@ -84,3 +85,30 @@ var podPredicates = predicate.Funcs{
return true
},
}

var automountPredicates = predicate.Funcs{
CreateFunc: func(ev event.CreateEvent) bool {
return objectIsAutomountResource(ev.Object)
},
DeleteFunc: func(ev event.DeleteEvent) bool {
return objectIsAutomountResource(ev.Object)
},
UpdateFunc: func(ev event.UpdateEvent) bool {
return objectIsAutomountResource(ev.ObjectNew)
},
GenericFunc: func(_ event.GenericEvent) bool { return false },
}

func objectIsAutomountResource(obj client.Object) bool {
labels := obj.GetLabels()
switch {
case labels[constants.DevWorkspaceMountLabel] == "true",
labels[constants.DevWorkspaceGitCredentialLabel] == "true",
labels[constants.DevWorkspaceGitTLSLabel] == "true",
labels[constants.DevWorkspacePullSecretLabel] == "true":
return true
default:
return false
}

}
9 changes: 9 additions & 0 deletions pkg/constants/metadata.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -58,6 +58,15 @@ const (
// If the git host is not defined then the certificate will be used for all http repositories.
DevWorkspaceGitTLSLabel = "controller.devfile.io/git-tls-credential"

// GitCredentialsConfigMapName is the name used for the configmap that stores the Git configuration for workspaces
// in a given namespace. It is used when e.g. adding Git credentials via secret
GitCredentialsConfigMapName = "devworkspace-gitconfig"

// GitCredentialsMergedSecretName is the name for the merged Git credentials secret that is mounted to workspaces
// when Git credentials are defined. This secret combines the values of any secrets labelled
// "controller.devfile.io/git-credential"
GitCredentialsMergedSecretName = "devworkspace-merged-git-credentials"

// DevWorkspaceMountPathAnnotation is the annotation key to store the mount path for the secret or configmap.
// If no mount path is provided, configmaps will be mounted at /etc/config/<configmap-name>, secrets will
// be mounted at /etc/secret/<secret-name>, and persistent volume claims will be mounted to /tmp/<claim-name>
Expand Down
4 changes: 2 additions & 2 deletions pkg/provision/automount/gitconfig.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -116,7 +116,7 @@ func getGitResources(api sync.ClusterAPI, namespace string) (credentialSecrets [

func cleanupGitConfig(api sync.ClusterAPI, namespace string) error {
secretNN := types.NamespacedName{
Name: gitCredentialsSecretName,
Name: constants.GitCredentialsMergedSecretName,
Namespace: namespace,
}
tlsSecret := &corev1.Secret{}
Expand All@@ -134,7 +134,7 @@ func cleanupGitConfig(api sync.ClusterAPI, namespace string) error {
}

configmapNN := types.NamespacedName{
Name: gitCredentialsConfigMapName,
Name: constants.GitCredentialsConfigMapName,
Namespace: namespace,
}
credentialsConfigMap := &corev1.ConfigMap{}
Expand Down
6 changes: 2 additions & 4 deletions pkg/provision/automount/templates.go
Original file line numberDiff line numberDiff line change
Expand Up@@ -28,10 +28,8 @@ const gitTLSCertificateKey = "certificate"

const gitConfigName = "gitconfig"
const gitConfigLocation = "/etc/" + gitConfigName
const gitCredentialsConfigMapName = "devworkspace-gitconfig"

const gitCredentialsSecretKey = "credentials"
const gitCredentialsSecretName = "devworkspace-merged-git-credentials"

// gitLFSConfig is the default configuration that gets provisioned when git-lfs
// is installed. It needs to be included in the overridden gitconfig to avoid
Expand DownExpand Up@@ -95,7 +93,7 @@ func constructGitConfig(namespace, credentialMountPath string, certificatesConfi

gitConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: gitCredentialsConfigMapName,
Name: constants.GitCredentialsConfigMapName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/defaultName": "git-config-secret",
Expand All@@ -122,7 +120,7 @@ func mergeGitCredentials(namespace string, credentialSecrets []corev1.Secret) (*
}
mergedCredentials := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: gitCredentialsSecretName,
Name: constants.GitCredentialsMergedSecretName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/defaultName": "git-config-secret",
Expand Down