github.com/mirantis/virtlet@v1.5.2-0.20191204181327-1659b8a48e9b/pkg/metadata/types/annotations.go (about)

     1  /*
     2  Copyright 2018 Mirantis
     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 types
    18  
    19  import (
    20  	"encoding/base64"
    21  	"fmt"
    22  	"strconv"
    23  	"strings"
    24  
    25  	"github.com/ghodss/yaml"
    26  	libvirtxml "github.com/libvirt/libvirt-go-xml"
    27  	uuid "github.com/nu7hatch/gouuid"
    28  	"k8s.io/apimachinery/pkg/api/resource"
    29  
    30  	"github.com/Mirantis/virtlet/pkg/utils"
    31  )
    32  
    33  const (
    34  	maxVCPUCount                      = 255
    35  	vcpuCountAnnotationKeyName        = "VirtletVCPUCount"
    36  	diskDriverKeyName                 = "VirtletDiskDriver"
    37  	cloudInitMetaDataKeyName          = "VirtletCloudInitMetaData"
    38  	cloudInitUserDataOverwriteKeyName = "VirtletCloudInitUserDataOverwrite"
    39  	cloudInitUserDataKeyName          = "VirtletCloudInitUserData"
    40  	cloudInitUserDataScriptKeyName    = "VirtletCloudInitUserDataScript"
    41  	cloudInitImageType                = "VirtletCloudInitImageType"
    42  	cpuModel                          = "VirtletCPUModel"
    43  	rootVolumeSizeKeyName             = "VirtletRootVolumeSize"
    44  	libvirtCPUSetting                 = "VirtletLibvirtCPUSetting"
    45  	sshKeysKeyName                    = "VirtletSSHKeys"
    46  	chown9pfsMountsKeyName            = "VirtletChown9pfsMounts"
    47  	systemUUIDKeyName                 = "VirtletSystemUUID"
    48  	forceDHCPNetworkConfigKeyName     = "VirtletForceDHCPNetworkConfig"
    49  	// CloudInitUserDataSourceKeyName is the name of user data source key in the pod annotations.
    50  	CloudInitUserDataSourceKeyName = "VirtletCloudInitUserDataSource"
    51  	// SSHKeySourceKeyName is the name of ssh key source key in the pod annotations.
    52  	SSHKeySourceKeyName = "VirtletSSHKeySource"
    53  
    54  	cloudInitUserDataSourceKeyKeyName      = "VirtletCloudInitUserDataSourceKey"
    55  	cloudInitUserDataSourceEncodingKeyName = "VirtletCloudInitUserDataSourceEncoding"
    56  
    57  	// FilesFromDSKeyName is the name of data source key in the pod annotations
    58  	// for the files to be injected into the rootfs.
    59  	FilesFromDSKeyName = "VirtletFilesFromDataSource"
    60  )
    61  
    62  // CloudInitImageType specifies the image type used for cloud-init
    63  type CloudInitImageType string
    64  
    65  // CPUModelType specifies cpu model in libvirt domain definition
    66  type CPUModelType string
    67  
    68  const (
    69  	// CloudInitImageTypeNoCloud specified nocloud cloud-init image type.
    70  	CloudInitImageTypeNoCloud CloudInitImageType = "nocloud"
    71  	// CloudInitImageTypeConfigDrive specified configdrive cloud-init image type.
    72  	CloudInitImageTypeConfigDrive CloudInitImageType = "configdrive"
    73  	// CPUModelHostModel specifies cpu model needed for nested virtualization
    74  	CPUModelHostModel = "host-model"
    75  )
    76  
    77  // DiskDriverName specifies disk driver name supported by Virtlet.
    78  type DiskDriverName string
    79  
    80  const (
    81  	// DiskDriverVirtio specifies virtio disk driver.
    82  	DiskDriverVirtio DiskDriverName = "virtio"
    83  	// DiskDriverScsi specifies scsi disk driver.
    84  	DiskDriverScsi DiskDriverName = "scsi"
    85  )
    86  
    87  // VirtletAnnotations contains parsed values for pod annotations supported
    88  // by Virtlet.
    89  type VirtletAnnotations struct {
    90  	// Number of virtual CPUs.
    91  	VCPUCount int
    92  	// CPU model.
    93  	CPUModel CPUModelType
    94  	// Cloud-Init image type to use.
    95  	CDImageType CloudInitImageType
    96  	// Cloud-Init metadata.
    97  	MetaData map[string]interface{}
    98  	// Cloud-Init userdata
    99  	UserData map[string]interface{}
   100  	// True if the userdata is overridden.
   101  	UserDataOverwrite bool
   102  	// UserDataScript specifies the script to be used as userdata.
   103  	UserDataScript string
   104  	// SSHKets specifies ssh public keys to use.
   105  	SSHKeys []string
   106  	// DiskDriver specifies the disk driver to use.
   107  	DiskDriver DiskDriverName
   108  	// CPUSetting directly specifies the cpu to use for libvirt.
   109  	CPUSetting *libvirtxml.DomainCPU
   110  	// Root volume size in bytes. Defaults to 0 which means using
   111  	// the size of QCOW2 image). If the value is less then the
   112  	// size of the QCOW2 image, the size of the QCOW2 image is
   113  	// used instead.
   114  	RootVolumeSize int64
   115  	// VirtletChown9pfsMounts indicates if chown is enabled for 9pfs mounts.
   116  	VirtletChown9pfsMounts bool
   117  	// InjectedFiles specifies the files to be injected into VM's
   118  	// rootfs before booting the VM.
   119  	InjectedFiles map[string][]byte
   120  	// SystemUUID specifies fixed SMBIOS UUID to be used for the domain.
   121  	// If not set, the SMBIOS UUID will be automatically generated from the Pod ID.
   122  	SystemUUID *uuid.UUID
   123  	// ForceDHCPNetworkConfig prevents Virtlet from using Cloud-Init based network
   124  	// configuration and makes it only provide DHCP. Note that this will
   125  	// not work for multi-CNI configuration.
   126  	ForceDHCPNetworkConfig bool
   127  }
   128  
   129  // ExternalDataLoader is used to load extra pod data from
   130  // Kubernetes ConfigMaps and secrets.
   131  type ExternalDataLoader interface {
   132  	// LoadCloudInitData loads cloud-init userdata and ssh keys
   133  	// from the data sources specified in the pod annotations.
   134  	LoadCloudInitData(va *VirtletAnnotations, namespace string, podAnnotations map[string]string) error
   135  	// LoadFileMap loads a set of files from the data sources.
   136  	LoadFileMap(namespace, dsSpec string) (map[string][]byte, error)
   137  }
   138  
   139  var externalDataLoader ExternalDataLoader
   140  
   141  // SetExternalDataLoader sets the ExternalDataLoader to use
   142  func SetExternalDataLoader(loader ExternalDataLoader) {
   143  	externalDataLoader = loader
   144  }
   145  
   146  // GetExternalDataLoader returns the current ExternalDataLoader
   147  func GetExternalDataLoader() ExternalDataLoader {
   148  	return externalDataLoader
   149  }
   150  
   151  func (va *VirtletAnnotations) applyDefaults() {
   152  	if va.VCPUCount <= 0 {
   153  		va.VCPUCount = 1
   154  	}
   155  
   156  	if va.DiskDriver == "" {
   157  		va.DiskDriver = DiskDriverScsi
   158  	}
   159  
   160  	if va.CDImageType == "" {
   161  		va.CDImageType = CloudInitImageTypeNoCloud
   162  	}
   163  }
   164  
   165  func (va *VirtletAnnotations) validate() error {
   166  	var errs []string
   167  	if va.VCPUCount > maxVCPUCount {
   168  		errs = append(errs, fmt.Sprintf("vcpu count %d too big, max is %d", va.VCPUCount, maxVCPUCount))
   169  	}
   170  
   171  	if va.DiskDriver != DiskDriverVirtio && va.DiskDriver != DiskDriverScsi {
   172  		errs = append(errs, fmt.Sprintf("bad disk driver %q. Must be either %q or %q", va.DiskDriver, DiskDriverVirtio, DiskDriverScsi))
   173  	}
   174  
   175  	if va.CDImageType != CloudInitImageTypeNoCloud && va.CDImageType != CloudInitImageTypeConfigDrive {
   176  		errs = append(errs, fmt.Sprintf("unknown config image type %q. Must be either %q or %q", va.CDImageType, CloudInitImageTypeNoCloud, CloudInitImageTypeConfigDrive))
   177  	}
   178  
   179  	if va.CPUModel != "" && va.CPUModel != CPUModelHostModel {
   180  		errs = append(errs, fmt.Sprintf("unknown cpu model type %q. Must be empty or %q", va.CPUModel, CPUModelHostModel))
   181  	}
   182  
   183  	if errs != nil {
   184  		return fmt.Errorf("bad virtlet annotations. Errors:\n%s", strings.Join(errs, "\n"))
   185  	}
   186  
   187  	return nil
   188  }
   189  
   190  func loadAnnotations(ns string, podAnnotations map[string]string) (*VirtletAnnotations, error) {
   191  	var va VirtletAnnotations
   192  	if err := va.parsePodAnnotations(ns, podAnnotations); err != nil {
   193  		return nil, err
   194  	}
   195  	va.applyDefaults()
   196  	if err := va.validate(); err != nil {
   197  		return nil, err
   198  	}
   199  	return &va, nil
   200  }
   201  
   202  func (va *VirtletAnnotations) parsePodAnnotations(ns string, podAnnotations map[string]string) error {
   203  	if cpuSettingStr, found := podAnnotations[libvirtCPUSetting]; found {
   204  		var cpuSetting libvirtxml.DomainCPU
   205  		if err := yaml.Unmarshal([]byte(cpuSettingStr), &cpuSetting); err != nil {
   206  			return err
   207  		}
   208  		va.CPUSetting = &cpuSetting
   209  	}
   210  
   211  	if cpuModelStr, found := podAnnotations[cpuModel]; found {
   212  		va.CPUModel = CPUModelType(cpuModelStr)
   213  	}
   214  
   215  	if podAnnotations[cloudInitUserDataOverwriteKeyName] == "true" {
   216  		va.UserDataOverwrite = true
   217  	}
   218  	if externalDataLoader != nil {
   219  		if err := externalDataLoader.LoadCloudInitData(va, ns, podAnnotations); err != nil {
   220  			return fmt.Errorf("error loading data via external user data loader: %v", err)
   221  		}
   222  	}
   223  
   224  	if filesFromDSstr, found := podAnnotations[FilesFromDSKeyName]; found && externalDataLoader != nil {
   225  		var err error
   226  		va.InjectedFiles, err = externalDataLoader.LoadFileMap(ns, filesFromDSstr)
   227  		if err != nil {
   228  			return fmt.Errorf("error loading data source %q as a file map: %v",
   229  				filesFromDSstr, err)
   230  		}
   231  	}
   232  
   233  	if vcpuCountStr, found := podAnnotations[vcpuCountAnnotationKeyName]; found {
   234  		var err error
   235  		if va.VCPUCount, err = strconv.Atoi(vcpuCountStr); err != nil {
   236  			return fmt.Errorf("error parsing cpu count for VM pod: %q: %v", vcpuCountStr, err)
   237  		}
   238  	}
   239  
   240  	if metaDataStr, found := podAnnotations[cloudInitMetaDataKeyName]; found {
   241  		if err := yaml.Unmarshal([]byte(metaDataStr), &va.MetaData); err != nil {
   242  			return fmt.Errorf("failed to unmarshal cloud-init metadata: %v", err)
   243  		}
   244  	}
   245  
   246  	if userDataStr, found := podAnnotations[cloudInitUserDataKeyName]; found {
   247  		var userData map[string]interface{}
   248  		if err := yaml.Unmarshal([]byte(userDataStr), &userData); err != nil {
   249  			return fmt.Errorf("failed to unmarshal cloud-init userdata: %v", err)
   250  		}
   251  		if va.UserDataOverwrite {
   252  			va.UserData = userData
   253  		} else {
   254  			va.UserData = utils.Merge(va.UserData, userData).(map[string]interface{})
   255  		}
   256  	}
   257  
   258  	va.UserDataScript = podAnnotations[cloudInitUserDataScriptKeyName]
   259  
   260  	encoding := "plain"
   261  	if encodingStr, found := podAnnotations[cloudInitUserDataSourceEncodingKeyName]; found {
   262  		encoding = encodingStr
   263  	}
   264  	if key, found := podAnnotations[cloudInitUserDataSourceKeyKeyName]; found {
   265  		data, found := va.UserData[key]
   266  		if !found {
   267  			return fmt.Errorf("user-data script source not found under the key %q", key)
   268  		}
   269  
   270  		dataStr, ok := data.(string)
   271  		if !ok {
   272  			return fmt.Errorf("failed to read user-data script source from the key %q", key)
   273  		}
   274  
   275  		switch encoding {
   276  		case "plain":
   277  			va.UserDataScript = dataStr
   278  		case "base64":
   279  			ud, err := base64.StdEncoding.DecodeString(dataStr)
   280  			if err != nil {
   281  				return fmt.Errorf("failed to decode the base64-encoded user-data script: %v", err)
   282  			}
   283  			va.UserDataScript = string(ud)
   284  		default:
   285  			return fmt.Errorf("failed to decode the user-data script: unknown encoding %q", encoding)
   286  		}
   287  	}
   288  
   289  	if sshKeysStr, found := podAnnotations[sshKeysKeyName]; found {
   290  		if va.UserDataOverwrite {
   291  			va.SSHKeys = nil
   292  		}
   293  		keys := strings.Split(sshKeysStr, "\n")
   294  		for _, k := range keys {
   295  			k = strings.TrimSpace(k)
   296  			if k != "" {
   297  				va.SSHKeys = append(va.SSHKeys, k)
   298  			}
   299  		}
   300  	}
   301  
   302  	va.CDImageType = CloudInitImageType(strings.ToLower(podAnnotations[cloudInitImageType]))
   303  	va.DiskDriver = DiskDriverName(podAnnotations[diskDriverKeyName])
   304  
   305  	if rootVolumeSizeStr, found := podAnnotations[rootVolumeSizeKeyName]; found {
   306  		if q, err := resource.ParseQuantity(rootVolumeSizeStr); err != nil {
   307  			return fmt.Errorf("error parsing the root volume size for VM pod: %q: %v", rootVolumeSizeStr, err)
   308  		} else if size, ok := q.AsInt64(); ok {
   309  			va.RootVolumeSize = size
   310  		} else {
   311  			return fmt.Errorf("bad root volume size %q", rootVolumeSizeStr)
   312  		}
   313  	}
   314  
   315  	if podAnnotations[chown9pfsMountsKeyName] == "true" {
   316  		va.VirtletChown9pfsMounts = true
   317  	}
   318  
   319  	if systemUUIDStr, found := podAnnotations[systemUUIDKeyName]; found {
   320  		var err error
   321  		if va.SystemUUID, err = uuid.ParseHex(systemUUIDStr); err != nil {
   322  			return fmt.Errorf("failed to parse %q as a UUID: %v", systemUUIDStr, err)
   323  		}
   324  	}
   325  
   326  	if podAnnotations[chown9pfsMountsKeyName] == "true" {
   327  		va.VirtletChown9pfsMounts = true
   328  	}
   329  
   330  	if podAnnotations[forceDHCPNetworkConfigKeyName] == "true" {
   331  		va.ForceDHCPNetworkConfig = true
   332  	}
   333  
   334  	return nil
   335  }