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

feat: Allow configuration of multiple SSH Keys #181

Merged
merged 2 commits into from
Jun 28, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
17 changes: 13 additions & 4 deletions api/v1alpha1/microvmcluster_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ type MicrovmClusterSpec struct {
//
// +optional
ControlPlaneEndpoint clusterv1.APIEndpoint `json:"controlPlaneEndpoint"`
// SSHPublicKey is an SSH public key that will be used with the default user. If specified
// this will apply to all machine created unless you specify a different key at the
// machine level.
// SSHPublicKeys is a list of SSHPublicKeys and their associated users.
// If specified these keys will be applied to all machine created unless you
// specify different keys at the machine level.
// +optional
SSHPublicKey string `json:"sshPublicKey,omitempty"`
SSHPublicKeys []SSHPublicKey `json:"sshPublicKeys,omitempty"`
// Placement specifies how machines for the cluster should be placed onto hosts (i.e. where the microvms are created).
// +kubebuilder:validation:Required
Placement Placement `json:"placement"`
Expand All @@ -30,6 +30,15 @@ type MicrovmClusterSpec struct {
MicrovmProxy *Proxy `json:"microvmProxy,omitempty"`
}

type SSHPublicKey struct {
// User is the name of the user to add keys for (eg root, ubuntu).
// +kubebuilder:validation:Required
User string `json:"user,omitempty"`
// AuthorizedKeys is a list of public keys to add to the user
// +kubebuilder:validation:Required
AuthorizedKeys []string `json:"authorizedKeys,omitempty"`
}

// MicrovmClusterStatus defines the observed state of MicrovmCluster.
type MicrovmClusterStatus struct {
// Ready indicates that the cluster is ready.
Expand Down
7 changes: 4 additions & 3 deletions api/v1alpha1/microvmmachine_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,12 @@ const (
type MicrovmMachineSpec struct {
MicrovmSpec `json:",inline"`

// SSHPublicKey is an SSH public key that will be used with the default user on this
// machine. If specified it will take precedence over any SSH key specified at
// SSHPublicKeys is list of SSH public keys that will be used with stated users
// on this machine.
// If specified they will take precedence over any SSH keys specified at
// the cluster level.
// +optional
SSHPublicKey string `json:"sshPublicKey,omitempty"`
SSHPublicKeys []SSHPublicKey `json:"sshPublicKeys,omitempty"`

// ProviderID is the unique identifier as specified by the cloud provider.
ProviderID *string `json:"providerID,omitempty"`
Expand Down
34 changes: 34 additions & 0 deletions api/v1alpha1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -119,11 +119,24 @@ spec:
- hosts
type: object
type: object
sshPublicKey:
description: SSHPublicKey is an SSH public key that will be used with
the default user. If specified this will apply to all machine created
unless you specify a different key at the machine level.
type: string
sshPublicKeys:
description: SSHPublicKeys is a list of SSHPublicKeys and their associated
users. If specified these keys will be applied to all machine created
unless you specify different keys at the machine level.
items:
properties:
authorizedKeys:
description: AuthorizedKeys is a list of public keys to add
to the user
items:
type: string
type: array
user:
description: User is the name of the user to add keys for (eg
root, ubuntu).
type: string
type: object
type: array
required:
- placement
type: object
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,24 @@ spec:
- id
- image
type: object
sshPublicKey:
description: SSHPublicKey is an SSH public key that will be used with
the default user on this machine. If specified it will take precedence
over any SSH key specified at the cluster level.
type: string
sshPublicKeys:
description: SSHPublicKeys is list of SSH public keys that will be
used with stated users on this machine. If specified they will take
precedence over any SSH keys specified at the cluster level.
items:
properties:
authorizedKeys:
description: AuthorizedKeys is a list of public keys to add
to the user
items:
type: string
type: array
user:
description: User is the name of the user to add keys for (eg
root, ubuntu).
type: string
type: object
type: array
vcpu:
description: VCPU specifies how many vcpu's the microvm will be allocated.
format: int64
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,12 +171,25 @@ spec:
- id
- image
type: object
sshPublicKey:
description: SSHPublicKey is an SSH public key that will be
used with the default user on this machine. If specified
it will take precedence over any SSH key specified at the
cluster level.
type: string
sshPublicKeys:
description: SSHPublicKeys is list of SSH public keys that
will be used with stated users on this machine. If specified
they will take precedence over any SSH keys specified at
the cluster level.
items:
properties:
authorizedKeys:
description: AuthorizedKeys is a list of public keys
to add to the user
items:
type: string
type: array
user:
description: User is the name of the user to add keys
for (eg root, ubuntu).
type: string
type: object
type: array
vcpu:
description: VCPU specifies how many vcpu's the microvm will
be allocated.
Expand Down
37 changes: 18 additions & 19 deletions controllers/helpers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
fakeremote "sigs.k8s.io/cluster-api/controllers/remote/fake"
"sigs.k8s.io/cluster-api/util/conditions"

"github.com/weaveworks-liquidmetal/cluster-api-provider-microvm/api/v1alpha1"
infrav1 "github.com/weaveworks-liquidmetal/cluster-api-provider-microvm/api/v1alpha1"
"github.com/weaveworks-liquidmetal/cluster-api-provider-microvm/controllers"
"github.com/weaveworks-liquidmetal/cluster-api-provider-microvm/internal/services/microvm"
Expand Down Expand Up @@ -395,30 +396,28 @@ func assertMachineNotReady(g *WithT, machine *infrav1.MicrovmMachine) {
g.Expect(machine.Status.Ready).To(BeFalse())
}

func assertVendorData(g *WithT, vendorDataRaw string, expectedSSHKey string) {
func assertVendorData(g *WithT, vendorDataRaw string, expectedSSHKeys []v1alpha1.SSHPublicKey) {
g.Expect(vendorDataRaw).ToNot(Equal(""))
g.Expect(expectedSSHKeys).ToNot(BeNil())

data, err := base64.StdEncoding.DecodeString(vendorDataRaw)
g.Expect(err).NotTo(HaveOccurred(), "expect vendor data to be base64 encoded")

if expectedSSHKey != "" {
vendorData := &userdata.UserData{}

unmarshallErr := yaml.Unmarshal(data, vendorData)
g.Expect(unmarshallErr).NotTo(HaveOccurred(), "expect vendor data to unmarshall to cloud-init userdata")
g.Expect(vendorData.Users).NotTo(BeNil())
users := vendorData.Users
g.Expect(users).To(HaveLen(2))
for i := range users {
user := users[i]

g.Expect(user.SSHAuthorizedKeys).NotTo(BeNil())
keys := user.SSHAuthorizedKeys
g.Expect(keys).To(HaveLen(1))
g.Expect(keys[0]).To(Equal(expectedSSHKey))
}
vendorData := &userdata.UserData{}
g.Expect(yaml.Unmarshal(data, vendorData)).To(Succeed(), "expect vendor data to unmarshall to cloud-init userdata")

users := vendorData.Users
g.Expect(users).NotTo(BeNil())
g.Expect(len(users)).To(Equal(len(expectedSSHKeys)))

vendorDataStr := string(data)
g.Expect(vendorDataStr).To(ContainSubstring("#cloud-config\n"))
for i, user := range users {
g.Expect(user.Name).To(Equal(expectedSSHKeys[i].User))

keys := user.SSHAuthorizedKeys
g.Expect(keys).To(HaveLen(1))
g.Expect(keys[0]).To(Equal(expectedSSHKeys[i].AuthorizedKeys[0]))
}

vendorDataStr := string(data)
g.Expect(vendorDataStr).To(ContainSubstring("#cloud-config\n"))
}
24 changes: 16 additions & 8 deletions controllers/microvmmachine_controller_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -311,11 +311,14 @@ func TestMachineReconcileNoVmCreateClusterSSHSucceeds(t *testing.T) {
t.Parallel()
g := NewWithT(t)

expectedSSHKey := "ClusterSSH"
expectedKeys := []v1alpha1.SSHPublicKey{{
User: "ubuntu",
AuthorizedKeys: []string{"ClusterSSH"},
}}

apiObjects := defaultClusterObjects()
apiObjects.MvmMachine.Spec.ProviderID = nil
apiObjects.MvmCluster.Spec.SSHPublicKey = expectedSSHKey
apiObjects.MvmMachine.Spec.SSHPublicKeys = expectedKeys

fakeAPIClient := mock_client.FakeClient{}
withMissingMicrovm(&fakeAPIClient)
Expand All @@ -336,20 +339,25 @@ func TestMachineReconcileNoVmCreateClusterSSHSucceeds(t *testing.T) {
g.Expect(createReq.Microvm.Metadata).To(HaveKeyWithValue("user-data", expectedBootstrapData))

g.Expect(createReq.Microvm.Metadata).To(HaveKey("vendor-data"), "expect cloud-init vendor-data to be created")
assertVendorData(g, createReq.Microvm.Metadata["vendor-data"], expectedSSHKey)
assertVendorData(g, createReq.Microvm.Metadata["vendor-data"], expectedKeys)
}

func TestMachineReconcileNoVmCreateClusterMachineSSHSucceeds(t *testing.T) {
t.Parallel()
g := NewWithT(t)

clusterSSH := "ClusterSSH"
machineSSH := "MachineSSH"
expectedKeys := []v1alpha1.SSHPublicKey{{
AuthorizedKeys: []string{"MachineSSH"},
User: "root",
}, {
AuthorizedKeys: []string{"MachineSSH"},
User: "ubuntu",
}}

apiObjects := defaultClusterObjects()
apiObjects.MvmMachine.Spec.ProviderID = nil
apiObjects.MvmCluster.Spec.SSHPublicKey = clusterSSH
apiObjects.MvmMachine.Spec.SSHPublicKey = machineSSH
apiObjects.MvmCluster.Spec.SSHPublicKeys = []v1alpha1.SSHPublicKey{{AuthorizedKeys: []string{"ClusterSSH"}}}
apiObjects.MvmMachine.Spec.SSHPublicKeys = expectedKeys

fakeAPIClient := mock_client.FakeClient{}
withMissingMicrovm(&fakeAPIClient)
Expand All @@ -369,7 +377,7 @@ func TestMachineReconcileNoVmCreateClusterMachineSSHSucceeds(t *testing.T) {
g.Expect(createReq.Microvm.Metadata).To(HaveKeyWithValue("user-data", expectedBootstrapData))

g.Expect(createReq.Microvm.Metadata).To(HaveKey("vendor-data"), "expect cloud-init vendor-data to be created")
assertVendorData(g, createReq.Microvm.Metadata["vendor-data"], machineSSH)
assertVendorData(g, createReq.Microvm.Metadata["vendor-data"], expectedKeys)
}

func TestMachineReconcileNoVmCreateAdditionReconcile(t *testing.T) {
Expand Down
16 changes: 8 additions & 8 deletions internal/scope/machine.go
Original file line number Diff line number Diff line change
Expand Up @@ -268,18 +268,18 @@ func (m *MachineScope) GetInstanceID() string {
return parsed.ID()
}

// GetSSHPublicKey will return the SSH public key for this machine. It will take into account
// precedence rules. If there is no key then an empty string will be returned.
func (m *MachineScope) GetSSHPublicKey() string {
if m.MvmMachine.Spec.SSHPublicKey != "" {
return m.MvmMachine.Spec.SSHPublicKey
// GetSSHPublicKeys will return the SSH public keys for this machine. It will take into account
// precedence rules. If there are no keys then nil will be returned.
func (m *MachineScope) GetSSHPublicKeys() []infrav1.SSHPublicKey {
if len(m.MvmMachine.Spec.SSHPublicKeys) != 0 {
return m.MvmMachine.Spec.SSHPublicKeys
}

if m.MvmCluster.Spec.SSHPublicKey != "" {
return m.MvmCluster.Spec.SSHPublicKey
if len(m.MvmCluster.Spec.SSHPublicKeys) != 0 {
return m.MvmCluster.Spec.SSHPublicKeys
}

return ""
return nil
}

func (m *MachineScope) getFailureDomainFromProviderID(providerID string) string {
Expand Down
21 changes: 5 additions & 16 deletions internal/services/microvm/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -151,24 +151,13 @@ func (s *Service) createVendorData() (string, error) {
},
}

// TODO: allow setting multiple keys #88
machineSSHKey := s.scope.GetSSHPublicKey()
if machineSSHKey != "" {
defaultUser := userdata.User{
Name: "ubuntu",
}
rootUser := userdata.User{
Name: "root",
}

defaultUser.SSHAuthorizedKeys = []string{
machineSSHKey,
}
rootUser.SSHAuthorizedKeys = []string{
machineSSHKey,
for _, key := range s.scope.GetSSHPublicKeys() {
user := userdata.User{
Name: key.User,
SSHAuthorizedKeys: key.AuthorizedKeys,
}

vendorUserdata.Users = []userdata.User{defaultUser, rootUser}
vendorUserdata.Users = append(vendorUserdata.Users, user)
}

data, err := yaml.Marshal(vendorUserdata)
Expand Down