|
| 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