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  }