Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Apply ssh askpass flow for the workspace container #1307

Merged
merged 1 commit into from
Sep 9, 2024
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
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since the project clone container also gets environment variables from the commonEnvironmentVariables() function, we no longer have to setup the SSH askpass related environment variables in the project clone container's Dockerfile.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Awesome thank you! Just leaving a comment here as a reminder to myself: it's worth re-testing the functionality of #1291 since we're modifying how it's implemented now.

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 \
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we keep the current approach of modifying the .bashrc, it's probably worth making the ssh-environment file hidden with a . i.e. 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"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason why the blank identifier _ is being used? Couldn't we just have

import (
	"embed"
...
)

I've never used the embed directive, but examples of it don't seem to be using the blank identifier.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Importing the embed explicitly causes an error: pkg/provision/workspace/ssh.go:19:2: "embed" imported and not used

"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
Loading