Skip to content

Commit ae15f9e

Browse files
Merge pull request #124 from alexander-demicev/ignition
Support ignition format for generating user data
2 parents d749975 + 244768f commit ae15f9e

9 files changed

Lines changed: 1944 additions & 9 deletions

File tree

.golangci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ run:
44
skip-files:
55
- "zz_generated.*\\.go$"
66
- "vendored_openapi\\.go$"
7+
- ".*_test\\.go$"
78
allow-parallel-runners: true
89
issues:
910
include:

bootstrap/internal/controllers/rke2config_controller.go

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ import (
4343

4444
bootstrapv1 "github.com/rancher-sandbox/cluster-api-provider-rke2/bootstrap/api/v1alpha1"
4545
"github.com/rancher-sandbox/cluster-api-provider-rke2/bootstrap/internal/cloudinit"
46+
"github.com/rancher-sandbox/cluster-api-provider-rke2/bootstrap/internal/ignition"
4647
controlplanev1 "github.com/rancher-sandbox/cluster-api-provider-rke2/controlplane/api/v1alpha1"
4748
"github.com/rancher-sandbox/cluster-api-provider-rke2/pkg/consts"
4849
"github.com/rancher-sandbox/cluster-api-provider-rke2/pkg/locking"
@@ -413,12 +414,23 @@ func (r *RKE2ConfigReconciler) handleClusterNotInitialized(ctx context.Context,
413414
Certificates: certificates,
414415
}
415416

416-
cloudInitData, err := cloudinit.NewInitControlPlane(cpinput)
417+
var userData []byte
418+
419+
switch scope.Config.Spec.AgentConfig.Format {
420+
case bootstrapv1.Ignition:
421+
userData, err = ignition.NewInitControlPlane(&ignition.ControlPlaneInput{
422+
ControlPlaneInput: cpinput,
423+
AdditionalIgnition: &scope.Config.Spec.AgentConfig.AdditionalUserData,
424+
})
425+
default:
426+
userData, err = cloudinit.NewInitControlPlane(cpinput)
427+
}
428+
417429
if err != nil {
418430
return ctrl.Result{}, err
419431
}
420432

421-
if err := r.storeBootstrapData(ctx, scope, cloudInitData); err != nil {
433+
if err := r.storeBootstrapData(ctx, scope, userData); err != nil {
422434
return ctrl.Result{}, err
423435
}
424436

@@ -577,12 +589,27 @@ func (r *RKE2ConfigReconciler) joinControlplane(ctx context.Context, scope *Scop
577589
},
578590
}
579591

580-
cloudInitData, err := cloudinit.NewJoinControlPlane(cpinput)
581592
if err != nil {
582593
return ctrl.Result{}, err
583594
}
584595

585-
if err := r.storeBootstrapData(ctx, scope, cloudInitData); err != nil {
596+
var userData []byte
597+
598+
switch scope.Config.Spec.AgentConfig.Format {
599+
case bootstrapv1.Ignition:
600+
userData, err = ignition.NewJoinControlPlane(&ignition.ControlPlaneInput{
601+
ControlPlaneInput: cpinput,
602+
AdditionalIgnition: &scope.Config.Spec.AgentConfig.AdditionalUserData,
603+
})
604+
default:
605+
userData, err = cloudinit.NewJoinControlPlane(cpinput)
606+
}
607+
608+
if err != nil {
609+
return ctrl.Result{}, err
610+
}
611+
612+
if err := r.storeBootstrapData(ctx, scope, userData); err != nil {
586613
return ctrl.Result{}, err
587614
}
588615

@@ -668,12 +695,23 @@ func (r *RKE2ConfigReconciler) joinWorker(ctx context.Context, scope *Scope) (re
668695
AdditionalCloudInit: scope.Config.Spec.AgentConfig.AdditionalUserData.Config,
669696
}
670697

671-
cloudInitData, err := cloudinit.NewJoinWorker(wkInput)
698+
var userData []byte
699+
700+
switch scope.Config.Spec.AgentConfig.Format {
701+
case bootstrapv1.Ignition:
702+
userData, err = ignition.NewJoinWorker(&ignition.JoinWorkerInput{
703+
BaseUserData: wkInput,
704+
AdditionalIgnition: &scope.Config.Spec.AgentConfig.AdditionalUserData,
705+
})
706+
default:
707+
userData, err = cloudinit.NewJoinWorker(wkInput)
708+
}
709+
672710
if err != nil {
673711
return ctrl.Result{}, err
674712
}
675713

676-
if err := r.storeBootstrapData(ctx, scope, cloudInitData); err != nil {
714+
if err := r.storeBootstrapData(ctx, scope, userData); err != nil {
677715
return ctrl.Result{}, err
678716
}
679717

@@ -740,7 +778,8 @@ func (r *RKE2ConfigReconciler) storeBootstrapData(ctx context.Context, scope *Sc
740778
},
741779
},
742780
Data: map[string][]byte{
743-
"value": data,
781+
"value": data,
782+
"format": []byte(scope.Config.Spec.AgentConfig.Format),
744783
},
745784
Type: clusterv1.ClusterSecretType,
746785
}
Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
/*
2+
Copyright 2023 SUSE.
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 clc
18+
19+
import (
20+
"bytes"
21+
"encoding/json"
22+
"fmt"
23+
"strings"
24+
"text/template"
25+
26+
clct "github.com/flatcar/container-linux-config-transpiler/config"
27+
ignition "github.com/flatcar/ignition/config/v2_3"
28+
ignitionTypes "github.com/flatcar/ignition/config/v2_3/types"
29+
"github.com/pkg/errors"
30+
31+
bootstrapv1 "github.com/rancher-sandbox/cluster-api-provider-rke2/bootstrap/api/v1alpha1"
32+
"github.com/rancher-sandbox/cluster-api-provider-rke2/bootstrap/internal/cloudinit"
33+
)
34+
35+
// The template contains configurations for two main sections: systemd units and storage files.
36+
// The first section defines two systemd units: rke2-install.service and ntpd.service.
37+
// The rke2-install.service unit is enabled and is executed only once during the boot process to run the /etc/rke2-install.sh script.
38+
// This script installs and deploys RKE2, and performs pre and post-installation commands.
39+
// The ntpd.service unit is enabled only if NTP servers are specified.
40+
// The second section defines storage files for the system. It creates a file at /etc/rke2-install.sh. If CISEnabled is set to true,
41+
// it runs an additional CIS script to enforce system security standards. If NTP servers are specified,
42+
// it creates an NTP configuration file at /etc/ntp.conf.
43+
const (
44+
clcTemplate = `---
45+
systemd:
46+
units:
47+
- name: rke2-install.service
48+
enabled: true
49+
contents: |
50+
[Unit]
51+
Description=rke2-install
52+
[Service]
53+
# To not restart the unit when it exits, as it is expected.
54+
Type=oneshot
55+
ExecStart=/etc/rke2-install.sh
56+
[Install]
57+
WantedBy=multi-user.target
58+
{{- if .NTPServers }}
59+
- name: ntpd.service
60+
enabled: true
61+
{{- end }}
62+
storage:
63+
files:
64+
- path: /etc/ssh/sshd_config
65+
mode: 0600
66+
contents:
67+
inline: |
68+
# Use most defaults for sshd configuration.
69+
Subsystem sftp internal-sftp
70+
ClientAliveInterval 180
71+
UseDNS no
72+
UsePAM yes
73+
PrintLastLog no # handled by PAM
74+
PrintMotd no # handled by PAM
75+
{{- range .WriteFiles }}
76+
- path: {{ .Path }}
77+
{{- $owner := ParseOwner .Owner }}
78+
{{ if $owner.User -}}
79+
user:
80+
name: {{ $owner.User }}
81+
{{- end }}
82+
{{ if $owner.Group -}}
83+
group:
84+
name: {{ $owner.Group }}
85+
{{- end }}
86+
# Owner
87+
{{ if ne .Permissions "" -}}
88+
mode: {{ .Permissions }}
89+
{{ end -}}
90+
contents:
91+
{{ if eq .Encoding "base64" -}}
92+
inline: !!binary |
93+
{{- else -}}
94+
inline: |
95+
{{- end }}
96+
{{ .Content | Indent 10 }}
97+
{{- end }}
98+
- path: /etc/rke2-install.sh
99+
mode: 0700
100+
contents:
101+
inline: |
102+
#!/bin/bash
103+
set -e
104+
{{ range .PreRKE2Commands }}
105+
{{ . | Indent 10 }}
106+
{{- end }}
107+
108+
{{- if .CISEnabled }}
109+
/opt/rke2-cis-script.sh
110+
{{ end }}
111+
112+
{{ range .DeployRKE2Commands }}
113+
{{ . | Indent 10 }}
114+
{{- end }}
115+
116+
mkdir -p /run/cluster-api && echo success > /run/cluster-api/bootstrap-success.complete
117+
{{range .PostRKE2Commands }}
118+
{{ . | Indent 10 }}
119+
{{- end }}
120+
{{- if .NTPServers }}
121+
- path: /etc/ntp.conf
122+
mode: 0644
123+
contents:
124+
inline: |
125+
# Common pool
126+
{{- range .NTPServers }}
127+
server {{ . }}
128+
{{- end }}
129+
130+
# Warning: Using default NTP settings will leave your NTP
131+
# server accessible to all hosts on the Internet.
132+
133+
# If you want to deny all machines (including your own)
134+
# from accessing the NTP server, uncomment:
135+
#restrict default ignore
136+
137+
# Default configuration:
138+
# - Allow only time queries, at a limited rate, sending KoD when in excess.
139+
# - Allow all local queries (IPv4, IPv6)
140+
restrict default nomodify nopeer noquery notrap limited kod
141+
restrict 127.0.0.1
142+
restrict [::1]
143+
{{- end }}
144+
`
145+
)
146+
147+
func defaultTemplateFuncMap() template.FuncMap {
148+
return template.FuncMap{
149+
"Indent": templateYAMLIndent,
150+
"ParseOwner": parseOwner,
151+
}
152+
}
153+
154+
func templateYAMLIndent(i int, input string) string {
155+
split := strings.Split(input, "\n")
156+
ident := "\n" + strings.Repeat(" ", i)
157+
158+
return strings.Join(split, ident)
159+
}
160+
161+
type owner struct {
162+
User *string
163+
Group *string
164+
}
165+
166+
func parseOwner(ownerRaw string) owner {
167+
if ownerRaw == "" {
168+
return owner{}
169+
}
170+
171+
ownerSlice := strings.Split(ownerRaw, ":")
172+
173+
parseEntity := func(entity string) *string {
174+
if entity == "" {
175+
return nil
176+
}
177+
178+
entityTrimmed := strings.TrimSpace(entity)
179+
180+
return &entityTrimmed
181+
}
182+
183+
if len(ownerSlice) == 1 {
184+
return owner{
185+
User: parseEntity(ownerSlice[0]),
186+
}
187+
}
188+
189+
return owner{
190+
User: parseEntity(ownerSlice[0]),
191+
Group: parseEntity(ownerSlice[1]),
192+
}
193+
}
194+
195+
func renderCLC(input *cloudinit.BaseUserData) ([]byte, error) {
196+
t := template.Must(template.New("template").Funcs(defaultTemplateFuncMap()).Parse(clcTemplate))
197+
198+
var out bytes.Buffer
199+
if err := t.Execute(&out, input); err != nil {
200+
return nil, errors.Wrapf(err, "failed to render template")
201+
}
202+
203+
return out.Bytes(), nil
204+
}
205+
206+
// Render renders the provided user data and CLC snippets into Ignition config.
207+
func Render(input *cloudinit.BaseUserData, additionalConfig *bootstrapv1.AdditionalUserData) ([]byte, error) {
208+
if input == nil {
209+
return nil, errors.New("empty base user data")
210+
}
211+
212+
clcBytes, err := renderCLC(input)
213+
if err != nil {
214+
return nil, errors.Wrapf(err, "rendering CLC configuration")
215+
}
216+
217+
userData, _, err := buildIgnitionConfig(clcBytes, additionalConfig)
218+
if err != nil {
219+
return nil, errors.Wrapf(err, "building Ignition config")
220+
}
221+
222+
return userData, nil
223+
}
224+
225+
func buildIgnitionConfig(baseCLC []byte, additionalConfig *bootstrapv1.AdditionalUserData) ([]byte, string, error) {
226+
// We control baseCLC config, so treat it as strict.
227+
ign, _, err := clcToIgnition(baseCLC, true)
228+
if err != nil {
229+
return nil, "", errors.Wrapf(err, "converting generated CLC to Ignition")
230+
}
231+
232+
var clcWarnings string
233+
234+
if additionalConfig != nil && additionalConfig.Config != "" {
235+
additionalIgn, warnings, err := clcToIgnition([]byte(additionalConfig.Config), additionalConfig.Strict)
236+
if err != nil {
237+
return nil, "", errors.Wrapf(err, "converting additional CLC to Ignition")
238+
}
239+
240+
clcWarnings = warnings
241+
242+
ign = ignition.Append(ign, additionalIgn)
243+
}
244+
245+
userData, err := json.Marshal(&ign)
246+
if err != nil {
247+
return nil, "", errors.Wrapf(err, "marshaling generated Ignition config into JSON")
248+
}
249+
250+
return userData, clcWarnings, nil
251+
}
252+
253+
func clcToIgnition(data []byte, strict bool) (ignitionTypes.Config, string, error) {
254+
clc, ast, reports := clct.Parse(data)
255+
256+
if (len(reports.Entries) > 0 && strict) || reports.IsFatal() {
257+
return ignitionTypes.Config{}, "", fmt.Errorf("error parsing Container Linux Config: %v", reports.String())
258+
}
259+
260+
ign, report := clct.Convert(clc, "", ast)
261+
if (len(report.Entries) > 0 && strict) || report.IsFatal() {
262+
return ignitionTypes.Config{}, "", fmt.Errorf("error converting to Ignition: %v", report.String())
263+
}
264+
265+
reports.Merge(report)
266+
267+
return ign, reports.String(), nil
268+
}

0 commit comments

Comments
 (0)