Skip to content

Commit 723f241

Browse files
committed
AzureMachinePool/AzureManagedControlPlane: generate ssh key when is not set
1 parent 110f084 commit 723f241

8 files changed

+607
-12
lines changed
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1alpha3
18+
19+
import (
20+
"crypto/rand"
21+
"crypto/rsa"
22+
"encoding/base64"
23+
24+
"github.com/pkg/errors"
25+
"golang.org/x/crypto/ssh"
26+
)
27+
28+
// SetDefaultSSHPublicKey sets the default SSHPublicKey for an AzureMachinePool
29+
func (amp *AzureMachinePool) SetDefaultSSHPublicKey() error {
30+
sshKeyData := amp.Spec.Template.SSHPublicKey
31+
if sshKeyData == "" {
32+
privateKey, perr := rsa.GenerateKey(rand.Reader, 2048)
33+
if perr != nil {
34+
return errors.Wrap(perr, "Failed to generate private key")
35+
}
36+
37+
publicRsaKey, perr := ssh.NewPublicKey(&privateKey.PublicKey)
38+
if perr != nil {
39+
return errors.Wrap(perr, "Failed to generate public key")
40+
}
41+
amp.Spec.Template.SSHPublicKey = base64.StdEncoding.EncodeToString(ssh.MarshalAuthorizedKey(publicRsaKey))
42+
}
43+
44+
return nil
45+
}
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1alpha3
18+
19+
import (
20+
"testing"
21+
22+
. "github.com/onsi/gomega"
23+
)
24+
25+
func TestAzureMachinePool_SetDefaultSSHPublicKey(t *testing.T) {
26+
g := NewWithT(t)
27+
28+
type test struct {
29+
amp *AzureMachinePool
30+
}
31+
32+
existingPublicKey := "testpublickey"
33+
publicKeyExistTest := test{amp: createMachinePoolWithSSHPublicKey(t, existingPublicKey)}
34+
publicKeyNotExistTest := test{amp: createMachinePoolWithSSHPublicKey(t, "")}
35+
36+
err := publicKeyExistTest.amp.SetDefaultSSHPublicKey()
37+
g.Expect(err).To(BeNil())
38+
g.Expect(publicKeyExistTest.amp.Spec.Template.SSHPublicKey).To(Equal(existingPublicKey))
39+
40+
err = publicKeyNotExistTest.amp.SetDefaultSSHPublicKey()
41+
g.Expect(err).To(BeNil())
42+
g.Expect(publicKeyNotExistTest.amp.Spec.Template.SSHPublicKey).NotTo(BeEmpty())
43+
}
44+
45+
func createMachinePoolWithSSHPublicKey(t *testing.T, sshPublicKey string) *AzureMachinePool {
46+
return hardcodedAzureMachinePoolWithSSHKey(sshPublicKey)
47+
}
48+
49+
func hardcodedAzureMachinePoolWithSSHKey(sshPublicKey string) *AzureMachinePool {
50+
return &AzureMachinePool{
51+
Spec: AzureMachinePoolSpec{
52+
Template: AzureMachineTemplate{
53+
SSHPublicKey: sshPublicKey,
54+
},
55+
},
56+
}
57+
}

exp/api/v1alpha3/azuremachinepool_webhook.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,11 @@ var _ webhook.Defaulter = &AzureMachinePool{}
4545
// Default implements webhook.Defaulter so a webhook will be registered for the type
4646
func (amp *AzureMachinePool) Default() {
4747
azuremachinepoollog.Info("default", "name", amp.Name)
48+
49+
err := amp.SetDefaultSSHPublicKey()
50+
if err != nil {
51+
azuremachinepoollog.Error(err, "SetDefaultSshPublicKey failed")
52+
}
4853
}
4954

5055
// +kubebuilder:webhook:verbs=create;update,path=/validate-exp-cluster-x-k8s-io-x-k8s-io-v1alpha3-azuremachinepool,mutating=false,failurePolicy=fail,matchPolicy=Equivalent,groups=exp.cluster.x-k8s.io.x-k8s.io,resources=azuremachinepools,versions=v1alpha3,name=vazuremachinepool.kb.io,sideEffects=None
@@ -74,6 +79,7 @@ func (amp *AzureMachinePool) Validate() error {
7479
validators := []func() error{
7580
amp.ValidateImage,
7681
amp.ValidateTerminateNotificationTimeout,
82+
amp.ValidateSSHKey,
7783
}
7884

7985
var errs []error
@@ -100,6 +106,7 @@ func (amp *AzureMachinePool) ValidateImage() error {
100106
return agg
101107
}
102108
}
109+
103110
return nil
104111
}
105112

@@ -118,3 +125,17 @@ func (amp *AzureMachinePool) ValidateTerminateNotificationTimeout() error {
118125

119126
return nil
120127
}
128+
129+
// ValidateSSHKey validates an SSHKey
130+
func (amp *AzureMachinePool) ValidateSSHKey() error {
131+
if amp.Spec.Template.SSHPublicKey != "" {
132+
sshKey := amp.Spec.Template.SSHPublicKey
133+
if errs := infrav1.ValidateSSHKey(sshKey, field.NewPath("sshKey")); len(errs) > 0 {
134+
agg := kerrors.NewAggregate(errs.ToAggregate().Errors())
135+
azuremachinepoollog.Info("Invalid sshKey: %s", agg.Error())
136+
return agg
137+
}
138+
}
139+
140+
return nil
141+
}
Lines changed: 220 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,220 @@
1+
/*
2+
Copyright 2020 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1alpha3
18+
19+
import (
20+
"crypto/rand"
21+
"crypto/rsa"
22+
"encoding/base64"
23+
"testing"
24+
25+
"github.com/Azure/go-autorest/autorest/to"
26+
. "github.com/onsi/gomega"
27+
"golang.org/x/crypto/ssh"
28+
29+
infrav1 "sigs.k8s.io/cluster-api-provider-azure/api/v1alpha3"
30+
)
31+
32+
var (
33+
validSSHPublicKey = generateSSHPublicKey(true)
34+
)
35+
36+
func TestAzureMachinePool_ValidateCreate(t *testing.T) {
37+
g := NewWithT(t)
38+
39+
tests := []struct {
40+
name string
41+
amp *AzureMachinePool
42+
wantErr bool
43+
}{
44+
{
45+
name: "azuremachinepool with marketplace image - full",
46+
amp: createMachinePoolWithtMarketPlaceImage(t, "PUB1234", "OFFER1234", "SKU1234", "1.0.0", to.IntPtr(10)),
47+
wantErr: false,
48+
},
49+
{
50+
name: "azuremachinepool with marketplace image - missing publisher",
51+
amp: createMachinePoolWithtMarketPlaceImage(t, "", "OFFER1234", "SKU1234", "1.0.0", to.IntPtr(10)),
52+
wantErr: true,
53+
},
54+
{
55+
name: "azuremachinepool with shared gallery image - full",
56+
amp: createMachinePoolWithSharedImage(t, "SUB123", "RG123", "NAME123", "GALLERY1", "1.0.0", to.IntPtr(10)),
57+
wantErr: false,
58+
},
59+
{
60+
name: "azuremachinepool with marketplace image - missing subscription",
61+
amp: createMachinePoolWithSharedImage(t, "", "RG123", "NAME123", "GALLERY1", "1.0.0", to.IntPtr(10)),
62+
wantErr: true,
63+
},
64+
{
65+
name: "azuremachinepool with image by - with id",
66+
amp: createMachinePoolWithImageByID(t, "ID123", to.IntPtr(10)),
67+
wantErr: false,
68+
},
69+
{
70+
name: "azuremachinepool with image by - without id",
71+
amp: createMachinePoolWithImageByID(t, "", to.IntPtr(10)),
72+
wantErr: true,
73+
},
74+
{
75+
name: "azuremachinepool with valid SSHPublicKey",
76+
amp: createMachinePoolWithSSHPublicKey(t, validSSHPublicKey),
77+
wantErr: false,
78+
},
79+
{
80+
name: "azuremachinepool with invalid SSHPublicKey",
81+
amp: createMachinePoolWithSSHPublicKey(t, "invalid ssh key"),
82+
wantErr: true,
83+
},
84+
{
85+
name: "azuremachinepool with wrong terminate notification",
86+
amp: createMachinePoolWithSharedImage(t, "SUB123", "RG123", "NAME123", "GALLERY1", "1.0.0", to.IntPtr(35)),
87+
wantErr: true,
88+
},
89+
}
90+
for _, tc := range tests {
91+
t.Run(tc.name, func(t *testing.T) {
92+
err := tc.amp.ValidateCreate()
93+
if tc.wantErr {
94+
g.Expect(err).To(HaveOccurred())
95+
} else {
96+
g.Expect(err).NotTo(HaveOccurred())
97+
}
98+
})
99+
}
100+
}
101+
102+
func TestAzureMachinePool_ValidateUpdate(t *testing.T) {
103+
g := NewWithT(t)
104+
105+
tests := []struct {
106+
name string
107+
oldAMP *AzureMachinePool
108+
amp *AzureMachinePool
109+
wantErr bool
110+
}{
111+
{
112+
name: "azuremachine with valid SSHPublicKey",
113+
oldAMP: createMachinePoolWithSSHPublicKey(t, ""),
114+
amp: createMachinePoolWithSSHPublicKey(t, validSSHPublicKey),
115+
wantErr: false,
116+
},
117+
{
118+
name: "azuremachine with invalid SSHPublicKey",
119+
oldAMP: createMachinePoolWithSSHPublicKey(t, ""),
120+
amp: createMachinePoolWithSSHPublicKey(t, "invalid ssh key"),
121+
wantErr: true,
122+
},
123+
}
124+
for _, tc := range tests {
125+
t.Run(tc.name, func(t *testing.T) {
126+
err := tc.amp.ValidateUpdate(tc.oldAMP)
127+
if tc.wantErr {
128+
g.Expect(err).To(HaveOccurred())
129+
} else {
130+
g.Expect(err).NotTo(HaveOccurred())
131+
}
132+
})
133+
}
134+
}
135+
136+
func TestAzureMachine_Default(t *testing.T) {
137+
g := NewWithT(t)
138+
139+
type test struct {
140+
amp *AzureMachinePool
141+
}
142+
143+
existingPublicKey := validSSHPublicKey
144+
publicKeyExistTest := test{amp: createMachinePoolWithSSHPublicKey(t, existingPublicKey)}
145+
publicKeyNotExistTest := test{amp: createMachinePoolWithSSHPublicKey(t, "")}
146+
147+
publicKeyExistTest.amp.Default()
148+
g.Expect(publicKeyExistTest.amp.Spec.Template.SSHPublicKey).To(Equal(existingPublicKey))
149+
150+
publicKeyNotExistTest.amp.Default()
151+
g.Expect(publicKeyNotExistTest.amp.Spec.Template.SSHPublicKey).NotTo((BeEmpty()))
152+
}
153+
154+
func createMachinePoolWithtMarketPlaceImage(t *testing.T, publisher, offer, sku, version string, terminateNotificationTimeout *int) *AzureMachinePool {
155+
image := &infrav1.Image{
156+
Marketplace: &infrav1.AzureMarketplaceImage{
157+
Publisher: publisher,
158+
Offer: offer,
159+
SKU: sku,
160+
Version: version,
161+
},
162+
}
163+
164+
return &AzureMachinePool{
165+
Spec: AzureMachinePoolSpec{
166+
Template: AzureMachineTemplate{
167+
Image: image,
168+
SSHPublicKey: validSSHPublicKey,
169+
TerminateNotificationTimeout: terminateNotificationTimeout,
170+
},
171+
},
172+
}
173+
}
174+
175+
func createMachinePoolWithSharedImage(t *testing.T, subscriptionID, resourceGroup, name, gallery, version string, terminateNotificationTimeout *int) *AzureMachinePool {
176+
image := &infrav1.Image{
177+
SharedGallery: &infrav1.AzureSharedGalleryImage{
178+
SubscriptionID: subscriptionID,
179+
ResourceGroup: resourceGroup,
180+
Name: name,
181+
Gallery: gallery,
182+
Version: version,
183+
},
184+
}
185+
186+
return &AzureMachinePool{
187+
Spec: AzureMachinePoolSpec{
188+
Template: AzureMachineTemplate{
189+
Image: image,
190+
SSHPublicKey: validSSHPublicKey,
191+
TerminateNotificationTimeout: terminateNotificationTimeout,
192+
},
193+
},
194+
}
195+
}
196+
197+
func createMachinePoolWithImageByID(t *testing.T, imageID string, terminateNotificationTimeout *int) *AzureMachinePool {
198+
image := &infrav1.Image{
199+
ID: &imageID,
200+
}
201+
202+
return &AzureMachinePool{
203+
Spec: AzureMachinePoolSpec{
204+
Template: AzureMachineTemplate{
205+
Image: image,
206+
SSHPublicKey: validSSHPublicKey,
207+
TerminateNotificationTimeout: terminateNotificationTimeout,
208+
},
209+
},
210+
}
211+
}
212+
213+
func generateSSHPublicKey(b64Enconded bool) string {
214+
privateKey, _ := rsa.GenerateKey(rand.Reader, 2048)
215+
publicRsaKey, _ := ssh.NewPublicKey(&privateKey.PublicKey)
216+
if b64Enconded {
217+
return base64.StdEncoding.EncodeToString(ssh.MarshalAuthorizedKey(publicRsaKey))
218+
}
219+
return string(ssh.MarshalAuthorizedKey(publicRsaKey))
220+
}

0 commit comments

Comments
 (0)