Skip to content

Commit

Permalink
Apply ssh askpass flow for the workspace container
Browse files Browse the repository at this point in the history
Signed-off-by: ivinokur <ivinokur@redhat.com>

Fixes #1295
  • Loading branch information
vinokurig committed Sep 9, 2024
1 parent 58cd8f0 commit 8e7de8c
Show file tree
Hide file tree
Showing 9 changed files with 189 additions and 7 deletions.
13 changes: 13 additions & 0 deletions controllers/workspace/devworkspace_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import (
"strings"
"time"

"github.com/devfile/devworkspace-operator/pkg/library/ssh"

devfilevalidation "github.com/devfile/api/v2/pkg/validation"
controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
"github.com/devfile/devworkspace-operator/controllers/workspace/metrics"
Expand Down Expand Up @@ -278,6 +280,12 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request
reconcileStatus.addWarning(flatten.FormatVariablesWarning(warnings))
}
workspace.Spec.Template = *flattenedWorkspace

err = ssh.AddSshAgentPostStartEvent(&workspace.Spec.Template)
if err != nil {
return r.failWorkspace(workspace, "Failed to add ssh-agent post start event", metrics.ReasonWorkspaceEngineFailure, reqLogger, &reconcileStatus), nil
}

reconcileStatus.setConditionTrue(conditions.DevWorkspaceResolved, "Resolved plugins and parents from DevWorkspace")

// Verify that the devworkspace components are valid after flattening
Expand Down Expand Up @@ -352,6 +360,11 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request
return r.failWorkspace(workspace, fmt.Sprintf("Failed to mount ServiceAccount tokens to workspace: %s", err), metrics.ReasonBadRequest, reqLogger, &reconcileStatus), nil
}

// Add SSH ask-pass script into devfile containers
if err := wsprovision.ProvisionSshAskPass(clusterAPI, workspace.Namespace, devfilePodAdditions); err != nil {
return r.failWorkspace(workspace, fmt.Sprintf("Failed to mount SSH askpass script to workspace: %s", err), metrics.ReasonWorkspaceEngineFailure, reqLogger, &reconcileStatus), nil
}

// Add automount resources into devfile containers
err = automount.ProvisionAutoMountResourcesInto(devfilePodAdditions, clusterAPI, workspace.Namespace, home.PersistUserHomeEnabled(workspace))
if shouldReturn, reconcileResult, reconcileErr := r.checkDWError(workspace, err, "Failed to process automount resources", metrics.ReasonBadRequest, reqLogger, &reconcileStatus); shouldReturn {
Expand Down
2 changes: 2 additions & 0 deletions pkg/constants/constants.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ const (

HomeInitEventId = "init-persistent-home"

SshAgentStartEventId = "init-ssh-agent"

ServiceAccount = "devworkspace"

PVCStorageSize = "10Gi"
Expand Down
2 changes: 2 additions & 0 deletions pkg/constants/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,6 @@ const (
// DevWorkspaceComponentName contains env var name which indicates from which devfile container component
// the container is created from. Note the flattened devfile is used to evaluate it.
DevWorkspaceComponentName = "DEVWORKSPACE_COMPONENT_NAME"
DISPLAY = "DISPLAY"
SSHAskPass = "SSH_ASKPASS"
)
2 changes: 2 additions & 0 deletions pkg/constants/metadata.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ const (
// in a given namespace. It is used when e.g. adding Git credentials via secret
GitCredentialsConfigMapName = "devworkspace-gitconfig"

SshAskPassConfigMapName = "devworkspace-ssh-askpass"

// 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"
Expand Down
20 changes: 18 additions & 2 deletions pkg/library/env/workspaceenv.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ import (
"fmt"
"os"

"github.com/devfile/devworkspace-operator/pkg/provision/workspace"

dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
devfileConstants "github.com/devfile/devworkspace-operator/pkg/library/constants"
Expand Down Expand Up @@ -82,12 +84,26 @@ func commonEnvironmentVariables(workspaceWithConfig *common.DevWorkspaceWithConf
},
}

envvars = append(envvars, GetProxyEnvVars(workspaceWithConfig.Config.Routing.ProxyConfig)...)
envvars = append(envvars, getProxyEnvVars(workspaceWithConfig.Config.Routing.ProxyConfig)...)
envvars = append(envvars, getSshAskPassEnvVars()...)

return envvars
}

func GetProxyEnvVars(proxyConfig *v1alpha1.Proxy) []corev1.EnvVar {
func getSshAskPassEnvVars() []corev1.EnvVar {
return []corev1.EnvVar{
{
Name: constants.SSHAskPass,
Value: fmt.Sprintf("%s%s", workspace.SshAskPassMountPath, workspace.SshAskPassScriptFileName),
},
{
Name: constants.DISPLAY,
Value: ":0",
},
}
}

func getProxyEnvVars(proxyConfig *v1alpha1.Proxy) []corev1.EnvVar {
if proxyConfig == nil {
return nil
}
Expand Down
57 changes: 57 additions & 0 deletions pkg/library/ssh/event.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
// Copyright (c) 2019-2024 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 ssh

import (
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
"github.com/devfile/devworkspace-operator/pkg/constants"
"github.com/devfile/devworkspace-operator/pkg/library/lifecycle"
)

const commandLine = `SSH_ENV_PATH=$HOME/ssh-environment \
&& if [ -f /etc/ssh/passphrase ] && command -v ssh-add >/dev/null; \
then ssh-agent | sed 's/^echo/#echo/' > $SSH_ENV_PATH \
&& chmod 600 $SSH_ENV_PATH && source $SSH_ENV_PATH \
&& ssh-add /etc/ssh/dwo_ssh_key < /etc/ssh/passphrase \
&& if [ -f $HOME/.bashrc ] && [ -w $HOME/.bashrc ]; then echo "source ${SSH_ENV_PATH}" >> $HOME/.bashrc; fi; fi`

// AddSshAgentPostStartEvent Start ssh-agent and add the default ssh key to it, if the ssh key has a passphrase.
// Initialise the ssh-agent session env variables in the user .bashrc file.
func AddSshAgentPostStartEvent(spec *v1alpha2.DevWorkspaceTemplateSpec) error {
if spec.Commands == nil {
spec.Commands = []v1alpha2.Command{}
}

if spec.Events == nil {
spec.Events = &v1alpha2.Events{}
}

_, mainComponents, err := lifecycle.GetInitContainers(spec.DevWorkspaceTemplateSpecContent)
for _, component := range mainComponents {
if component.Container == nil {
continue
}
spec.Commands = append(spec.Commands, v1alpha2.Command{
Id: constants.SshAgentStartEventId,
CommandUnion: v1alpha2.CommandUnion{
Exec: &v1alpha2.ExecCommand{
CommandLine: commandLine,
Component: component.Name,
},
},
})
}
spec.Events.PostStart = append(spec.Events.PostStart, constants.SshAgentStartEventId)
return err
}
File renamed without changes.
94 changes: 94 additions & 0 deletions pkg/provision/workspace/ssh.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
//
// Copyright (c) 2019-2024 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 workspace

import (
_ "embed"
"path"

"github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
"github.com/devfile/devworkspace-operator/pkg/constants"
"github.com/devfile/devworkspace-operator/pkg/dwerrors"
"github.com/devfile/devworkspace-operator/pkg/provision/sync"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/utils/pointer"
)

const SshAskPassMountPath = "/.ssh-askpass/"
const SshAskPassScriptFileName = "ssh-askpass.sh"

//go:embed ssh-askpass.sh
var data string

func ProvisionSshAskPass(api sync.ClusterAPI, namespace string, podAdditions *v1alpha1.PodAdditions) error {
sshAskPassConfigMap := constructSshAskPassCM(namespace)
if _, err := sync.SyncObjectWithCluster(sshAskPassConfigMap, api); err != nil {
switch err.(type) {
case *sync.NotInSyncError: // Ignore the object created error
default:
return dwerrors.WrapSyncError(err)
}
}

sshAskPassVolumeMounts, sshAskPassVolumes, err := getSshAskPassVolumesAndVolumeMounts()
if err != nil {
return err
}
podAdditions.VolumeMounts = append(podAdditions.VolumeMounts, sshAskPassVolumeMounts...)
podAdditions.Volumes = append(podAdditions.Volumes, sshAskPassVolumes...)
return nil
}

func constructSshAskPassCM(namespace string) *corev1.ConfigMap {
askPassConfigMap := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: constants.SshAskPassConfigMapName,
Namespace: namespace,
Labels: map[string]string{
"app.kubernetes.io/defaultName": "ssh-askpass-secret",
"app.kubernetes.io/part-of": "devworkspace-operator",
"controller.devfile.io/watch-configmap": "true",
},
},
Data: map[string]string{
SshAskPassScriptFileName: data,
},
}
return askPassConfigMap
}

func getSshAskPassVolumesAndVolumeMounts() ([]corev1.VolumeMount, []corev1.Volume, error) {
name := "ssh-askpass"
volume := corev1.Volume{
Name: name,
VolumeSource: corev1.VolumeSource{
ConfigMap: &corev1.ConfigMapVolumeSource{
LocalObjectReference: corev1.LocalObjectReference{
Name: constants.SshAskPassConfigMapName,
},
DefaultMode: pointer.Int32(0755),
},
},
}
volumeMount := corev1.VolumeMount{
Name: name,
ReadOnly: true,
MountPath: path.Join(SshAskPassMountPath, SshAskPassScriptFileName),
SubPath: SshAskPassScriptFileName,
}
return []corev1.VolumeMount{volumeMount}, []corev1.Volume{volume}, nil
}
6 changes: 1 addition & 5 deletions project-clone/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,10 @@ COPY --from=builder /project-clone/_output/bin/project-clone /usr/local/bin/proj

ENV USER_UID=1001 \
USER_NAME=project-clone \
HOME=/home/user \
DISPLAY=":0" \
SSH_ASKPASS=/usr/local/bin/ssh-askpass.sh
HOME=/home/user

COPY build/bin /usr/local/bin
COPY project-clone/ssh-askpass.sh /usr/local/bin
RUN /usr/local/bin/user_setup
RUN chmod +x /usr/local/bin/ssh-askpass.sh

USER ${USER_UID}

Expand Down

0 comments on commit 8e7de8c

Please sign in to comment.