Skip to content
Open
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
2 changes: 1 addition & 1 deletion controllers/workspace/devworkspace_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -465,7 +465,7 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request
}
}

err = automount.ProvisionAutoMountResourcesInto(devfilePodAdditions, clusterAPI, workspace.Namespace, home.PersistUserHomeEnabled(workspace), workspaceDeployment)
err = automount.ProvisionAutoMountResourcesInto(devfilePodAdditions, clusterAPI, workspace.Namespace, workspace.Name, home.PersistUserHomeEnabled(workspace), workspaceDeployment)
if shouldReturn, reconcileResult, reconcileErr := r.checkDWError(workspace, err, "Failed to process automount resources", metrics.ReasonBadRequest, reqLogger, &reconcileStatus); shouldReturn {
return reconcileResult, reconcileErr
}
Expand Down
27 changes: 18 additions & 9 deletions controllers/workspace/eventhandlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
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"
"github.com/devfile/devworkspace-operator/pkg/provision/automount"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
Expand Down Expand Up @@ -118,22 +119,30 @@ func (r *DevWorkspaceReconciler) dwPVCHandler(ctx context.Context, obj client.Ob
return reconciles
}

func (r *DevWorkspaceReconciler) runningWorkspacesHandler(ctx context.Context, obj client.Object) []reconcile.Request {
func (r *DevWorkspaceReconciler) runningWorkspacesHandler(_ context.Context, 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 {
Comment thread
tolusha marked this conversation as resolved.
reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
// Queue reconciles ONLY for any started workspaces to make sure they pick up new object
if !workspace.Spec.Started {
continue
}

// Skip workspaces that don't match the resource's include/exclude annotations
// to avoid unnecessary reconciles in multi-workspace clusters.
if !automount.MatchesWorkspaceTarget(obj, workspace.GetName()) {
continue
}

reconciles = append(reconciles, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: workspace.GetName(),
Namespace: workspace.GetNamespace(),
},
})
}
return reconciles
}
66 changes: 65 additions & 1 deletion docs/additional-configuration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,58 @@ When "file" is used, the configmap is mounted as a directory within the workspac

* `controller.devfile.io/mount-on-start`: when set to `"true"`, the resource will only be mounted when a workspace starts. If the resource is created while a workspace is already running, it will not be automatically mounted until the workspace is restarted. This prevents unwanted workspace restarts caused by newly created automount resources. This annotation can be applied to configmaps, secrets, and persistent volume claims.
+
For git credential secrets (labelled with `controller.devfile.io/git-credential`) and git TLS configmaps (labelled with `controller.devfile.io/git-tls-credential`), the annotation is evaluated across all git credential resources as a group, not individually. Since all git credentials are merged into a single mounted secret, if at least one git credential secret (or TLS configmap) lacks the `controller.devfile.io/mount-on-start` annotation, all git credentials (or TLS configmaps) will be mounted, including those marked with `mount-on-start`.
For git credential secrets (labeled with `controller.devfile.io/git-credential`)
and git TLS configmaps (labelled with `controller.devfile.io/git-tls-credential`),
this annotation behave differently. The `controller.devfile.io/mount-on-start` annotation is evaluated across
all git credential resources as a group, not individually. Since all git credentials are
merged into a single mounted secret, if at least one git credential secret (or TLS configmap)
lacks the `controller.devfile.io/mount-on-start` annotation, all git credentials
(or TLS configmaps) will be mounted, including those marked with `controller.devfile.io/mount-on-start`.

* `controller.devfile.io/mount-to-devworkspace-include`: configure which DevWorkspaces the
resource should be mounted to. The value is a comma-separated list of patterns matched against
the DevWorkspace name. The resource is only mounted to workspaces whose name matches at
least one pattern. This annotation can be applied to ConfigMaps, Secrets and Persistent Volume Claims.
+
* `controller.devfile.io/mount-to-devworkspace-exclude`: configure which DevWorkspaces the
resource should NOT be mounted to. The value is a comma-separated list of patterns
matched against the DevWorkspace name. The resource is not mounted to workspaces whose name
matches any pattern. This annotation can be applied to ConfigMaps, Secrets and Persistent Volume Claims.
+
Supported patterns for both annotations:
+
--
** `name` -- exact match
** `name*` -- prefix match
** `*name` -- suffix match
** `\*name*` -- contains match
** `*` -- matches all workspaces
--
+
Both annotations can be used at the same time on the same resource. When both are present, the resource is mounted only to workspaces that match the include pattern AND do not match the exclude pattern.
If neither annotation is present, the resource is mounted to all workspaces (default behavior).
+
[source,yaml]
----
metadata:
annotations:
controller.devfile.io/mount-to-devworkspace-include: "my-workspace, staging-*"
----
+
[source,yaml]
----
metadata:
annotations:
controller.devfile.io/mount-to-devworkspace-exclude: "temp-*, *-test"
----
+
[source,yaml]
----
metadata:
annotations:
controller.devfile.io/mount-to-devworkspace-include: "staging-*"
controller.devfile.io/mount-to-devworkspace-exclude: "staging-legacy"
----
+
[source,yaml]
----
Expand Down Expand Up @@ -201,6 +252,19 @@ data:

This will mount a file `/tmp/.git-credentials/credentials` in all workspace containers, and construct a git config to use this file as a credentials store.

### Using a base gitconfig
If an automount configmap or secret (labeled with `controller.devfile.io/automount: true`)
is mounted as `subpath` and contains a key that resolves to `/etc/gitconfig`,
its contents are used as the base git configuration for the workspace.
This base gitconfig is merged with the generated git credentials configuration.
Comment thread
tolusha marked this conversation as resolved.

For example, a configmap with `controller.devfile.io/mount-path: /etc` and a key named
`gitconfig` will be detected and used as the base git configuration.

The `controller.devfile.io/mount-to-devworkspace-include` and
`controller.devfile.io/mount-to-devworkspace-exclude` annotations are respected,
allowing the base gitconfig to be targeted to specific workspaces.
Comment thread
tolusha marked this conversation as resolved.

## Configuring DevWorkspaces to use SSH keys for Git operations
Git SSH keys can be configured for DevWorkspaces by mounting secrets to workspaces.

Expand Down
22 changes: 22 additions & 0 deletions pkg/constants/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,28 @@ const (
// DevWorkspaceMountLabel is the label key to store if a configmap, secret, or PVC should be mounted to the devworkspace
DevWorkspaceMountLabel = "controller.devfile.io/mount-to-devworkspace"

// DevWorkspaceMountIncludeAnnotation is an annotation to configure which DevWorkspaces an automount
// resource should be mounted to. The value is a comma-separated list of patterns matched against
// the DevWorkspace name. The resource is only mounted to workspaces whose name matches at least one pattern.
// Supported patterns:
// - exact match `name`
// - prefix match `name*`
// - suffix match `*name`
// - contains match `*name*`
Comment thread
tolusha marked this conversation as resolved.
// - matches all workspaces `*`
DevWorkspaceMountIncludeAnnotation = "controller.devfile.io/mount-to-devworkspace-include"

// DevWorkspaceMountExcludeAnnotation is an annotation to configure which DevWorkspaces an automount
// resource should NOT be mounted to. The value is a comma-separated list of patterns matched against
// the DevWorkspace name. The resource is not mounted to workspaces whose name matches any pattern.
// Supported patterns:
// - exact match `name`
// - prefix match `name*`
// - suffix match `*name`
// - contains match `*name*`
// - matches all workspaces `*`
DevWorkspaceMountExcludeAnnotation = "controller.devfile.io/mount-to-devworkspace-exclude"

// 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
101 changes: 87 additions & 14 deletions pkg/provision/automount/common.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,12 @@ type Resources struct {
func ProvisionAutoMountResourcesInto(
podAdditions *v1alpha1.PodAdditions,
api sync.ClusterAPI,
namespace string,
workspaceNamespace string,
workspaceName string,
persistentHome bool,
workspaceDeployment *appsv1.Deployment,
) error {
resources, err := getAutomountResources(api, namespace, workspaceDeployment)
resources, err := getAutomountResources(api, workspaceNamespace, workspaceName, workspaceDeployment)

if err != nil {
return err
Expand Down Expand Up @@ -85,20 +86,21 @@ func ProvisionAutoMountResourcesInto(

func getAutomountResources(
api sync.ClusterAPI,
namespace string,
workspaceNamespace string,
workspaceName string,
workspaceDeployment *appsv1.Deployment,
) (*Resources, error) {
gitCMAutoMountResources, err := ProvisionGitConfiguration(api, namespace, workspaceDeployment)
gitCMAutoMountResources, err := ProvisionGitConfiguration(api, workspaceNamespace, workspaceName, workspaceDeployment)
if err != nil {
return nil, err
}

cmAutoMountResources, err := getDevWorkspaceConfigmaps(namespace, api, workspaceDeployment)
cmAutoMountResources, err := getDevWorkspaceConfigmaps(workspaceNamespace, workspaceName, api, workspaceDeployment)
if err != nil {
return nil, err
}

secretAutoMountResources, err := getDevWorkspaceSecrets(namespace, api, workspaceDeployment)
secretAutoMountResources, err := getDevWorkspaceSecrets(workspaceNamespace, workspaceName, api, workspaceDeployment)
if err != nil {
return nil, err
}
Expand All @@ -115,7 +117,7 @@ func getAutomountResources(
}
dropItemsFieldFromVolumes(mergedResources.Volumes)

pvcAutoMountResources, err := getAutoMountPVCs(namespace, api, workspaceDeployment)
pvcAutoMountResources, err := getAutoMountPVCs(workspaceNamespace, workspaceName, api, workspaceDeployment)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -214,12 +216,17 @@ func flattenAutomountResources(resources []Resources) Resources {
return flattened
}

// findGitconfigAutomount searches a namespace for a automount resource (configmap or secret) that contains
// findGitconfigAutomount searches a namespace for an automount resource (configmap or secret) that contains
// a system-wide gitconfig (i.e. the mountpath is `/etc/gitconfig`). Only objects with mount type "subpath"
// are considered. If a suitable object is found, the contents of the gitconfig defined there is returned.
func findGitconfigAutomount(api sync.ClusterAPI, namespace string) (gitconfig *string, err error) {
// are considered. Resources that do not match the workspace's include/exclude annotations are skipped.
// If a suitable object is found, the contents of the gitconfig defined there is returned.
func findGitconfigAutomount(
api sync.ClusterAPI,
workspaceNamespace string,
workspaceName string,
) (gitconfig *string, err error) {
configmapList := &corev1.ConfigMapList{}
if err := api.Client.List(api.Ctx, configmapList, k8sclient.InNamespace(namespace), k8sclient.MatchingLabels{
if err := api.Client.List(api.Ctx, configmapList, k8sclient.InNamespace(workspaceNamespace), k8sclient.MatchingLabels{
constants.DevWorkspaceMountLabel: "true",
}); err != nil {
return nil, err
Expand All @@ -228,6 +235,13 @@ func findGitconfigAutomount(api sync.ClusterAPI, namespace string) (gitconfig *s
if cm.Annotations[constants.DevWorkspaceMountAsAnnotation] != constants.DevWorkspaceMountAsSubpath {
continue
}

// Filter resources by workspace name
if !MatchesWorkspaceTarget(&cm, workspaceName) {
log.V(1).Info("Skipping ConfigMap, workspace does not match include/exclude annotations", "namespace", cm.Namespace, "name", cm.Name, "workspace", workspaceName)
continue
}

mountPath := cm.Annotations[constants.DevWorkspaceMountPathAnnotation]
for key, value := range cm.Data {
if path.Join(mountPath, key) == "/etc/gitconfig" {
Expand All @@ -240,7 +254,7 @@ func findGitconfigAutomount(api sync.ClusterAPI, namespace string) (gitconfig *s
}

secretList := &corev1.SecretList{}
if err := api.Client.List(api.Ctx, secretList, k8sclient.InNamespace(namespace), k8sclient.MatchingLabels{
if err := api.Client.List(api.Ctx, secretList, k8sclient.InNamespace(workspaceNamespace), k8sclient.MatchingLabels{
constants.DevWorkspaceMountLabel: "true",
}); err != nil {
return nil, err
Expand All @@ -249,6 +263,13 @@ func findGitconfigAutomount(api sync.ClusterAPI, namespace string) (gitconfig *s
if secret.Annotations[constants.DevWorkspaceMountAsAnnotation] != constants.DevWorkspaceMountAsSubpath {
continue
}

// Filter resources by workspace name
if !MatchesWorkspaceTarget(&secret, workspaceName) {
log.V(1).Info("Skipping Secret, workspace does not match include/exclude annotations", "namespace", secret.Namespace, "name", secret.Name, "workspace", workspaceName)
continue
}

mountPath := secret.Annotations[constants.DevWorkspaceMountPathAnnotation]
for key, value := range secret.Data {
if path.Join(mountPath, key) == "/etc/gitconfig" {
Expand Down Expand Up @@ -370,10 +391,10 @@ func isMountOnStart(obj k8sclient.Object) bool {
return obj.GetAnnotations()[constants.MountOnStartAttribute] == "true"
}

// isAllowedToMount checks whether an automount resource can be added to the workspace pod.
// canMountWithoutRestart checks whether an automount resource can be added to the workspace pod.
// Resources marked with mount-on-start are only allowed when
// the workspace is not yet running or when they are already present in the current deployment.
func isAllowedToMount(
func canMountWithoutRestart(
obj k8sclient.Object,
automountResource Resources,
workspaceDeployment *appsv1.Deployment,
Expand Down Expand Up @@ -437,3 +458,55 @@ func isEnvFromSourceExistsInDeployment(automountResource Resources, workspaceDep

return false
}

// MatchesWorkspaceTarget checks whether a resource should be mounted to a given workspace
// based on include/exclude annotations. Both annotations can be used together: the resource
// is mounted when it matches the include pattern (or no include is set) and does not match the exclude pattern.
Comment thread
tolusha marked this conversation as resolved.
func MatchesWorkspaceTarget(
obj k8sclient.Object,
workspaceName string,
) bool {
annotations := obj.GetAnnotations()

includePatterns := strings.TrimSpace(annotations[constants.DevWorkspaceMountIncludeAnnotation])
excludePatterns := strings.TrimSpace(annotations[constants.DevWorkspaceMountExcludeAnnotation])

included := includePatterns == "" || matchesAnyPattern(includePatterns, workspaceName)
excluded := excludePatterns != "" && matchesAnyPattern(excludePatterns, workspaceName)

return included && !excluded
}

func matchesAnyPattern(patternsStr string, workspaceName string) bool {
patterns := strings.Split(patternsStr, ",")
for _, pattern := range patterns {
pattern = strings.TrimSpace(pattern)
if pattern == "" {
continue
}

if pattern == "*" {
return true
}

startsWithWildcard := strings.HasPrefix(pattern, "*")
endsWithWildcard := strings.HasSuffix(pattern, "*")

var matched bool
switch {
case startsWithWildcard && endsWithWildcard:
Comment thread
akurinnoy marked this conversation as resolved.
matched = strings.Contains(workspaceName, pattern[1:len(pattern)-1])
case endsWithWildcard:
matched = strings.HasPrefix(workspaceName, pattern[:len(pattern)-1])
case startsWithWildcard:
matched = strings.HasSuffix(workspaceName, pattern[1:])
default:
matched = workspaceName == pattern
}

if matched {
return true
}
}
return false
}
Comment thread
tolusha marked this conversation as resolved.
2 changes: 1 addition & 1 deletion pkg/provision/automount/common_persistenthome_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func TestProvisionAutomountResourcesIntoPersistentHomeEnabled(t *testing.T) {
Client: fake.NewClientBuilder().WithObjects(tt.Input.allObjects...).Build(),
}

err := ProvisionAutoMountResourcesInto(podAdditions, testAPI, testNamespace, true, nil)
err := ProvisionAutoMountResourcesInto(podAdditions, testAPI, testNamespace, "test-workspace", true, nil)

if !assert.NoError(t, err, "Unexpected error") {
return
Expand Down
Loading
Loading