sigs.k8s.io/cluster-api@v1.7.1/bootstrap/kubeadm/internal/ignition/clc/clc.go (about) 1 /* 2 Copyright 2021 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 clc generates bootstrap data in Ignition format using Container Linux Config Transpiler. 18 // 19 // CLC configuration defined in this package will run kubeadm command by creating a /etc/kubeadm.sh script 20 // file containing both pre and post kubeadm commands as well as the kubeadm command itself. 21 // 22 // /etc/kubeadm.sh script will be executed using kubeadm.service systemd unit, which will only happen 23 // if /etc/kubeadm.yml file exists, which ensures the script will run only once, as by the end of the 24 // script, /etc/kubeadm.yml is moved to /tmp filesystem, so it gets automatically cleaned up after a 25 // reboot. This is to align the implementation with cloud-init, which places kubeadm configuration in 26 // /tmp directory directly, which is not possible with Ignition. 27 // 28 // /etc/kubeadm.yml file contains generated kubeadm configuration and can be customized using pre kubeadm 29 // commands if needed, as a replacement for Jinja templates supported by cloud-init, for example 30 // using 'envsubst' or 'sed'. 31 // 32 // To override the behavior of kubeadm.service unit, one should create an override drop-in 33 // using AdditionalConfig field. Data from this field takes precedence and will be merged with 34 // configuration generated by the bootstrap provider, overriding already defined fields following the 35 // merge strategy described in https://coreos.github.io/ignition/operator-notes/#config-merging. 36 package clc 37 38 import ( 39 "bytes" 40 "encoding/json" 41 "fmt" 42 "strings" 43 "text/template" 44 45 clct "github.com/flatcar/container-linux-config-transpiler/config" 46 ignition "github.com/flatcar/ignition/config/v2_3" 47 ignitionTypes "github.com/flatcar/ignition/config/v2_3/types" 48 "github.com/pkg/errors" 49 50 bootstrapv1 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/api/v1beta1" 51 "sigs.k8s.io/cluster-api/bootstrap/kubeadm/internal/cloudinit" 52 ) 53 54 const ( 55 clcTemplate = `--- 56 {{- if .Users }} 57 passwd: 58 users: 59 {{- range .Users }} 60 - name: {{ .Name }} 61 {{- with .Gecos }} 62 gecos: {{ . }} 63 {{- end }} 64 {{- if .Groups }} 65 groups: 66 {{- range Split .Groups ", " }} 67 - {{ . }} 68 {{- end }} 69 {{- end }} 70 {{- with .HomeDir }} 71 home_dir: {{ . }} 72 {{- end }} 73 {{- with .Shell }} 74 shell: {{ . }} 75 {{- end }} 76 {{- with .Passwd }} 77 password_hash: {{ . }} 78 {{- end }} 79 {{- with .PrimaryGroup }} 80 primary_group: {{ . }} 81 {{- end }} 82 {{- if .SSHAuthorizedKeys }} 83 ssh_authorized_keys: 84 {{- range .SSHAuthorizedKeys }} 85 - {{ . }} 86 {{- end }} 87 {{- end }} 88 {{- end }} 89 {{- end }} 90 systemd: 91 units: 92 - name: kubeadm.service 93 enabled: true 94 contents: | 95 [Unit] 96 Description=kubeadm 97 # Run only once. After successful run, this file is moved to /tmp/. 98 ConditionPathExists=/etc/kubeadm.yml 99 After=network.target 100 [Service] 101 # To not restart the unit when it exits, as it is expected. 102 Type=oneshot 103 ExecStart=/etc/kubeadm.sh 104 [Install] 105 WantedBy=multi-user.target 106 {{- if .NTP }}{{ if .NTP.Enabled }} 107 - name: ntpd.service 108 enabled: true 109 {{- end }}{{- end }} 110 {{- range .Mounts }} 111 {{- $label := index . 0 }} 112 {{- $mountpoint := index . 1 }} 113 {{- $disk := index $.FilesystemDevicesByLabel $label }} 114 {{- $mountOptions := slice . 2 }} 115 - name: {{ $mountpoint | MountpointName }}.mount 116 enabled: true 117 contents: | 118 [Unit] 119 Description = Mount {{ $label }} 120 121 [Mount] 122 What={{ $disk }} 123 Where={{ $mountpoint }} 124 Options={{ Join $mountOptions "," }} 125 126 [Install] 127 WantedBy=multi-user.target 128 {{- end }} 129 storage: 130 {{- if .DiskSetup }}{{- if .DiskSetup.Partitions }} 131 disks: 132 {{- range .DiskSetup.Partitions }} 133 - device: {{ .Device }} 134 {{- with .Overwrite }} 135 wipe_table: {{ . }} 136 {{- end }} 137 {{- if .Layout }} 138 partitions: 139 - {} 140 {{- end }} 141 {{- end }} 142 {{- end }}{{- end }} 143 {{- if .DiskSetup }}{{- if .DiskSetup.Filesystems }} 144 filesystems: 145 {{- range .DiskSetup.Filesystems }} 146 - name: {{ .Label }} 147 mount: 148 device: {{ .Device }} 149 format: {{ .Filesystem }} 150 wipe_filesystem: {{ .Overwrite }} 151 label: {{ .Label }} 152 {{- if .ExtraOpts }} 153 options: 154 {{- range .ExtraOpts }} 155 - {{ . }} 156 {{- end }} 157 {{- end }} 158 {{- end }} 159 {{- end }}{{- end }} 160 files: 161 {{- range .Users }} 162 {{- if .Sudo }} 163 - path: /etc/sudoers.d/{{ .Name }} 164 mode: 0600 165 contents: 166 inline: | 167 {{ .Name }} {{ .Sudo }} 168 {{- end }} 169 {{- end }} 170 {{- with .UsersWithPasswordAuth }} 171 - path: /etc/ssh/sshd_config 172 mode: 0600 173 contents: 174 inline: | 175 # Use most defaults for sshd configuration. 176 Subsystem sftp internal-sftp 177 ClientAliveInterval 180 178 UseDNS no 179 UsePAM yes 180 PrintLastLog no # handled by PAM 181 PrintMotd no # handled by PAM 182 183 Match User {{ . }} 184 PasswordAuthentication yes 185 {{- end }} 186 {{- range .WriteFiles }} 187 - path: {{ .Path }} 188 {{- $owner := ParseOwner .Owner }} 189 {{ if $owner.User -}} 190 user: 191 name: {{ $owner.User }} 192 {{- end }} 193 {{ if $owner.Group -}} 194 group: 195 name: {{ $owner.Group }} 196 {{- end }} 197 # Owner 198 {{ if ne .Permissions "" -}} 199 mode: {{ .Permissions }} 200 {{ end -}} 201 contents: 202 {{ if eq .Encoding "base64" -}} 203 inline: !!binary | 204 {{- else -}} 205 inline: | 206 {{- end }} 207 {{ .Content | Indent 10 }} 208 {{- end }} 209 - path: /etc/kubeadm.sh 210 mode: 0700 211 contents: 212 inline: | 213 #!/bin/bash 214 set -e 215 {{ range .PreKubeadmCommands }} 216 {{ . | Indent 10 }} 217 {{- end }} 218 219 {{ .KubeadmCommand }} 220 mkdir -p /run/cluster-api && echo success > /run/cluster-api/bootstrap-success.complete 221 mv /etc/kubeadm.yml /tmp/ 222 {{range .PostKubeadmCommands }} 223 {{ . | Indent 10 }} 224 {{- end }} 225 - path: /etc/kubeadm.yml 226 mode: 0600 227 contents: 228 inline: | 229 --- 230 {{ .KubeadmConfig | Indent 10 }} 231 {{- if .NTP }}{{- if and .NTP.Enabled .NTP.Servers }} 232 - path: /etc/ntp.conf 233 mode: 0644 234 contents: 235 inline: | 236 # Common pool 237 {{- range .NTP.Servers }} 238 server {{ . }} 239 {{- end }} 240 241 # Warning: Using default NTP settings will leave your NTP 242 # server accessible to all hosts on the Internet. 243 244 # If you want to deny all machines (including your own) 245 # from accessing the NTP server, uncomment: 246 #restrict default ignore 247 248 # Default configuration: 249 # - Allow only time queries, at a limited rate, sending KoD when in excess. 250 # - Allow all local queries (IPv4, IPv6) 251 restrict default nomodify nopeer noquery notrap limited kod 252 restrict 127.0.0.1 253 restrict [::1] 254 {{- end }}{{- end }} 255 ` 256 ) 257 258 type render struct { 259 *cloudinit.BaseUserData 260 261 KubeadmConfig string 262 UsersWithPasswordAuth string 263 FilesystemDevicesByLabel map[string]string 264 } 265 266 func defaultTemplateFuncMap() template.FuncMap { 267 return template.FuncMap{ 268 "Indent": templateYAMLIndent, 269 "Split": strings.Split, 270 "Join": strings.Join, 271 "MountpointName": mountpointName, 272 "ParseOwner": parseOwner, 273 } 274 } 275 276 func mountpointName(name string) string { 277 return strings.TrimPrefix(strings.ReplaceAll(name, "/", "-"), "-") 278 } 279 280 func templateYAMLIndent(i int, input string) string { 281 split := strings.Split(input, "\n") 282 ident := "\n" + strings.Repeat(" ", i) 283 return strings.Join(split, ident) 284 } 285 286 type owner struct { 287 User *string 288 Group *string 289 } 290 291 func parseOwner(ownerRaw string) owner { 292 if ownerRaw == "" { 293 return owner{} 294 } 295 296 ownerSlice := strings.Split(ownerRaw, ":") 297 298 parseEntity := func(entity string) *string { 299 if entity == "" { 300 return nil 301 } 302 303 entityTrimmed := strings.TrimSpace(entity) 304 305 return &entityTrimmed 306 } 307 308 if len(ownerSlice) == 1 { 309 return owner{ 310 User: parseEntity(ownerSlice[0]), 311 } 312 } 313 314 return owner{ 315 User: parseEntity(ownerSlice[0]), 316 Group: parseEntity(ownerSlice[1]), 317 } 318 } 319 320 func renderCLC(input *cloudinit.BaseUserData, kubeadmConfig string) ([]byte, error) { 321 t := template.Must(template.New("template").Funcs(defaultTemplateFuncMap()).Parse(clcTemplate)) 322 323 usersWithPasswordAuth := []string{} 324 for _, user := range input.Users { 325 if user.LockPassword != nil && !*user.LockPassword { 326 usersWithPasswordAuth = append(usersWithPasswordAuth, user.Name) 327 } 328 } 329 330 filesystemDevicesByLabel := map[string]string{} 331 if input.DiskSetup != nil { 332 for _, filesystem := range input.DiskSetup.Filesystems { 333 filesystemDevicesByLabel[filesystem.Label] = filesystem.Device 334 } 335 } 336 337 data := render{ 338 BaseUserData: input, 339 KubeadmConfig: kubeadmConfig, 340 UsersWithPasswordAuth: strings.Join(usersWithPasswordAuth, ","), 341 FilesystemDevicesByLabel: filesystemDevicesByLabel, 342 } 343 344 var out bytes.Buffer 345 if err := t.Execute(&out, data); err != nil { 346 return nil, errors.Wrapf(err, "failed to render template") 347 } 348 349 return out.Bytes(), nil 350 } 351 352 // Render renders the provided user data and CLC snippets into Ignition config. 353 func Render(input *cloudinit.BaseUserData, clc *bootstrapv1.ContainerLinuxConfig, kubeadmConfig string) ([]byte, string, error) { 354 if input == nil { 355 return nil, "", errors.New("empty base user data") 356 } 357 358 clcBytes, err := renderCLC(input, kubeadmConfig) 359 if err != nil { 360 return nil, "", errors.Wrapf(err, "rendering CLC configuration") 361 } 362 363 userData, warnings, err := buildIgnitionConfig(clcBytes, clc) 364 if err != nil { 365 return nil, "", errors.Wrapf(err, "building Ignition config") 366 } 367 368 return userData, warnings, nil 369 } 370 371 func buildIgnitionConfig(baseCLC []byte, clc *bootstrapv1.ContainerLinuxConfig) ([]byte, string, error) { 372 // We control baseCLC config, so treat it as strict. 373 ign, _, err := clcToIgnition(baseCLC, true) 374 if err != nil { 375 return nil, "", errors.Wrapf(err, "converting generated CLC to Ignition") 376 } 377 378 var clcWarnings string 379 380 if clc != nil && clc.AdditionalConfig != "" { 381 additionalIgn, warnings, err := clcToIgnition([]byte(clc.AdditionalConfig), clc.Strict) 382 if err != nil { 383 return nil, "", errors.Wrapf(err, "converting additional CLC to Ignition") 384 } 385 386 clcWarnings = warnings 387 388 ign = ignition.Append(ign, additionalIgn) 389 } 390 391 userData, err := json.Marshal(&ign) 392 if err != nil { 393 return nil, "", errors.Wrapf(err, "marshaling generated Ignition config into JSON") 394 } 395 396 return userData, clcWarnings, nil 397 } 398 399 func clcToIgnition(data []byte, strict bool) (ignitionTypes.Config, string, error) { 400 clc, ast, reports := clct.Parse(data) 401 402 if (len(reports.Entries) > 0 && strict) || reports.IsFatal() { 403 return ignitionTypes.Config{}, "", fmt.Errorf("error parsing Container Linux Config: %v", reports.String()) 404 } 405 406 ign, report := clct.Convert(clc, "", ast) 407 if (len(report.Entries) > 0 && strict) || report.IsFatal() { 408 return ignitionTypes.Config{}, "", fmt.Errorf("error converting to Ignition: %v", report.String()) 409 } 410 411 reports.Merge(report) 412 413 return ign, reports.String(), nil 414 }