Skip to content

Commit 2f8064f

Browse files
refactor cloud-init
Signed-off-by: Nikita Korolev <nikita.korolev@flant.com>
1 parent f9065c3 commit 2f8064f

4 files changed

Lines changed: 291 additions & 81 deletions

File tree

test/e2e/go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/deckhouse/virtualization/api v0.0.0-20240923080356-bb5809dba578
1010
github.com/onsi/ginkgo/v2 v2.23.3
1111
github.com/onsi/gomega v1.37.0
12+
github.com/stretchr/testify v1.11.1
1213
gopkg.in/yaml.v3 v3.0.1
1314
k8s.io/api v0.34.2
1415
k8s.io/apimachinery v0.34.2
@@ -57,6 +58,7 @@ require (
5758
github.com/openshift/custom-resource-status v1.1.2 // indirect
5859
github.com/peterbourgon/diskv v2.0.1+incompatible // indirect
5960
github.com/pkg/errors v0.9.1 // indirect
61+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
6062
github.com/spf13/cobra v1.9.1 // indirect
6163
github.com/spf13/pflag v1.0.7 // indirect
6264
github.com/x448/float16 v0.8.4 // indirect
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
Copyright 2025 Flant JSC
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 object
18+
19+
import (
20+
"fmt"
21+
22+
"k8s.io/utils/ptr"
23+
"sigs.k8s.io/yaml"
24+
)
25+
26+
// CloudConfig mirrors the cloud-init cloud-config YAML schema.
27+
// Only the keys used by e2e tests are included.
28+
// See https://cloudinit.readthedocs.io/en/latest/reference/modules.html
29+
type CloudConfig struct {
30+
PackageUpdate bool `json:"package_update,omitempty"`
31+
Packages []string `json:"packages,omitempty"`
32+
WriteFiles []WriteFile `json:"write_files,omitempty"`
33+
Users []CloudConfigUser `json:"users,omitempty"`
34+
Runcmd []string `json:"runcmd,omitempty"`
35+
SSHPwauth *bool `json:"ssh_pwauth,omitempty"`
36+
}
37+
38+
type CloudConfigUser struct {
39+
Name string `json:"name"`
40+
Passwd string `json:"passwd,omitempty"`
41+
Shell string `json:"shell,omitempty"`
42+
Sudo string `json:"sudo,omitempty"`
43+
LockPasswd *bool `json:"lock_passwd,omitempty"`
44+
SSHAuthorizedKeys []string `json:"ssh_authorized_keys,omitempty"`
45+
}
46+
47+
type WriteFile struct {
48+
Path string `json:"path"`
49+
Permissions string `json:"permissions,omitempty"`
50+
Content string `json:"content,omitempty"`
51+
Append bool `json:"append,omitempty"`
52+
}
53+
54+
const defaultSSHPublicKey = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxcXHmwaGnJ8scJaEN5RzklBPZpVSic4GdaAsKjQoeA your_email@example.com"
55+
56+
// DefaultCloudUser returns the standard e2e test user (cloud/cloud) with SSH key.
57+
func DefaultCloudUser() CloudConfigUser {
58+
return CloudConfigUser{
59+
Name: DefaultUser,
60+
Passwd: "$6$rounds=4096$vln/.aPHBOI7BMYR$bBMkqQvuGs5Gyd/1H5DP4m9HjQSy.kgrxpaGEHwkX7KEFV8BS.HZWPitAtZ2Vd8ZqIZRqmlykRCagTgPejt1i.",
61+
Shell: "/bin/bash",
62+
Sudo: "ALL=(ALL) NOPASSWD:ALL",
63+
LockPasswd: ptr.To(false),
64+
SSHAuthorizedKeys: []string{defaultSSHPublicKey},
65+
}
66+
}
67+
68+
var basePackages = []string{
69+
"qemu-guest-agent", "curl", "bash", "sudo", "util-linux", "iperf3", "jq",
70+
}
71+
72+
// Render serializes the CloudConfig to a valid cloud-init user-data string
73+
// with the required #cloud-config header.
74+
func (c CloudConfig) Render() string {
75+
data, err := yaml.Marshal(c)
76+
if err != nil {
77+
panic(fmt.Sprintf("cloud-config marshal: %v", err))
78+
}
79+
return "#cloud-config\n" + string(data)
80+
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
/*
2+
Copyright 2025 Flant JSC
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 object
18+
19+
import (
20+
"strings"
21+
"testing"
22+
23+
"github.com/stretchr/testify/assert"
24+
"github.com/stretchr/testify/require"
25+
"sigs.k8s.io/yaml"
26+
)
27+
28+
func TestCloudConfigRender(t *testing.T) {
29+
tests := []struct {
30+
name string
31+
rendered string
32+
}{
33+
{"AlpineCloudInit", AlpineCloudInit},
34+
{"UbuntuCloudInit", UbuntuCloudInit},
35+
{"PerfCloudInit", PerfCloudInit},
36+
}
37+
38+
for _, tt := range tests {
39+
t.Run(tt.name, func(t *testing.T) {
40+
require.True(t, strings.HasPrefix(tt.rendered, "#cloud-config\n"),
41+
"cloud-init must start with #cloud-config header")
42+
43+
var parsed map[string]interface{}
44+
err := yaml.Unmarshal([]byte(tt.rendered), &parsed)
45+
require.NoError(t, err, "cloud-init must be valid YAML")
46+
47+
assert.Equal(t, true, parsed["package_update"])
48+
49+
users, ok := parsed["users"].([]interface{})
50+
require.True(t, ok, "users must be a list")
51+
require.Len(t, users, 1)
52+
53+
user := users[0].(map[string]interface{})
54+
assert.Equal(t, DefaultUser, user["name"])
55+
assert.Equal(t, false, user["lock_passwd"])
56+
57+
keys, ok := user["ssh_authorized_keys"].([]interface{})
58+
require.True(t, ok)
59+
require.Len(t, keys, 1)
60+
61+
runcmd, ok := parsed["runcmd"].([]interface{})
62+
require.True(t, ok, "runcmd must be a list")
63+
assert.NotEmpty(t, runcmd)
64+
})
65+
}
66+
}
67+
68+
func TestPerfCloudInitHasWriteFiles(t *testing.T) {
69+
var parsed map[string]interface{}
70+
err := yaml.Unmarshal([]byte(PerfCloudInit), &parsed)
71+
require.NoError(t, err)
72+
73+
writeFiles, ok := parsed["write_files"].([]interface{})
74+
require.True(t, ok, "PerfCloudInit must have write_files")
75+
require.Len(t, writeFiles, 1)
76+
77+
wf := writeFiles[0].(map[string]interface{})
78+
assert.Equal(t, "/usr/scripts/iperf3.sh", wf["path"])
79+
assert.Equal(t, "0755", wf["permissions"])
80+
81+
content, ok := wf["content"].(string)
82+
require.True(t, ok)
83+
assert.Contains(t, content, "#!/bin/bash")
84+
assert.Contains(t, content, "iperf3")
85+
}
86+
87+
func TestPerfCloudInitGolden(t *testing.T) {
88+
expected := `#cloud-config
89+
package_update: true
90+
packages:
91+
- qemu-guest-agent
92+
- curl
93+
- bash
94+
- sudo
95+
- util-linux
96+
- iperf3
97+
- jq
98+
- iputils
99+
runcmd:
100+
- /usr/scripts/iperf3.sh
101+
- rc-update add qemu-guest-agent && rc-service qemu-guest-agent start
102+
- rc-update add iperf3 && rc-service iperf3 start
103+
- rc-update add sshd && rc-service sshd start
104+
users:
105+
- lock_passwd: false
106+
name: cloud
107+
passwd: $6$rounds=4096$vln/.aPHBOI7BMYR$bBMkqQvuGs5Gyd/1H5DP4m9HjQSy.kgrxpaGEHwkX7KEFV8BS.HZWPitAtZ2Vd8ZqIZRqmlykRCagTgPejt1i.
108+
shell: /bin/bash
109+
ssh_authorized_keys:
110+
- ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIFxcXHmwaGnJ8scJaEN5RzklBPZpVSic4GdaAsKjQoeA
111+
your_email@example.com
112+
sudo: ALL=(ALL) NOPASSWD:ALL
113+
write_files:
114+
- content: |
115+
#!/bin/bash
116+
cat > /etc/init.d/iperf3 <<-"EOF"
117+
#!/sbin/openrc-run
118+
119+
name="iperf3"
120+
description="iperf3 server"
121+
command="/usr/bin/iperf3"
122+
command_args="-s"
123+
pidfile="/run/${name}.pid"
124+
supervisor="supervise-daemon"
125+
supervise_daemon_args="--respawn-delay 2 --stdout /var/log/iperf3.log --stderr /var/log/iperf3.log"
126+
127+
depend() {
128+
need net
129+
}
130+
131+
start_pre() {
132+
checkpath --directory --owner root:root --mode 0755 /run
133+
touch /var/log/iperf3.log
134+
chmod 644 /var/log/iperf3.log
135+
}
136+
137+
stop_post() {
138+
logger -t iperf3 "Stopped by $(whoami) at $(date)"
139+
rm -f "$pidfile"
140+
}
141+
EOF
142+
chmod +x /etc/init.d/iperf3
143+
rc-update add iperf3 default
144+
path: /usr/scripts/iperf3.sh
145+
permissions: "0755"
146+
`
147+
148+
assert.Equal(t, expected, PerfCloudInit)
149+
}

0 commit comments

Comments
 (0)