github.com/mirantis/virtlet@v1.5.2-0.20191204181327-1659b8a48e9b/pkg/libvirttools/cloudinit.go (about) 1 /* 2 Copyright 2017 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 libvirttools 18 19 import ( 20 "bytes" 21 "encoding/base64" 22 "encoding/json" 23 "fmt" 24 "io/ioutil" 25 "net" 26 "os" 27 "path" 28 "path/filepath" 29 "strconv" 30 "strings" 31 32 cnitypes "github.com/containernetworking/cni/pkg/types" 33 cnicurrent "github.com/containernetworking/cni/pkg/types/current" 34 "github.com/ghodss/yaml" 35 "github.com/golang/glog" 36 "github.com/kballard/go-shellquote" 37 libvirtxml "github.com/libvirt/libvirt-go-xml" 38 39 "github.com/Mirantis/virtlet/pkg/flexvolume" 40 "github.com/Mirantis/virtlet/pkg/fs" 41 "github.com/Mirantis/virtlet/pkg/metadata/types" 42 "github.com/Mirantis/virtlet/pkg/network" 43 "github.com/Mirantis/virtlet/pkg/utils" 44 ) 45 46 const ( 47 envFileLocation = "/etc/cloud/environment" 48 symlinkFileLocation = "/etc/cloud/symlink-devs.sh" 49 mountFileLocation = "/etc/cloud/mount-volumes.sh" 50 mountScriptSubst = "@virtlet-mount-script@" 51 cloudInitPerBootDir = "/var/lib/cloud/scripts/per-boot" 52 ) 53 54 // Note that in the templates below, we don't use shellquote (shq) on 55 // SysfsPath, because it *must* be expanded by the shell to work 56 // (it contains '*') 57 58 var linkStartupScriptTemplate = utils.NewShellTemplate( 59 "ln -s {{ shq .StartupScript }} /var/lib/cloud/scripts/per-boot/") 60 var linkBlockDeviceScriptTemplate = utils.NewShellTemplate( 61 "ln -fs /dev/`ls {{ .SysfsPath }}` {{ shq .DevicePath }}") 62 var mountDevScriptTemplate = utils.NewShellTemplate( 63 "if ! mountpoint {{ shq .ContainerPath }}; then " + 64 "mkdir -p {{ shq .ContainerPath }} && " + 65 "mount /dev/`ls {{ .SysfsPath }}`{{ .DevSuffix }} {{ .ContainerPath }}; " + 66 "fi") 67 var mountFSScriptTemplate = utils.NewShellTemplate( 68 "if ! mountpoint {{ shq .ContainerPath }}; then " + 69 "mkdir -p {{ shq .ContainerPath }} && " + 70 "mount -t 9p -o trans=virtio {{ shq .MountTag }} {{ shq .ContainerPath }}; " + 71 "fi") 72 73 // CloudInitGenerator provides a common part for Cloud Init ISO drive preparation 74 // for NoCloud and ConfigDrive volume sources. 75 type CloudInitGenerator struct { 76 config *types.VMConfig 77 isoDir string 78 } 79 80 // NewCloudInitGenerator returns new CloudInitGenerator. 81 func NewCloudInitGenerator(config *types.VMConfig, isoDir string) *CloudInitGenerator { 82 return &CloudInitGenerator{ 83 config: config, 84 isoDir: isoDir, 85 } 86 } 87 88 func (g *CloudInitGenerator) generateMetaData() ([]byte, error) { 89 m := map[string]interface{}{ 90 "instance-id": fmt.Sprintf("%s.%s", g.config.PodName, g.config.PodNamespace), 91 "local-hostname": g.config.PodName, 92 } 93 94 // TODO: get rid of this if. Use descriptor for cloud-init image types. 95 if g.config.ParsedAnnotations.CDImageType == types.CloudInitImageTypeConfigDrive { 96 m["uuid"] = m["instance-id"] 97 m["hostname"] = m["local-hostname"] 98 } 99 100 if len(g.config.ParsedAnnotations.SSHKeys) != 0 { 101 var keys []string 102 for _, key := range g.config.ParsedAnnotations.SSHKeys { 103 keys = append(keys, key) 104 } 105 m["public-keys"] = keys 106 } 107 108 for k, v := range g.config.ParsedAnnotations.MetaData { 109 m[k] = v 110 } 111 112 r, err := json.Marshal(m) 113 if err != nil { 114 return nil, fmt.Errorf("error marshaling meta-data: %v", err) 115 } 116 117 return r, nil 118 } 119 120 func (g *CloudInitGenerator) generateUserData(volumeMap diskPathMap) ([]byte, error) { 121 symlinkScript := g.generateSymlinkScript(volumeMap) 122 mounts, mountScript := g.generateMounts(volumeMap) 123 124 if userDataScript := g.config.ParsedAnnotations.UserDataScript; userDataScript != "" { 125 fullMountScript := "" 126 switch { 127 case mountScript != "" && symlinkScript != "": 128 fullMountScript = symlinkScript + "\n" + mountScript 129 case mountScript != "": 130 fullMountScript = mountScript 131 case symlinkScript != "": 132 fullMountScript = symlinkScript 133 } 134 return []byte(strings.Replace(userDataScript, mountScriptSubst, fullMountScript, -1)), nil 135 } 136 137 userData := make(map[string]interface{}) 138 for k, v := range g.config.ParsedAnnotations.UserData { 139 userData[k] = v 140 } 141 142 mounts = utils.Merge(userData["mounts"], mounts).([]interface{}) 143 if len(mounts) != 0 { 144 userData["mounts"] = g.fixMounts(volumeMap, mounts) 145 } 146 147 writeFilesUpdater := newWriteFilesUpdater(g.config.Mounts) 148 writeFilesUpdater.addSecrets() 149 writeFilesUpdater.addConfigMapEntries() 150 writeFilesUpdater.addFileLikeMounts() 151 if symlinkScript != "" { 152 writeFilesUpdater.addSymlinkScript(symlinkScript) 153 userData["runcmd"] = utils.Merge(userData["runcmd"], []string{ 154 shellquote.Join(symlinkFileLocation), 155 linkStartupScriptTemplate.MustExecuteToString(map[string]string{ 156 "StartupScript": symlinkFileLocation, 157 }), 158 }) 159 } 160 if mountScript != "" { 161 writeFilesUpdater.addMountScript(mountScript) 162 } 163 if envContent := g.generateEnvVarsContent(); envContent != "" { 164 writeFilesUpdater.addEnvironmentFile(envContent) 165 } 166 writeFilesUpdater.updateUserData(userData) 167 168 r := []byte{} 169 if len(userData) != 0 { 170 var err error 171 r, err = yaml.Marshal(userData) 172 if err != nil { 173 return nil, fmt.Errorf("error marshalling user-data: %v", err) 174 } 175 } 176 return []byte("#cloud-config\n" + string(r)), nil 177 } 178 179 func (g *CloudInitGenerator) generateNetworkConfiguration() ([]byte, error) { 180 if g.config.ParsedAnnotations.ForceDHCPNetworkConfig || g.config.RootVolumeDevice() != nil { 181 // Don't use cloud-init network config if asked not 182 // to do so. 183 // Also, we don't use network config with persistent 184 // rootfs for now because with some cloud-init 185 // implementations it's applied only once 186 return nil, nil 187 } 188 // TODO: get rid of this switch. Use descriptor for cloud-init image types. 189 switch g.config.ParsedAnnotations.CDImageType { 190 case types.CloudInitImageTypeNoCloud: 191 return g.generateNetworkConfigurationNoCloud() 192 case types.CloudInitImageTypeConfigDrive: 193 return g.generateNetworkConfigurationConfigDrive() 194 } 195 196 return nil, fmt.Errorf("unknown cloud-init config image type: %q", g.config.ParsedAnnotations.CDImageType) 197 } 198 199 func (g *CloudInitGenerator) generateNetworkConfigurationNoCloud() ([]byte, error) { 200 if g.config.ContainerSideNetwork == nil { 201 // This can only happen during integration tests 202 // where a dummy sandbox is used 203 return []byte("version: 1\n"), nil 204 } 205 cniResult := g.config.ContainerSideNetwork.Result 206 207 var config []map[string]interface{} 208 209 // physical interfaces 210 for i, iface := range cniResult.Interfaces { 211 if iface.Sandbox == "" { 212 // skip host interfaces 213 continue 214 } 215 subnets := g.getSubnetsForNthInterface(i, cniResult) 216 mtu, err := mtuForMacAddress(iface.Mac, g.config.ContainerSideNetwork.Interfaces) 217 if err != nil { 218 return nil, err 219 } 220 interfaceConf := map[string]interface{}{ 221 "type": "physical", 222 "name": iface.Name, 223 "mac_address": iface.Mac, 224 "subnets": subnets, 225 "mtu": mtu, 226 } 227 config = append(config, interfaceConf) 228 } 229 230 // dns 231 dnsData := getDNSData(cniResult.DNS) 232 if dnsData != nil { 233 config = append(config, dnsData...) 234 } 235 236 r, err := yaml.Marshal(map[string]interface{}{ 237 "config": config, 238 }) 239 if err != nil { 240 return nil, err 241 } 242 return []byte("version: 1\n" + string(r)), nil 243 } 244 245 func (g *CloudInitGenerator) getSubnetsForNthInterface(interfaceNo int, cniResult *cnicurrent.Result) []map[string]interface{} { 246 var subnets []map[string]interface{} 247 routes := append(cniResult.Routes[:0:0], cniResult.Routes...) 248 gotDefault := false 249 for _, ipConfig := range cniResult.IPs { 250 if ipConfig.Interface == interfaceNo { 251 subnet := map[string]interface{}{ 252 "type": "static", 253 "address": ipConfig.Address.IP.String(), 254 "netmask": net.IP(ipConfig.Address.Mask).String(), 255 } 256 257 var subnetRoutes []map[string]interface{} 258 // iterate on routes slice in reverse order because at 259 // the end of loop found element will be removed from slice 260 allRoutesLen := len(routes) 261 for i := range routes { 262 cniRoute := routes[allRoutesLen-1-i] 263 var gw net.IP 264 if cniRoute.GW != nil && ipConfig.Address.Contains(cniRoute.GW) { 265 gw = cniRoute.GW 266 } else if cniRoute.GW == nil && !ipConfig.Gateway.IsUnspecified() { 267 gw = ipConfig.Gateway 268 } else { 269 continue 270 } 271 if ones, _ := cniRoute.Dst.Mask.Size(); ones == 0 { 272 if gotDefault { 273 glog.Warning("cloud-init: got more than one default route, using only the first one") 274 continue 275 } 276 gotDefault = true 277 } 278 route := map[string]interface{}{ 279 "network": cniRoute.Dst.IP.String(), 280 "netmask": net.IP(cniRoute.Dst.Mask).String(), 281 "gateway": gw.String(), 282 } 283 subnetRoutes = append(subnetRoutes, route) 284 routes = append(routes[:allRoutesLen-1-i], routes[allRoutesLen-i:]...) 285 } 286 if subnetRoutes != nil { 287 subnet["routes"] = subnetRoutes 288 } 289 290 subnets = append(subnets, subnet) 291 } 292 } 293 294 // fallback to dhcp - should never happen, we always should have IPs 295 if subnets == nil { 296 subnets = append(subnets, map[string]interface{}{ 297 "type": "dhcp", 298 }) 299 } 300 301 return subnets 302 } 303 304 func getDNSData(cniDNS cnitypes.DNS) []map[string]interface{} { 305 var dnsData []map[string]interface{} 306 if cniDNS.Nameservers != nil { 307 dnsData = append(dnsData, map[string]interface{}{ 308 "type": "nameserver", 309 "address": cniDNS.Nameservers, 310 }) 311 if cniDNS.Search != nil { 312 dnsData[0]["search"] = cniDNS.Search 313 } 314 } 315 return dnsData 316 } 317 318 func (g *CloudInitGenerator) generateNetworkConfigurationConfigDrive() ([]byte, error) { 319 if g.config.ContainerSideNetwork == nil { 320 // This can only happen during integration tests 321 // where a dummy sandbox is used 322 return []byte("{}"), nil 323 } 324 cniResult := g.config.ContainerSideNetwork.Result 325 326 config := make(map[string]interface{}) 327 328 // links 329 var links []map[string]interface{} 330 for _, iface := range cniResult.Interfaces { 331 if iface.Sandbox == "" { 332 // skip host interfaces 333 continue 334 } 335 mtu, err := mtuForMacAddress(iface.Mac, g.config.ContainerSideNetwork.Interfaces) 336 if err != nil { 337 return nil, err 338 } 339 linkConf := map[string]interface{}{ 340 "type": "phy", 341 "id": iface.Name, 342 "ethernet_mac_address": iface.Mac, 343 "mtu": mtu, 344 } 345 links = append(links, linkConf) 346 } 347 config["links"] = links 348 349 var networks []map[string]interface{} 350 for i, ipConfig := range cniResult.IPs { 351 netConf := map[string]interface{}{ 352 "id": fmt.Sprintf("net-%d", i), 353 // config from openstack have as network_id network uuid 354 "network_id": fmt.Sprintf("net-%d", i), 355 "type": fmt.Sprintf("ipv%s", ipConfig.Version), 356 "link": cniResult.Interfaces[ipConfig.Interface].Name, 357 "ip_address": ipConfig.Address.IP.String(), 358 "netmask": net.IP(ipConfig.Address.Mask).String(), 359 } 360 361 routes := routesForIP(ipConfig.Address, cniResult.Routes) 362 if routes != nil { 363 netConf["routes"] = routes 364 } 365 366 networks = append(networks, netConf) 367 } 368 config["networks"] = networks 369 370 dnsData := getDNSData(cniResult.DNS) 371 if dnsData != nil { 372 config["services"] = dnsData 373 } 374 375 r, err := json.Marshal(config) 376 if err != nil { 377 return nil, fmt.Errorf("error marshaling network configuration: %v", err) 378 } 379 return r, nil 380 } 381 382 func routesForIP(sourceIP net.IPNet, allRoutes []*cnitypes.Route) []map[string]interface{} { 383 var routes []map[string]interface{} 384 385 // NOTE: at the moment on cni result level there is no distinction 386 // for which interface particular route should be set, 387 // so we are returning there all routes with gateway accessible 388 // by particular source ip address. 389 for _, route := range allRoutes { 390 if sourceIP.Contains(route.GW) { 391 routes = append(routes, map[string]interface{}{ 392 "network": route.Dst.IP.String(), 393 "netmask": net.IP(route.Dst.Mask).String(), 394 "gateway": route.GW.String(), 395 }) 396 } 397 } 398 399 return routes 400 } 401 402 func mtuForMacAddress(mac string, ifaces []*network.InterfaceDescription) (uint16, error) { 403 for _, iface := range ifaces { 404 if iface.HardwareAddr.String() == strings.ToLower(mac) { 405 return iface.MTU, nil 406 } 407 } 408 return 0, fmt.Errorf("interface with mac address %q not found in ContainerSideNetwork", mac) 409 } 410 411 // IsoPath returns a full path to iso image with configuration for VM pod. 412 func (g *CloudInitGenerator) IsoPath() string { 413 return filepath.Join(g.isoDir, fmt.Sprintf("config-%s.iso", g.config.DomainUUID)) 414 } 415 416 // DiskDef returns a DomainDisk definition for Cloud Init ISO image to be included 417 // in VM pod libvirt domain definition. 418 func (g *CloudInitGenerator) DiskDef() *libvirtxml.DomainDisk { 419 return &libvirtxml.DomainDisk{ 420 Device: "cdrom", 421 Driver: &libvirtxml.DomainDiskDriver{Name: "qemu", Type: "raw"}, 422 Source: &libvirtxml.DomainDiskSource{File: &libvirtxml.DomainDiskSourceFile{File: g.IsoPath()}}, 423 ReadOnly: &libvirtxml.DomainDiskReadOnly{}, 424 } 425 } 426 427 // GenerateImage collects metadata, userdata and network configuration and uses 428 // them to prepare an ISO image for NoCloud or ConfigDrive selecting the type 429 // using an info from pod annotations. 430 func (g *CloudInitGenerator) GenerateImage(volumeMap diskPathMap) error { 431 tmpDir, err := ioutil.TempDir("", "config-") 432 if err != nil { 433 return fmt.Errorf("can't create temp dir for config image: %v", err) 434 } 435 defer os.RemoveAll(tmpDir) 436 437 var metaData, userData, networkConfiguration []byte 438 metaData, err = g.generateMetaData() 439 if err == nil { 440 userData, err = g.generateUserData(volumeMap) 441 } 442 if err == nil { 443 networkConfiguration, err = g.generateNetworkConfiguration() 444 } 445 if err != nil { 446 return err 447 } 448 449 var userDataLocation, metaDataLocation, networkConfigLocation string 450 var volumeName string 451 452 // TODO: get rid of this switch. Use descriptor for cloud-init image types. 453 switch g.config.ParsedAnnotations.CDImageType { 454 case types.CloudInitImageTypeNoCloud: 455 userDataLocation = "user-data" 456 metaDataLocation = "meta-data" 457 networkConfigLocation = "network-config" 458 volumeName = "cidata" 459 case types.CloudInitImageTypeConfigDrive: 460 userDataLocation = "openstack/latest/user_data" 461 metaDataLocation = "openstack/latest/meta_data.json" 462 networkConfigLocation = "openstack/latest/network_data.json" 463 volumeName = "config-2" 464 default: 465 // that should newer happen, as imageType should be validated 466 // already earlier 467 return fmt.Errorf("unknown cloud-init config image type: %q", g.config.ParsedAnnotations.CDImageType) 468 } 469 470 fileMap := map[string][]byte{ 471 userDataLocation: userData, 472 metaDataLocation: metaData, 473 } 474 if networkConfiguration != nil { 475 fileMap[networkConfigLocation] = networkConfiguration 476 } 477 if err := fs.WriteFiles(tmpDir, fileMap); err != nil { 478 return fmt.Errorf("can't write user-data: %v", err) 479 } 480 481 if err := os.MkdirAll(g.isoDir, 0777); err != nil { 482 return fmt.Errorf("error making iso directory %q: %v", g.isoDir, err) 483 } 484 485 if err := fs.GenIsoImage(g.IsoPath(), volumeName, tmpDir); err != nil { 486 if rmErr := os.Remove(g.IsoPath()); rmErr != nil { 487 glog.Warningf("Error removing iso file %s: %v", g.IsoPath(), rmErr) 488 } 489 return fmt.Errorf("error generating iso image: %v", err) 490 } 491 492 return nil 493 } 494 495 func (g *CloudInitGenerator) generateEnvVarsContent() string { 496 var buffer bytes.Buffer 497 for _, entry := range g.config.Environment { 498 buffer.WriteString(fmt.Sprintf("%s=%s\n", entry.Key, entry.Value)) 499 } 500 501 return buffer.String() 502 } 503 504 func isRegularFile(path string) bool { 505 fi, err := os.Stat(path) 506 if err != nil { 507 return false 508 } 509 return fi.Mode().IsRegular() 510 } 511 512 func (g *CloudInitGenerator) generateSymlinkScript(volumeMap diskPathMap) string { 513 var symlinkLines []string 514 for _, dev := range g.config.VolumeDevices { 515 if dev.IsRoot() { 516 // special case for the persistent rootfs 517 continue 518 } 519 dpath, found := volumeMap[dev.UUID()] 520 if !found { 521 glog.Warningf("Couldn't determine the path for device %q inside the VM (target path inside the VM: %q)", dev.HostPath, dev.DevicePath) 522 continue 523 } 524 line := linkBlockDeviceScriptTemplate.MustExecuteToString(map[string]string{ 525 "SysfsPath": dpath.sysfsPath, 526 "DevicePath": dev.DevicePath, 527 }) 528 symlinkLines = append(symlinkLines, line) 529 } 530 return makeScript(symlinkLines) 531 } 532 533 func (g *CloudInitGenerator) fixMounts(volumeMap diskPathMap, mounts []interface{}) []interface{} { 534 devMap := make(map[string]string) 535 for _, dev := range g.config.VolumeDevices { 536 if dev.IsRoot() { 537 // special case for the persistent rootfs 538 continue 539 } 540 dpath, found := volumeMap[dev.UUID()] 541 if !found { 542 glog.Warningf("Couldn't determine the path for device %q inside the VM (target path inside the VM: %q)", dev.HostPath, dev.DevicePath) 543 continue 544 } 545 devMap[dev.DevicePath] = dpath.devPath 546 } 547 if len(devMap) == 0 { 548 return mounts 549 } 550 551 var r []interface{} 552 for _, item := range mounts { 553 m, ok := item.([]interface{}) 554 if !ok || len(m) == 0 { 555 r = append(r, item) 556 continue 557 } 558 devPath, ok := m[0].(string) 559 if !ok { 560 r = append(r, item) 561 continue 562 } 563 mapTo, found := devMap[devPath] 564 if !found { 565 r = append(r, item) 566 continue 567 } 568 r = append(r, append([]interface{}{mapTo}, m[1:]...)) 569 } 570 return r 571 } 572 573 func (g *CloudInitGenerator) generateMounts(volumeMap diskPathMap) ([]interface{}, string) { 574 var r []interface{} 575 var mountScriptLines []string 576 for _, m := range g.config.Mounts { 577 // Skip file based mounts (including secrets and config maps). 578 if isRegularFile(m.HostPath) || 579 strings.Contains(m.HostPath, "kubernetes.io~secret") || 580 strings.Contains(m.HostPath, "kubernetes.io~configmap") { 581 continue 582 } 583 584 mountInfo, mountScriptLine, err := generateFlexvolumeMounts(volumeMap, m) 585 if err != nil { 586 if !os.IsNotExist(err) { 587 glog.Errorf("Can't mount directory %q to %q inside the VM: %v", m.HostPath, m.ContainerPath, err) 588 continue 589 } 590 591 // Fs based volume 592 mountInfo, mountScriptLine, err = generateFsBasedVolumeMounts(m) 593 if err != nil { 594 glog.Errorf("Can't mount directory %q to %q inside the VM: %v", m.HostPath, m.ContainerPath, err) 595 continue 596 } 597 } 598 599 r = append(r, mountInfo) 600 mountScriptLines = append(mountScriptLines, mountScriptLine) 601 } 602 603 return r, makeScript(mountScriptLines) 604 } 605 606 func generateFlexvolumeMounts(volumeMap diskPathMap, mount types.VMMount) ([]interface{}, string, error) { 607 uuid, part, err := flexvolume.GetFlexvolumeInfo(mount.HostPath) 608 if err != nil { 609 // If the error is NotExist, return the original error 610 if os.IsNotExist(err) { 611 return nil, "", err 612 } 613 err = fmt.Errorf("can't get flexvolume uuid: %v", err) 614 return nil, "", err 615 } 616 dpath, found := volumeMap[uuid] 617 if !found { 618 err = fmt.Errorf("no device found for flexvolume uuid %q", uuid) 619 return nil, "", err 620 } 621 if part < 0 { 622 part = 1 623 } 624 devPath := dpath.devPath 625 mountDevSuffix := "" 626 if part != 0 { 627 devPath += fmt.Sprintf("-part%d", part) 628 mountDevSuffix += strconv.Itoa(part) 629 } 630 mountScriptLine := mountDevScriptTemplate.MustExecuteToString(map[string]string{ 631 "ContainerPath": mount.ContainerPath, 632 "SysfsPath": dpath.sysfsPath, 633 "DevSuffix": mountDevSuffix, 634 }) 635 return []interface{}{devPath, mount.ContainerPath}, mountScriptLine, nil 636 } 637 638 func generateFsBasedVolumeMounts(mount types.VMMount) ([]interface{}, string, error) { 639 mountTag := path.Base(mount.ContainerPath) 640 fsMountScript := mountFSScriptTemplate.MustExecuteToString(map[string]string{ 641 "ContainerPath": mount.ContainerPath, 642 "MountTag": mountTag, 643 }) 644 r := []interface{}{mountTag, mount.ContainerPath, "9p", "trans=virtio"} 645 return r, fsMountScript, nil 646 } 647 648 type writeFilesUpdater struct { 649 entries []interface{} 650 mounts []types.VMMount 651 } 652 653 func newWriteFilesUpdater(mounts []types.VMMount) *writeFilesUpdater { 654 return &writeFilesUpdater{ 655 mounts: mounts, 656 } 657 } 658 659 func (u *writeFilesUpdater) put(entry interface{}) { 660 u.entries = append(u.entries, entry) 661 } 662 663 func (u *writeFilesUpdater) putPlainText(path string, content string, perms os.FileMode) { 664 u.put(map[string]interface{}{ 665 "path": path, 666 "content": content, 667 "permissions": fmt.Sprintf("%#o", uint32(perms)), 668 }) 669 } 670 671 func (u *writeFilesUpdater) putBase64(path string, content []byte, perms os.FileMode) { 672 encodedContent := base64.StdEncoding.EncodeToString(content) 673 u.put(map[string]interface{}{ 674 "path": path, 675 "content": encodedContent, 676 "encoding": "b64", 677 "permissions": fmt.Sprintf("%#o", uint32(perms)), 678 }) 679 } 680 681 func (u *writeFilesUpdater) updateUserData(userData map[string]interface{}) { 682 if len(u.entries) == 0 { 683 return 684 } 685 686 writeFiles := utils.Merge(userData["write_files"], u.entries).([]interface{}) 687 if len(writeFiles) != 0 { 688 userData["write_files"] = writeFiles 689 } 690 } 691 692 func (u *writeFilesUpdater) addSecrets() { 693 u.addFilesForVolumeType("secret") 694 } 695 696 func (u *writeFilesUpdater) addConfigMapEntries() { 697 u.addFilesForVolumeType("configmap") 698 } 699 700 func (u *writeFilesUpdater) addFileLikeMounts() { 701 for _, mount := range u.filterMounts(func(path string) bool { 702 fi, err := os.Stat(path) 703 switch { 704 case err != nil: 705 return false 706 case fi.Mode().IsRegular(): 707 return true 708 } 709 return false 710 }) { 711 content, err := ioutil.ReadFile(mount.HostPath) 712 if err != nil { 713 glog.Warningf("Error during reading content of '%s' file: %v", mount.HostPath, err) 714 continue 715 } 716 717 glog.V(3).Infof("Adding file '%s' as volume: %s", mount.HostPath, mount.ContainerPath) 718 u.putBase64(mount.ContainerPath, content, 0644) 719 } 720 } 721 722 func (u *writeFilesUpdater) addFilesForVolumeType(suffix string) { 723 filter := "volumes/kubernetes.io~" + suffix + "/" 724 for _, mount := range u.filterMounts(func(path string) bool { 725 return strings.Contains(path, filter) 726 }) { 727 u.addFilesForMount(mount) 728 } 729 } 730 731 func (u *writeFilesUpdater) addSymlinkScript(content string) { 732 u.putPlainText(symlinkFileLocation, content, 0755) 733 } 734 735 func (u *writeFilesUpdater) addMountScript(content string) { 736 u.putPlainText(mountFileLocation, content, 0755) 737 } 738 739 func (u *writeFilesUpdater) addEnvironmentFile(content string) { 740 u.putPlainText(envFileLocation, content, 0644) 741 } 742 743 func (u *writeFilesUpdater) addFilesForMount(mount types.VMMount) []interface{} { 744 var writeFiles []interface{} 745 746 addFileContent := func(fullPath string) error { 747 content, err := ioutil.ReadFile(fullPath) 748 if err != nil { 749 return err 750 } 751 stat, err := os.Stat(fullPath) 752 if err != nil { 753 return err 754 } 755 relativePath := fullPath[len(mount.HostPath)+1:] 756 u.putBase64(path.Join(mount.ContainerPath, relativePath), content, stat.Mode()) 757 return nil 758 } 759 760 glog.V(3).Infof("Scanning %s for files", mount.HostPath) 761 if err := scanDirectory(mount.HostPath, addFileContent); err != nil { 762 glog.Errorf("Error while scanning directory %s: %v", mount.HostPath, err) 763 } 764 glog.V(3).Infof("Found %d entries", len(writeFiles)) 765 766 return writeFiles 767 } 768 769 func (u *writeFilesUpdater) filterMounts(filter func(string) bool) []types.VMMount { 770 var r []types.VMMount 771 for _, mount := range u.mounts { 772 if filter(mount.HostPath) { 773 r = append(r, mount) 774 } 775 } 776 return r 777 } 778 779 func scanDirectory(dirPath string, callback func(string) error) error { 780 entries, err := ioutil.ReadDir(dirPath) 781 if err != nil { 782 return err 783 } 784 785 for _, entry := range entries { 786 fullPath := path.Join(dirPath, entry.Name()) 787 788 switch { 789 case entry.Mode().IsDir(): 790 glog.V(3).Infof("Scanning directory: %s", entry.Name()) 791 if err := scanDirectory(fullPath, callback); err != nil { 792 return err 793 } 794 glog.V(3).Infof("Leaving directory: %s", entry.Name()) 795 case entry.Mode().IsRegular(): 796 glog.V(3).Infof("Found regular file: %s", entry.Name()) 797 err := callback(fullPath) 798 if err != nil { 799 return err 800 } 801 continue 802 case entry.Mode()&os.ModeSymlink != 0: 803 glog.V(3).Infof("Found symlink: %s", entry.Name()) 804 fi, err := os.Stat(fullPath) 805 switch { 806 case err != nil: 807 return err 808 case fi.Mode().IsRegular(): 809 err = callback(fullPath) 810 if err != nil { 811 return err 812 } 813 case fi.Mode().IsDir(): 814 glog.V(3).Info("... which points to directory, going deeper ...") 815 816 // NOTE: this does not need to be protected against loops 817 // because it's prepared by kubelet in safe manner (if it's not 818 // it's bug on kubelet side 819 if err := scanDirectory(fullPath, callback); err != nil { 820 return err 821 } 822 glog.V(3).Infof("... came back from symlink to directory: %s", entry.Name()) 823 default: 824 glog.V(3).Info("... but it's pointing to something other than directory or regular file") 825 } 826 } 827 } 828 829 return nil 830 } 831 832 func makeScript(lines []string) string { 833 if len(lines) != 0 { 834 return fmt.Sprintf("#!/bin/sh\n%s\n", strings.Join(lines, "\n")) 835 } 836 return "" 837 }