Sitelet https://github.com/kubernetes/kops/commit/843608764f48536fadea3eace4d3cfafc3c28cf1
Skip to content

Commit 8436087

Browse files
committed
refactor(linode): build SSH keys with instances
Signed-off-by: Moshe Vayner <moshe@vayner.me>
1 parent 66df36c commit 8436087

5 files changed

Lines changed: 193 additions & 241 deletions

File tree

‎pkg/model/linodemodel/instances.go‎

Lines changed: 41 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package linodemodel
1818

1919
import (
2020
"fmt"
21+
"strings"
2122

2223
"k8s.io/kops/pkg/apis/kops"
2324
"k8s.io/kops/pkg/model"
@@ -26,19 +27,25 @@ import (
2627
"k8s.io/kops/upup/pkg/fi/cloudup/linodetasks"
2728
)
2829

30+
const maxLinodeSSHKeyNameLength = 64
31+
2932
// InstanceModelBuilder configures the Akamai (Linode) instances (aka Linodes) for the cluster.
3033
type InstanceModelBuilder struct {
3134
*LinodeModelContext
3235
Lifecycle fi.Lifecycle
36+
SSHKeyLifecycle fi.Lifecycle
3337
BootstrapScriptBuilder *model.BootstrapScriptBuilder
3438
}
3539

3640
var _ fi.CloudupModelBuilder = &InstanceModelBuilder{}
3741

3842
func (b *InstanceModelBuilder) Build(c *fi.CloudupModelBuilderContext) error {
39-
for _, ig := range b.InstanceGroups {
40-
var sshKeyTasks []*linodetasks.SSHKey
43+
sshKeyTask, err := b.buildSSHKeyTask(c)
44+
if err != nil {
45+
return err
46+
}
4147

48+
for _, ig := range b.InstanceGroups {
4249
subnets, err := b.GatherSubnets(ig)
4350
if err != nil {
4451
return err
@@ -53,12 +60,6 @@ func (b *InstanceModelBuilder) Build(c *fi.CloudupModelBuilderContext) error {
5360
return err
5461
}
5562

56-
for _, task := range c.Tasks {
57-
if sshKey, ok := task.(*linodetasks.SSHKey); ok {
58-
sshKeyTasks = append(sshKeyTasks, sshKey)
59-
}
60-
}
61-
6263
userData, err := b.BootstrapScriptBuilder.ResourceNodeUp(c, ig)
6364
if err != nil {
6465
return err
@@ -72,6 +73,10 @@ func (b *InstanceModelBuilder) Build(c *fi.CloudupModelBuilderContext) error {
7273
for k, v := range tagsMap {
7374
tags = append(tags, fmt.Sprintf("%s:%s", k, v))
7475
}
76+
var authorizedKeys []*linodetasks.SSHKey
77+
if sshKeyTask != nil {
78+
authorizedKeys = []*linodetasks.SSHKey{sshKeyTask}
79+
}
7580

7681
instanceGroup := linodetasks.Instance{
7782
Name: new(ig.Name),
@@ -80,7 +85,7 @@ func (b *InstanceModelBuilder) Build(c *fi.CloudupModelBuilderContext) error {
8085
Type: ig.Spec.MachineType,
8186
Subnet: subnetTask,
8287
RequirePublicInterface: requirePublicInterface(subnetSpec, ig),
83-
AuthorizedKeys: sshKeyTasks,
88+
AuthorizedKeys: authorizedKeys,
8489
Count: int(fi.ValueOf(ig.Spec.MinSize)),
8590
Image: ig.Spec.Image,
8691
UserData: userData,
@@ -93,6 +98,33 @@ func (b *InstanceModelBuilder) Build(c *fi.CloudupModelBuilderContext) error {
9398
return nil
9499
}
95100

101+
func (b *InstanceModelBuilder) buildSSHKeyTask(c *fi.CloudupModelBuilderContext) (*linodetasks.SSHKey, error) {
102+
if !b.UseSSHKey() {
103+
return nil, nil
104+
}
105+
106+
name, err := b.SSHKeyName()
107+
if err != nil {
108+
return nil, fmt.Errorf("error building Akamai (Linode) SSH key task: %w", err)
109+
}
110+
name = linode.NormalizeLinodeLabel(name)
111+
if len(name) > maxLinodeSSHKeyNameLength {
112+
name = strings.Trim(name[:maxLinodeSSHKeyNameLength], "-_")
113+
}
114+
115+
sshKeyTask := &linodetasks.SSHKey{
116+
Name: new(name),
117+
Lifecycle: b.SSHKeyLifecycle,
118+
}
119+
if len(b.SSHPublicKeys) > 0 {
120+
publicKey := fi.Resource(fi.NewBytesResource(b.SSHPublicKeys[0]))
121+
sshKeyTask.PublicKey = &publicKey
122+
}
123+
c.AddTask(sshKeyTask)
124+
125+
return sshKeyTask, nil
126+
}
127+
96128
// requirePublicInterface checks whether the instance group requires a public interface based on the subnet type and instance group settings.
97129
func requirePublicInterface(subnet *kops.ClusterSubnetSpec, ig *kops.InstanceGroup) *bool {
98130
requirePublic := false

‎pkg/model/linodemodel/instances_test.go‎

Lines changed: 151 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ import (
3030
"k8s.io/kops/upup/pkg/fi/fitasks"
3131
)
3232

33+
const testSSHPublicKey = "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCySdqIU+FhCWl3BNrAvPaOe5VfL2aCARUWwy91ZP+T7LBwFa9lhdttfjp/VX1D1/PVwntn2EhN079m8c2kfdmiZ/iCHqrLyIGSd+BOiCz0lT47znvANSfxYjLUuKrWWWeaXqerJkOsAD4PHchRLbZGPdbfoBKwtb/WT4GMRQmb9vmiaZYjsfdPPM9KkWI9ECoWFGjGehA8D+iYIPR711kRacb1xdYmnjHqxAZHFsb5L8wDWIeAyhy49cBD+lbzTiioq2xWLorXuFmXh6Do89PgzvHeyCLY6816f/kCX6wIFts8A2eaEHFL4rAOsuh6qHmSxGCR9peSyuRW8DxV725x justin@test"
34+
3335
func TestInstanceModelBuilderBuildUsesIGSubnetAndAllowsZeroSize(t *testing.T) {
3436
cluster := &kops.Cluster{
3537
ObjectMeta: metav1.ObjectMeta{Name: "example.k8s.local"},
@@ -51,30 +53,36 @@ func TestInstanceModelBuilderBuildUsesIGSubnetAndAllowsZeroSize(t *testing.T) {
5153
MinSize: new(int32),
5254
},
5355
}
56+
secondIG := &kops.InstanceGroup{
57+
ObjectMeta: metav1.ObjectMeta{Name: "nodes-us-east-2"},
58+
Spec: kops.InstanceGroupSpec{
59+
Role: kops.InstanceGroupRoleNode,
60+
Subnets: []string{"subnet-a"},
61+
MachineType: "g6-standard-2",
62+
Image: "linode/ubuntu24.04",
63+
MinSize: new(int32),
64+
},
65+
}
5466
context := newLinodeInstanceModelBuilderContext(cluster)
5567
addBootstrapPrerequisites(context)
68+
modelContext := contextModel(cluster, []*kops.InstanceGroup{ig, secondIG})
69+
modelContext.SSHPublicKeys = [][]byte{[]byte(testSSHPublicKey)}
5670

5771
networkBuilder := &NetworkModelBuilder{
58-
LinodeModelContext: &LinodeModelContext{KopsModelContext: contextModel(cluster, []*kops.InstanceGroup{ig})},
72+
LinodeModelContext: &LinodeModelContext{KopsModelContext: modelContext},
5973
Lifecycle: fi.LifecycleSync,
6074
}
6175
if err := networkBuilder.Build(context); err != nil {
6276
t.Fatalf("NetworkModelBuilder.Build returned error: %v", err)
6377
}
6478

65-
publicKey := fi.Resource(fi.NewStringResource(testSSHPublicKey))
66-
context.AddTask(&linodetasks.SSHKey{
67-
Name: new("example-k8s-local-default"),
68-
Lifecycle: fi.LifecycleSync,
69-
PublicKey: &publicKey,
70-
})
71-
7279
builder := &InstanceModelBuilder{
73-
LinodeModelContext: &LinodeModelContext{KopsModelContext: contextModel(cluster, []*kops.InstanceGroup{ig})},
80+
LinodeModelContext: &LinodeModelContext{KopsModelContext: modelContext},
7481
Lifecycle: fi.LifecycleSync,
82+
SSHKeyLifecycle: fi.LifecycleSync,
7583
BootstrapScriptBuilder: &model.BootstrapScriptBuilder{
7684
Lifecycle: fi.LifecycleSync,
77-
KopsModelContext: contextModel(cluster, []*kops.InstanceGroup{ig}),
85+
KopsModelContext: modelContext,
7886
},
7987
}
8088

@@ -101,6 +109,23 @@ func TestInstanceModelBuilderBuildUsesIGSubnetAndAllowsZeroSize(t *testing.T) {
101109
if got, want := len(instanceTask.AuthorizedKeys), 1; got != want {
102110
t.Fatalf("unexpected authorized key count: got %d, want %d", got, want)
103111
}
112+
secondInstanceTask := findInstanceTask(t, context, secondIG.Name)
113+
if got, want := len(secondInstanceTask.AuthorizedKeys), 1; got != want {
114+
t.Fatalf("unexpected second instance authorized key count: got %d, want %d", got, want)
115+
}
116+
if instanceTask.AuthorizedKeys[0] != secondInstanceTask.AuthorizedKeys[0] {
117+
t.Fatalf("expected instance groups to share the SSH key task")
118+
}
119+
120+
sshKeyTaskCount := 0
121+
for _, task := range context.Tasks {
122+
if _, ok := task.(*linodetasks.SSHKey); ok {
123+
sshKeyTaskCount++
124+
}
125+
}
126+
if got, want := sshKeyTaskCount, 1; got != want {
127+
t.Fatalf("unexpected SSH key task count: got %d, want %d", got, want)
128+
}
104129
}
105130

106131
func TestInstanceModelBuilderBuildRejectsMultipleIGSubnets(t *testing.T) {
@@ -190,13 +215,6 @@ func TestInstanceModelBuilderBuildDerivesPrivateSubnetInterfacePolicy(t *testing
190215
t.Fatalf("NetworkModelBuilder.Build returned error: %v", err)
191216
}
192217

193-
publicKey := fi.Resource(fi.NewStringResource(testSSHPublicKey))
194-
context.AddTask(&linodetasks.SSHKey{
195-
Name: new("example-k8s-local-default"),
196-
Lifecycle: fi.LifecycleSync,
197-
PublicKey: &publicKey,
198-
})
199-
200218
builder := &InstanceModelBuilder{
201219
LinodeModelContext: &LinodeModelContext{KopsModelContext: contextModel(cluster, []*kops.InstanceGroup{ig})},
202220
Lifecycle: fi.LifecycleSync,
@@ -216,6 +234,122 @@ func TestInstanceModelBuilderBuildDerivesPrivateSubnetInterfacePolicy(t *testing
216234
}
217235
}
218236

237+
func TestInstanceModelBuilderBuildSSHKeyTaskWithPublicKey(t *testing.T) {
238+
sshKeyName := "custom.ssh:key"
239+
cluster := &kops.Cluster{
240+
ObjectMeta: metav1.ObjectMeta{Name: "example.k8s.local"},
241+
Spec: kops.ClusterSpec{SSHKeyName: new(sshKeyName)},
242+
}
243+
b := &InstanceModelBuilder{
244+
LinodeModelContext: &LinodeModelContext{KopsModelContext: &model.KopsModelContext{
245+
IAMModelContext: iam.IAMModelContext{Cluster: cluster},
246+
SSHPublicKeys: [][]byte{[]byte(testSSHPublicKey)},
247+
}},
248+
SSHKeyLifecycle: fi.LifecycleSync,
249+
}
250+
context := &fi.CloudupModelBuilderContext{Tasks: map[string]fi.CloudupTask{}}
251+
252+
if _, err := b.buildSSHKeyTask(context); err != nil {
253+
t.Fatalf("buildSSHKeyTask returned error: %v", err)
254+
}
255+
256+
if got, want := len(context.Tasks), 1; got != want {
257+
t.Fatalf("unexpected task count: got %d, want %d", got, want)
258+
}
259+
260+
for _, task := range context.Tasks {
261+
sshKey, ok := task.(*linodetasks.SSHKey)
262+
if !ok {
263+
t.Fatalf("expected SSHKey task, got %T", task)
264+
}
265+
if got, want := fi.ValueOf(sshKey.Name), linode.NormalizeLinodeLabel(sshKeyName); got != want {
266+
t.Fatalf("unexpected SSH key name: got %q, want %q", got, want)
267+
}
268+
if sshKey.PublicKey == nil {
269+
t.Fatalf("expected SSH public key resource")
270+
}
271+
publicKey, err := fi.ResourceAsString(*sshKey.PublicKey)
272+
if err != nil {
273+
t.Fatalf("ResourceAsString returned error: %v", err)
274+
}
275+
if got, want := publicKey, testSSHPublicKey; got != want {
276+
t.Fatalf("unexpected SSH public key: got %q, want %q", got, want)
277+
}
278+
if got, want := sshKey.Lifecycle, fi.LifecycleSync; got != want {
279+
t.Fatalf("unexpected lifecycle: got %q, want %q", got, want)
280+
}
281+
}
282+
}
283+
284+
func TestInstanceModelBuilderBuildSSHKeyTaskWithExistingKeyName(t *testing.T) {
285+
sshKeyName := "existing.ssh:key"
286+
cluster := &kops.Cluster{
287+
ObjectMeta: metav1.ObjectMeta{Name: "example.k8s.local"},
288+
Spec: kops.ClusterSpec{SSHKeyName: new(sshKeyName)},
289+
}
290+
b := &InstanceModelBuilder{
291+
LinodeModelContext: &LinodeModelContext{KopsModelContext: &model.KopsModelContext{
292+
IAMModelContext: iam.IAMModelContext{Cluster: cluster},
293+
}},
294+
SSHKeyLifecycle: fi.LifecycleSync,
295+
}
296+
context := &fi.CloudupModelBuilderContext{Tasks: map[string]fi.CloudupTask{}}
297+
298+
if _, err := b.buildSSHKeyTask(context); err != nil {
299+
t.Fatalf("buildSSHKeyTask returned error: %v", err)
300+
}
301+
302+
if got, want := len(context.Tasks), 1; got != want {
303+
t.Fatalf("unexpected task count: got %d, want %d", got, want)
304+
}
305+
306+
for _, task := range context.Tasks {
307+
sshKey, ok := task.(*linodetasks.SSHKey)
308+
if !ok {
309+
t.Fatalf("expected SSHKey task, got %T", task)
310+
}
311+
if got, want := fi.ValueOf(sshKey.Name), linode.NormalizeLinodeLabel(sshKeyName); got != want {
312+
t.Fatalf("unexpected SSH key name: got %q, want %q", got, want)
313+
}
314+
if sshKey.PublicKey != nil {
315+
t.Fatalf("expected existing key task to omit public key data")
316+
}
317+
}
318+
}
319+
320+
func TestInstanceModelBuilderBuildSSHKeyTaskTruncatesLongGeneratedName(t *testing.T) {
321+
cluster := &kops.Cluster{
322+
ObjectMeta: metav1.ObjectMeta{Name: strings.Repeat("a", 32)},
323+
}
324+
b := &InstanceModelBuilder{
325+
LinodeModelContext: &LinodeModelContext{KopsModelContext: &model.KopsModelContext{
326+
IAMModelContext: iam.IAMModelContext{Cluster: cluster},
327+
SSHPublicKeys: [][]byte{[]byte(testSSHPublicKey)},
328+
}},
329+
SSHKeyLifecycle: fi.LifecycleSync,
330+
}
331+
context := &fi.CloudupModelBuilderContext{Tasks: map[string]fi.CloudupTask{}}
332+
333+
if _, err := b.buildSSHKeyTask(context); err != nil {
334+
t.Fatalf("buildSSHKeyTask returned error: %v", err)
335+
}
336+
337+
prefix := linode.NormalizeLinodeLabel("kubernetes." + cluster.ObjectMeta.Name)
338+
for _, task := range context.Tasks {
339+
sshKey, ok := task.(*linodetasks.SSHKey)
340+
if !ok {
341+
t.Fatalf("expected SSHKey task, got %T", task)
342+
}
343+
name := fi.ValueOf(sshKey.Name)
344+
if got, want := len(name), 64; got != want {
345+
t.Fatalf("unexpected SSH key name length: got %d, want %d", got, want)
346+
}
347+
if !strings.HasPrefix(name, prefix+"-") {
348+
t.Fatalf("unexpected SSH key name prefix: got %q, want prefix %q", name, prefix+"-")
349+
}
350+
}
351+
}
352+
219353
func contextModel(cluster *kops.Cluster, instanceGroups []*kops.InstanceGroup) *model.KopsModelContext {
220354
return &model.KopsModelContext{
221355
IAMModelContext: iam.IAMModelContext{Cluster: cluster},

‎pkg/model/linodemodel/sshkey.go‎

Lines changed: 0 additions & 65 deletions
This file was deleted.

0 commit comments

Comments
 (0)