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 }