Skip to content

Commit cc0c246

Browse files
vinokurigdkwon17
authored andcommitted
Apply ssh askpass flow for the workspace container
Signed-off-by: ivinokur <[email protected]> Fixes #1295
1 parent 6dadf49 commit cc0c246

File tree

9 files changed

+189
-7
lines changed

9 files changed

+189
-7
lines changed

controllers/workspace/devworkspace_controller.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ import (
2222
"strings"
2323
"time"
2424

25+
"github.com/devfile/devworkspace-operator/pkg/library/ssh"
26+
2527
devfilevalidation "github.com/devfile/api/v2/pkg/validation"
2628
controllerv1alpha1 "github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
2729
"github.com/devfile/devworkspace-operator/controllers/workspace/metrics"
@@ -278,6 +280,12 @@ func (r *DevWorkspaceReconciler) Reconcile(ctx context.Context, req ctrl.Request
278280
reconcileStatus.addWarning(flatten.FormatVariablesWarning(warnings))
279281
}
280282
workspace.Spec.Template = *flattenedWorkspace
283+
284+
err = ssh.AddSshAgentPostStartEvent(&workspace.Spec.Template)
285+
if err != nil {
286+
return r.failWorkspace(workspace, "Failed to add ssh-agent post start event", metrics.ReasonWorkspaceEngineFailure, reqLogger, &reconcileStatus), nil
287+
}
288+
281289
reconcileStatus.setConditionTrue(conditions.DevWorkspaceResolved, "Resolved plugins and parents from DevWorkspace")
282290

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

363+
// Add SSH ask-pass script into devfile containers
364+
if err := wsprovision.ProvisionSshAskPass(clusterAPI, workspace.Namespace, devfilePodAdditions); err != nil {
365+
return r.failWorkspace(workspace, fmt.Sprintf("Failed to mount SSH askpass script to workspace: %s", err), metrics.ReasonWorkspaceEngineFailure, reqLogger, &reconcileStatus), nil
366+
}
367+
355368
// Add automount resources into devfile containers
356369
err = automount.ProvisionAutoMountResourcesInto(devfilePodAdditions, clusterAPI, workspace.Namespace, home.PersistUserHomeEnabled(workspace))
357370
if shouldReturn, reconcileResult, reconcileErr := r.checkDWError(workspace, err, "Failed to process automount resources", metrics.ReasonBadRequest, reqLogger, &reconcileStatus); shouldReturn {

pkg/constants/constants.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ const (
4545

4646
HomeInitEventId = "init-persistent-home"
4747

48+
SshAgentStartEventId = "init-ssh-agent"
49+
4850
ServiceAccount = "devworkspace"
4951

5052
PVCStorageSize = "10Gi"

pkg/constants/env.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,6 @@ const (
3737
// DevWorkspaceComponentName contains env var name which indicates from which devfile container component
3838
// the container is created from. Note the flattened devfile is used to evaluate it.
3939
DevWorkspaceComponentName = "DEVWORKSPACE_COMPONENT_NAME"
40+
DISPLAY = "DISPLAY"
41+
SSHAskPass = "SSH_ASKPASS"
4042
)

pkg/constants/metadata.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ const (
8787
// in a given namespace. It is used when e.g. adding Git credentials via secret
8888
GitCredentialsConfigMapName = "devworkspace-gitconfig"
8989

90+
SshAskPassConfigMapName = "devworkspace-ssh-askpass"
91+
9092
// GitCredentialsMergedSecretName is the name for the merged Git credentials secret that is mounted to workspaces
9193
// when Git credentials are defined. This secret combines the values of any secrets labelled
9294
// "controller.devfile.io/git-credential"

pkg/library/env/workspaceenv.go

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import (
1919
"fmt"
2020
"os"
2121

22+
"github.com/devfile/devworkspace-operator/pkg/provision/workspace"
23+
2224
dw "github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
2325
"github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
2426
devfileConstants "github.com/devfile/devworkspace-operator/pkg/library/constants"
@@ -82,12 +84,26 @@ func commonEnvironmentVariables(workspaceWithConfig *common.DevWorkspaceWithConf
8284
},
8385
}
8486

85-
envvars = append(envvars, GetProxyEnvVars(workspaceWithConfig.Config.Routing.ProxyConfig)...)
87+
envvars = append(envvars, getProxyEnvVars(workspaceWithConfig.Config.Routing.ProxyConfig)...)
88+
envvars = append(envvars, getSshAskPassEnvVars()...)
8689

8790
return envvars
8891
}
8992

90-
func GetProxyEnvVars(proxyConfig *v1alpha1.Proxy) []corev1.EnvVar {
93+
func getSshAskPassEnvVars() []corev1.EnvVar {
94+
return []corev1.EnvVar{
95+
{
96+
Name: constants.SSHAskPass,
97+
Value: fmt.Sprintf("%s%s", workspace.SshAskPassMountPath, workspace.SshAskPassScriptFileName),
98+
},
99+
{
100+
Name: constants.DISPLAY,
101+
Value: ":0",
102+
},
103+
}
104+
}
105+
106+
func getProxyEnvVars(proxyConfig *v1alpha1.Proxy) []corev1.EnvVar {
91107
if proxyConfig == nil {
92108
return nil
93109
}

pkg/library/ssh/event.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
// Copyright (c) 2019-2024 Red Hat, Inc.
2+
// Licensed under the Apache License, Version 2.0 (the "License");
3+
// you may not use this file except in compliance with the License.
4+
// You may obtain a copy of the License at
5+
//
6+
// http://www.apache.org/licenses/LICENSE-2.0
7+
//
8+
// Unless required by applicable law or agreed to in writing, software
9+
// distributed under the License is distributed on an "AS IS" BASIS,
10+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
11+
// See the License for the specific language governing permissions and
12+
// limitations under the License.
13+
14+
package ssh
15+
16+
import (
17+
"github.com/devfile/api/v2/pkg/apis/workspaces/v1alpha2"
18+
"github.com/devfile/devworkspace-operator/pkg/constants"
19+
"github.com/devfile/devworkspace-operator/pkg/library/lifecycle"
20+
)
21+
22+
const commandLine = `SSH_ENV_PATH=$HOME/ssh-environment \
23+
&& if [ -f /etc/ssh/passphrase ] && command -v ssh-add >/dev/null; \
24+
then ssh-agent | sed 's/^echo/#echo/' > $SSH_ENV_PATH \
25+
&& chmod 600 $SSH_ENV_PATH && source $SSH_ENV_PATH \
26+
&& ssh-add /etc/ssh/dwo_ssh_key < /etc/ssh/passphrase \
27+
&& if [ -f $HOME/.bashrc ] && [ -w $HOME/.bashrc ]; then echo "source ${SSH_ENV_PATH}" >> $HOME/.bashrc; fi; fi`
28+
29+
// AddSshAgentPostStartEvent Start ssh-agent and add the default ssh key to it, if the ssh key has a passphrase.
30+
// Initialise the ssh-agent session env variables in the user .bashrc file.
31+
func AddSshAgentPostStartEvent(spec *v1alpha2.DevWorkspaceTemplateSpec) error {
32+
if spec.Commands == nil {
33+
spec.Commands = []v1alpha2.Command{}
34+
}
35+
36+
if spec.Events == nil {
37+
spec.Events = &v1alpha2.Events{}
38+
}
39+
40+
_, mainComponents, err := lifecycle.GetInitContainers(spec.DevWorkspaceTemplateSpecContent)
41+
for _, component := range mainComponents {
42+
if component.Container == nil {
43+
continue
44+
}
45+
spec.Commands = append(spec.Commands, v1alpha2.Command{
46+
Id: constants.SshAgentStartEventId,
47+
CommandUnion: v1alpha2.CommandUnion{
48+
Exec: &v1alpha2.ExecCommand{
49+
CommandLine: commandLine,
50+
Component: component.Name,
51+
},
52+
},
53+
})
54+
}
55+
spec.Events.PostStart = append(spec.Events.PostStart, constants.SshAgentStartEventId)
56+
return err
57+
}

pkg/provision/workspace/ssh.go

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
//
2+
// Copyright (c) 2019-2024 Red Hat, Inc.
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
//
15+
16+
package workspace
17+
18+
import (
19+
_ "embed"
20+
"path"
21+
22+
"github.com/devfile/devworkspace-operator/apis/controller/v1alpha1"
23+
"github.com/devfile/devworkspace-operator/pkg/constants"
24+
"github.com/devfile/devworkspace-operator/pkg/dwerrors"
25+
"github.com/devfile/devworkspace-operator/pkg/provision/sync"
26+
corev1 "k8s.io/api/core/v1"
27+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
28+
"k8s.io/utils/pointer"
29+
)
30+
31+
const SshAskPassMountPath = "/.ssh-askpass/"
32+
const SshAskPassScriptFileName = "ssh-askpass.sh"
33+
34+
//go:embed ssh-askpass.sh
35+
var data string
36+
37+
func ProvisionSshAskPass(api sync.ClusterAPI, namespace string, podAdditions *v1alpha1.PodAdditions) error {
38+
sshAskPassConfigMap := constructSshAskPassCM(namespace)
39+
if _, err := sync.SyncObjectWithCluster(sshAskPassConfigMap, api); err != nil {
40+
switch err.(type) {
41+
case *sync.NotInSyncError: // Ignore the object created error
42+
default:
43+
return dwerrors.WrapSyncError(err)
44+
}
45+
}
46+
47+
sshAskPassVolumeMounts, sshAskPassVolumes, err := getSshAskPassVolumesAndVolumeMounts()
48+
if err != nil {
49+
return err
50+
}
51+
podAdditions.VolumeMounts = append(podAdditions.VolumeMounts, sshAskPassVolumeMounts...)
52+
podAdditions.Volumes = append(podAdditions.Volumes, sshAskPassVolumes...)
53+
return nil
54+
}
55+
56+
func constructSshAskPassCM(namespace string) *corev1.ConfigMap {
57+
askPassConfigMap := &corev1.ConfigMap{
58+
ObjectMeta: metav1.ObjectMeta{
59+
Name: constants.SshAskPassConfigMapName,
60+
Namespace: namespace,
61+
Labels: map[string]string{
62+
"app.kubernetes.io/defaultName": "ssh-askpass-secret",
63+
"app.kubernetes.io/part-of": "devworkspace-operator",
64+
"controller.devfile.io/watch-configmap": "true",
65+
},
66+
},
67+
Data: map[string]string{
68+
SshAskPassScriptFileName: data,
69+
},
70+
}
71+
return askPassConfigMap
72+
}
73+
74+
func getSshAskPassVolumesAndVolumeMounts() ([]corev1.VolumeMount, []corev1.Volume, error) {
75+
name := "ssh-askpass"
76+
volume := corev1.Volume{
77+
Name: name,
78+
VolumeSource: corev1.VolumeSource{
79+
ConfigMap: &corev1.ConfigMapVolumeSource{
80+
LocalObjectReference: corev1.LocalObjectReference{
81+
Name: constants.SshAskPassConfigMapName,
82+
},
83+
DefaultMode: pointer.Int32(0755),
84+
},
85+
},
86+
}
87+
volumeMount := corev1.VolumeMount{
88+
Name: name,
89+
ReadOnly: true,
90+
MountPath: path.Join(SshAskPassMountPath, SshAskPassScriptFileName),
91+
SubPath: SshAskPassScriptFileName,
92+
}
93+
return []corev1.VolumeMount{volumeMount}, []corev1.Volume{volume}, nil
94+
}

project-clone/Dockerfile

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,10 @@ COPY --from=builder /project-clone/_output/bin/project-clone /usr/local/bin/proj
4444

4545
ENV USER_UID=1001 \
4646
USER_NAME=project-clone \
47-
HOME=/home/user \
48-
DISPLAY=":0" \
49-
SSH_ASKPASS=/usr/local/bin/ssh-askpass.sh
47+
HOME=/home/user
5048

5149
COPY build/bin /usr/local/bin
52-
COPY project-clone/ssh-askpass.sh /usr/local/bin
5350
RUN /usr/local/bin/user_setup
54-
RUN chmod +x /usr/local/bin/ssh-askpass.sh
5551

5652
USER ${USER_UID}
5753

0 commit comments

Comments
 (0)