From 6fea3e203dfe4b0aebd7d9869e8777596e857ef5 Mon Sep 17 00:00:00 2001 From: Moath Qasim Date: Wed, 12 Aug 2020 16:51:32 +0200 Subject: [PATCH] adding ssh key support Signed-off-by: Moath Qasim --- pkg/kubevirt/apis/provider_spec.go | 3 ++ pkg/kubevirt/core/core.go | 26 +++++++++++---- pkg/kubevirt/core/util.go | 33 +++++++++++++++---- pkg/kubevirt/core/util_test.go | 51 ++++++++++++++++++++++++++++++ 4 files changed, 100 insertions(+), 13 deletions(-) create mode 100644 pkg/kubevirt/core/util_test.go diff --git a/pkg/kubevirt/apis/provider_spec.go b/pkg/kubevirt/apis/provider_spec.go index cd2b09f..a63654c 100644 --- a/pkg/kubevirt/apis/provider_spec.go +++ b/pkg/kubevirt/apis/provider_spec.go @@ -37,4 +37,7 @@ type KubeVirtProviderSpec struct { // DNS options set along with hostNetwork, you have to specify DNS policy explicitly to 'ClusterFirstWithHostNet'. // +optional DNSPolicy string `json:"dnsPolicy,omitempty"` + // SSHKeys is an optional array of ssh public keys to deploy to VM (may already be included in UserData) + // +optional + SSHKeys []string `json:"sshKeys,omitempty"` } diff --git a/pkg/kubevirt/core/core.go b/pkg/kubevirt/core/core.go index 6b14b21..69441f0 100644 --- a/pkg/kubevirt/core/core.go +++ b/pkg/kubevirt/core/core.go @@ -19,6 +19,7 @@ import ( "context" "fmt" "strconv" + "strings" "time" api "github.com/gardener/machine-controller-manager-provider-kubevirt/pkg/kubevirt/apis" @@ -60,8 +61,8 @@ func NewPluginSPIImpl(client ClientFunc) (*PluginSPIImpl, error) { // CreateMachine creates a kubevirt virtual machine based on the passed provider spec with an associated data volume based on the // DataVolumeTemplate. It also creates a secret where the userdata(cloud-init) are saved and mounted on the vm. -func (p PluginSPIImpl) CreateMachine(ctx context.Context, machineName string, providerSpec *api.KubeVirtProviderSpec, secrets *corev1.Secret) (providerID string, err error) { - c, err := p.client(secrets) +func (p PluginSPIImpl) CreateMachine(ctx context.Context, machineName string, providerSpec *api.KubeVirtProviderSpec, secret *corev1.Secret) (providerID string, err error) { + c, err := p.client(secret) if err != nil { return "", fmt.Errorf("failed to create kubevirt client: %v", err) } @@ -98,6 +99,19 @@ func (p PluginSPIImpl) CreateMachine(ctx context.Context, machineName string, pr } } + userData := string(secret.Data["userData"]) + if len(providerSpec.SSHKeys) > 0 { + var userSSHKeys []string + for _, sshKey := range providerSpec.SSHKeys { + userSSHKeys = append(userSSHKeys, strings.TrimSpace(sshKey)) + } + + userData, err = addUserSSHKeysToUserData(userData, userSSHKeys) + if err != nil { + return "", fmt.Errorf("failed to add ssh keys to cloud-init: %v", err) + } + } + virtualMachine := &kubevirtv1.VirtualMachine{ ObjectMeta: metav1.ObjectMeta{ Name: machineName, @@ -188,20 +202,20 @@ func (p PluginSPIImpl) CreateMachine(ctx context.Context, machineName string, pr return "", fmt.Errorf("failed to create vmi: %v", err) } - secret := &corev1.Secret{ + userDataSecret := &corev1.Secret{ ObjectMeta: metav1.ObjectMeta{ Name: userdataSecretName, Namespace: virtualMachine.Namespace, OwnerReferences: []metav1.OwnerReference{*metav1.NewControllerRef(virtualMachine, kubevirtv1.VirtualMachineGroupVersionKind)}, }, - Data: map[string][]byte{"userdata": []byte(secrets.Data["userData"])}, + Data: map[string][]byte{"userdata": []byte(userData)}, } - if err := c.Create(ctx, secret); err != nil { + if err := c.Create(ctx, userDataSecret); err != nil { return "", fmt.Errorf("failed to create secret for userdata: %v", err) } - return p.machineProviderID(ctx, secrets, machineName, providerSpec.Namespace) + return p.machineProviderID(ctx, secret, machineName, providerSpec.Namespace) } // DeleteMachine delete the virtual machine which then delete tha cirtual machine instance and all associated resources such DataVolume. diff --git a/pkg/kubevirt/core/util.go b/pkg/kubevirt/core/util.go index beb3c86..eb5f812 100644 --- a/pkg/kubevirt/core/util.go +++ b/pkg/kubevirt/core/util.go @@ -15,20 +15,15 @@ package core import ( + "errors" "fmt" + "strings" corev1 "k8s.io/api/core/v1" "k8s.io/client-go/tools/clientcmd" "sigs.k8s.io/controller-runtime/pkg/client" ) -func encodeProviderID(machineID string) string { - if machineID == "" { - return "" - } - return fmt.Sprintf("%s/%s", ProviderName, machineID) -} - // KubevirtClient creates kubevirt client based on the kubeconfig of the kubevirt cluster which is saved in the secret that // is passed to it. func KubevirtClient(secret *corev1.Secret) (client.Client, error) { @@ -44,3 +39,27 @@ func KubevirtClient(secret *corev1.Secret) (client.Client, error) { return client.New(config, client.Options{}) } + +func encodeProviderID(machineID string) string { + if machineID == "" { + return "" + } + return fmt.Sprintf("%s/%s", ProviderName, machineID) +} + +func addUserSSHKeysToUserData(userData string, sshKeys []string) (string, error) { + var userDataBuilder strings.Builder + if strings.Contains(userData, "ssh_authorized_keys:") { + return "", errors.New("userdata already contains key `ssh_authorized_keys`") + } + + userDataBuilder.WriteString(userData) + userDataBuilder.WriteString("\nssh_authorized_keys:\n") + for _, key := range sshKeys { + userDataBuilder.WriteString("- ") + userDataBuilder.WriteString(key) + userDataBuilder.WriteString("\n") + } + + return userDataBuilder.String(), nil +} diff --git a/pkg/kubevirt/core/util_test.go b/pkg/kubevirt/core/util_test.go new file mode 100644 index 0000000..1e92276 --- /dev/null +++ b/pkg/kubevirt/core/util_test.go @@ -0,0 +1,51 @@ +package core + +import ( + "strings" + "testing" +) + +var ( + testCases = []struct { + name string + userData string + sshKeys []string + expectedUserData string + expectedError bool + }{ + { + name: "`ssh_authorized_keys` key already exists error", + userData: "#cloud-config\nchpasswd:\nexpire: false\npassword: pass\nuser: test\nssh_authorized_keys:\n- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDdOIhYmzCK5DSVLu", + sshKeys: []string{"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDdOIhYmzCK5DSVLu3b"}, + expectedUserData: "", + expectedError: true, + }, + { + name: "add user ssh key to userdata successfully", + userData: "#cloud-config\nchpasswd:\nexpire: false\npassword: pass\nuser: test", + sshKeys: []string{"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDdOIhYmzCK5DSVLu3b"}, + expectedUserData: "#cloud-config\nchpasswd:\nexpire: false\npassword: pass\nuser: test\nssh_authorized_keys:\n- ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAACAQDdOIhYmzCK5DSVLu3b", + expectedError: false, + }, + } +) + +func TestAddUserSSHKeysToUserData(t *testing.T) { + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + u, err := addUserSSHKeysToUserData(testCase.userData, testCase.sshKeys) + if testCase.expectedError && err == nil { + t.Fatal("expected an error but got error: nil") + } + + if err != nil && !testCase.expectedError { + t.Fatalf("unexpected error was encoutred: %v", err) + } + + if strings.TrimSpace(testCase.expectedUserData) != strings.TrimSpace(u) { + t.Fatalf("expecting userdata: %v and got: %v", testCase.expectedUserData, u) + } + + }) + } +}