github.com/amanya/packer@v0.12.1-0.20161117214323-902ac5ab2eb6/provisioner/chef-client/provisioner.go (about) 1 // This package implements a provisioner for Packer that uses 2 // Chef to provision the remote machine, specifically with chef-client (that is, 3 // with a Chef server). 4 package chefclient 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "fmt" 10 "io/ioutil" 11 "os" 12 "path/filepath" 13 "strings" 14 15 "github.com/mitchellh/packer/common" 16 "github.com/mitchellh/packer/common/uuid" 17 "github.com/mitchellh/packer/helper/config" 18 "github.com/mitchellh/packer/packer" 19 "github.com/mitchellh/packer/provisioner" 20 "github.com/mitchellh/packer/template/interpolate" 21 ) 22 23 type guestOSTypeConfig struct { 24 executeCommand string 25 installCommand string 26 knifeCommand string 27 stagingDir string 28 } 29 30 var guestOSTypeConfigs = map[string]guestOSTypeConfig{ 31 provisioner.UnixOSType: { 32 executeCommand: "{{if .Sudo}}sudo {{end}}chef-client --no-color -c {{.ConfigPath}} -j {{.JsonPath}}", 33 installCommand: "curl -L https://www.chef.io/chef/install.sh | {{if .Sudo}}sudo {{end}}bash", 34 knifeCommand: "{{if .Sudo}}sudo {{end}}knife {{.Args}} {{.Flags}}", 35 stagingDir: "/tmp/packer-chef-client", 36 }, 37 provisioner.WindowsOSType: { 38 executeCommand: "c:/opscode/chef/bin/chef-client.bat --no-color -c {{.ConfigPath}} -j {{.JsonPath}}", 39 installCommand: "powershell.exe -Command \"(New-Object System.Net.WebClient).DownloadFile('http://chef.io/chef/install.msi', 'C:\\Windows\\Temp\\chef.msi');Start-Process 'msiexec' -ArgumentList '/qb /i C:\\Windows\\Temp\\chef.msi' -NoNewWindow -Wait\"", 40 knifeCommand: "c:/opscode/chef/bin/knife.bat {{.Args}} {{.Flags}}", 41 stagingDir: "C:/Windows/Temp/packer-chef-client", 42 }, 43 } 44 45 type Config struct { 46 common.PackerConfig `mapstructure:",squash"` 47 48 Json map[string]interface{} 49 50 ChefEnvironment string `mapstructure:"chef_environment"` 51 ClientKey string `mapstructure:"client_key"` 52 ConfigTemplate string `mapstructure:"config_template"` 53 EncryptedDataBagSecretPath string `mapstructure:"encrypted_data_bag_secret_path"` 54 ExecuteCommand string `mapstructure:"execute_command"` 55 GuestOSType string `mapstructure:"guest_os_type"` 56 InstallCommand string `mapstructure:"install_command"` 57 KnifeCommand string `mapstructure:"knife_command"` 58 NodeName string `mapstructure:"node_name"` 59 PreventSudo bool `mapstructure:"prevent_sudo"` 60 RunList []string `mapstructure:"run_list"` 61 ServerUrl string `mapstructure:"server_url"` 62 SkipCleanClient bool `mapstructure:"skip_clean_client"` 63 SkipCleanNode bool `mapstructure:"skip_clean_node"` 64 SkipInstall bool `mapstructure:"skip_install"` 65 SslVerifyMode string `mapstructure:"ssl_verify_mode"` 66 StagingDir string `mapstructure:"staging_directory"` 67 ValidationClientName string `mapstructure:"validation_client_name"` 68 ValidationKeyPath string `mapstructure:"validation_key_path"` 69 70 ctx interpolate.Context 71 } 72 73 type Provisioner struct { 74 config Config 75 guestOSTypeConfig guestOSTypeConfig 76 guestCommands *provisioner.GuestCommands 77 } 78 79 type ConfigTemplate struct { 80 ChefEnvironment string 81 ClientKey string 82 EncryptedDataBagSecretPath string 83 NodeName string 84 ServerUrl string 85 SslVerifyMode string 86 ValidationClientName string 87 ValidationKeyPath string 88 } 89 90 type ExecuteTemplate struct { 91 ConfigPath string 92 JsonPath string 93 Sudo bool 94 } 95 96 type InstallChefTemplate struct { 97 Sudo bool 98 } 99 100 type KnifeTemplate struct { 101 Sudo bool 102 Flags string 103 Args string 104 } 105 106 func (p *Provisioner) Prepare(raws ...interface{}) error { 107 err := config.Decode(&p.config, &config.DecodeOpts{ 108 Interpolate: true, 109 InterpolateContext: &p.config.ctx, 110 InterpolateFilter: &interpolate.RenderFilter{ 111 Exclude: []string{ 112 "execute_command", 113 "install_command", 114 "knife_command", 115 }, 116 }, 117 }, raws...) 118 if err != nil { 119 return err 120 } 121 122 if p.config.GuestOSType == "" { 123 p.config.GuestOSType = provisioner.DefaultOSType 124 } 125 p.config.GuestOSType = strings.ToLower(p.config.GuestOSType) 126 127 var ok bool 128 p.guestOSTypeConfig, ok = guestOSTypeConfigs[p.config.GuestOSType] 129 if !ok { 130 return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) 131 } 132 133 p.guestCommands, err = provisioner.NewGuestCommands(p.config.GuestOSType, !p.config.PreventSudo) 134 if err != nil { 135 return fmt.Errorf("Invalid guest_os_type: \"%s\"", p.config.GuestOSType) 136 } 137 138 if p.config.ExecuteCommand == "" { 139 p.config.ExecuteCommand = p.guestOSTypeConfig.executeCommand 140 } 141 142 if p.config.InstallCommand == "" { 143 p.config.InstallCommand = p.guestOSTypeConfig.installCommand 144 } 145 146 if p.config.RunList == nil { 147 p.config.RunList = make([]string, 0) 148 } 149 150 if p.config.StagingDir == "" { 151 p.config.StagingDir = p.guestOSTypeConfig.stagingDir 152 } 153 154 if p.config.KnifeCommand == "" { 155 p.config.KnifeCommand = p.guestOSTypeConfig.knifeCommand 156 } 157 158 var errs *packer.MultiError 159 if p.config.ConfigTemplate != "" { 160 fi, err := os.Stat(p.config.ConfigTemplate) 161 if err != nil { 162 errs = packer.MultiErrorAppend( 163 errs, fmt.Errorf("Bad config template path: %s", err)) 164 } else if fi.IsDir() { 165 errs = packer.MultiErrorAppend( 166 errs, fmt.Errorf("Config template path must be a file: %s", err)) 167 } 168 } 169 170 if p.config.EncryptedDataBagSecretPath != "" { 171 pFileInfo, err := os.Stat(p.config.EncryptedDataBagSecretPath) 172 173 if err != nil || pFileInfo.IsDir() { 174 errs = packer.MultiErrorAppend( 175 errs, fmt.Errorf("Bad encrypted data bag secret '%s': %s", p.config.EncryptedDataBagSecretPath, err)) 176 } 177 } 178 179 if p.config.ServerUrl == "" { 180 errs = packer.MultiErrorAppend( 181 errs, fmt.Errorf("server_url must be set")) 182 } 183 184 if p.config.EncryptedDataBagSecretPath != "" { 185 pFileInfo, err := os.Stat(p.config.EncryptedDataBagSecretPath) 186 187 if err != nil || pFileInfo.IsDir() { 188 errs = packer.MultiErrorAppend( 189 errs, fmt.Errorf("Bad encrypted data bag secret '%s': %s", p.config.EncryptedDataBagSecretPath, err)) 190 } 191 } 192 193 jsonValid := true 194 for k, v := range p.config.Json { 195 p.config.Json[k], err = p.deepJsonFix(k, v) 196 if err != nil { 197 errs = packer.MultiErrorAppend( 198 errs, fmt.Errorf("Error processing JSON: %s", err)) 199 jsonValid = false 200 } 201 } 202 203 if jsonValid { 204 // Process the user variables within the JSON and set the JSON. 205 // Do this early so that we can validate and show errors. 206 p.config.Json, err = p.processJsonUserVars() 207 if err != nil { 208 errs = packer.MultiErrorAppend( 209 errs, fmt.Errorf("Error processing user variables in JSON: %s", err)) 210 } 211 } 212 213 if errs != nil && len(errs.Errors) > 0 { 214 return errs 215 } 216 217 return nil 218 } 219 220 func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error { 221 222 nodeName := p.config.NodeName 223 if nodeName == "" { 224 nodeName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID()) 225 } 226 remoteValidationKeyPath := "" 227 serverUrl := p.config.ServerUrl 228 229 if !p.config.SkipInstall { 230 if err := p.installChef(ui, comm); err != nil { 231 return fmt.Errorf("Error installing Chef: %s", err) 232 } 233 } 234 235 if err := p.createDir(ui, comm, p.config.StagingDir); err != nil { 236 return fmt.Errorf("Error creating staging directory: %s", err) 237 } 238 239 if p.config.ClientKey == "" { 240 p.config.ClientKey = fmt.Sprintf("%s/client.pem", p.config.StagingDir) 241 } 242 243 encryptedDataBagSecretPath := "" 244 if p.config.EncryptedDataBagSecretPath != "" { 245 encryptedDataBagSecretPath = fmt.Sprintf("%s/encrypted_data_bag_secret", p.config.StagingDir) 246 if err := p.uploadFile(ui, 247 comm, 248 encryptedDataBagSecretPath, 249 p.config.EncryptedDataBagSecretPath); err != nil { 250 return fmt.Errorf("Error uploading encrypted data bag secret: %s", err) 251 } 252 } 253 254 if p.config.ValidationKeyPath != "" { 255 remoteValidationKeyPath = fmt.Sprintf("%s/validation.pem", p.config.StagingDir) 256 if err := p.uploadFile(ui, comm, remoteValidationKeyPath, p.config.ValidationKeyPath); err != nil { 257 return fmt.Errorf("Error copying validation key: %s", err) 258 } 259 } 260 261 configPath, err := p.createConfig( 262 ui, 263 comm, 264 nodeName, 265 serverUrl, 266 p.config.ClientKey, 267 encryptedDataBagSecretPath, 268 remoteValidationKeyPath, 269 p.config.ValidationClientName, 270 p.config.ChefEnvironment, 271 p.config.SslVerifyMode) 272 if err != nil { 273 return fmt.Errorf("Error creating Chef config file: %s", err) 274 } 275 276 jsonPath, err := p.createJson(ui, comm) 277 if err != nil { 278 return fmt.Errorf("Error creating JSON attributes: %s", err) 279 } 280 281 err = p.executeChef(ui, comm, configPath, jsonPath) 282 283 knifeConfigPath, err2 := p.createKnifeConfig( 284 ui, comm, nodeName, serverUrl, p.config.ClientKey, p.config.SslVerifyMode) 285 if err2 != nil { 286 return fmt.Errorf("Error creating knife config on node: %s", err2) 287 } 288 if !p.config.SkipCleanNode { 289 if err2 := p.cleanNode(ui, comm, nodeName, knifeConfigPath); err2 != nil { 290 return fmt.Errorf("Error cleaning up chef node: %s", err2) 291 } 292 } 293 294 if !p.config.SkipCleanClient { 295 if err2 := p.cleanClient(ui, comm, nodeName, knifeConfigPath); err2 != nil { 296 return fmt.Errorf("Error cleaning up chef client: %s", err2) 297 } 298 } 299 300 if err != nil { 301 return fmt.Errorf("Error executing Chef: %s", err) 302 } 303 304 if err := p.removeDir(ui, comm, p.config.StagingDir); err != nil { 305 return fmt.Errorf("Error removing %s: %s", p.config.StagingDir, err) 306 } 307 308 return nil 309 } 310 311 func (p *Provisioner) Cancel() { 312 // Just hard quit. It isn't a big deal if what we're doing keeps 313 // running on the other side. 314 os.Exit(0) 315 } 316 317 func (p *Provisioner) uploadDirectory(ui packer.Ui, comm packer.Communicator, dst string, src string) error { 318 if err := p.createDir(ui, comm, dst); err != nil { 319 return err 320 } 321 322 // Make sure there is a trailing "/" so that the directory isn't 323 // created on the other side. 324 if src[len(src)-1] != '/' { 325 src = src + "/" 326 } 327 328 return comm.UploadDir(dst, src, nil) 329 } 330 331 func (p *Provisioner) uploadFile(ui packer.Ui, comm packer.Communicator, remotePath string, localPath string) error { 332 ui.Message(fmt.Sprintf("Uploading %s...", localPath)) 333 334 f, err := os.Open(localPath) 335 if err != nil { 336 return err 337 } 338 defer f.Close() 339 340 return comm.Upload(remotePath, f, nil) 341 } 342 343 func (p *Provisioner) createConfig( 344 ui packer.Ui, 345 comm packer.Communicator, 346 nodeName string, 347 serverUrl string, 348 clientKey string, 349 encryptedDataBagSecretPath, 350 remoteKeyPath string, 351 validationClientName string, 352 chefEnvironment string, 353 sslVerifyMode string) (string, error) { 354 355 ui.Message("Creating configuration file 'client.rb'") 356 357 // Read the template 358 tpl := DefaultConfigTemplate 359 if p.config.ConfigTemplate != "" { 360 f, err := os.Open(p.config.ConfigTemplate) 361 if err != nil { 362 return "", err 363 } 364 defer f.Close() 365 366 tplBytes, err := ioutil.ReadAll(f) 367 if err != nil { 368 return "", err 369 } 370 371 tpl = string(tplBytes) 372 } 373 374 ctx := p.config.ctx 375 ctx.Data = &ConfigTemplate{ 376 NodeName: nodeName, 377 ServerUrl: serverUrl, 378 ClientKey: clientKey, 379 ValidationKeyPath: remoteKeyPath, 380 ValidationClientName: validationClientName, 381 ChefEnvironment: chefEnvironment, 382 SslVerifyMode: sslVerifyMode, 383 EncryptedDataBagSecretPath: encryptedDataBagSecretPath, 384 } 385 configString, err := interpolate.Render(tpl, &ctx) 386 if err != nil { 387 return "", err 388 } 389 390 remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "client.rb")) 391 if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString)), nil); err != nil { 392 return "", err 393 } 394 395 return remotePath, nil 396 } 397 398 func (p *Provisioner) createKnifeConfig(ui packer.Ui, comm packer.Communicator, nodeName string, serverUrl string, clientKey string, sslVerifyMode string) (string, error) { 399 ui.Message("Creating configuration file 'knife.rb'") 400 401 // Read the template 402 tpl := DefaultKnifeTemplate 403 404 ctx := p.config.ctx 405 ctx.Data = &ConfigTemplate{ 406 NodeName: nodeName, 407 ServerUrl: serverUrl, 408 ClientKey: clientKey, 409 SslVerifyMode: sslVerifyMode, 410 } 411 configString, err := interpolate.Render(tpl, &ctx) 412 if err != nil { 413 return "", err 414 } 415 416 remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "knife.rb")) 417 if err := comm.Upload(remotePath, bytes.NewReader([]byte(configString)), nil); err != nil { 418 return "", err 419 } 420 421 return remotePath, nil 422 } 423 424 func (p *Provisioner) createJson(ui packer.Ui, comm packer.Communicator) (string, error) { 425 ui.Message("Creating JSON attribute file") 426 427 jsonData := make(map[string]interface{}) 428 429 // Copy the configured JSON 430 for k, v := range p.config.Json { 431 jsonData[k] = v 432 } 433 434 // Set the run list if it was specified 435 if len(p.config.RunList) > 0 { 436 jsonData["run_list"] = p.config.RunList 437 } 438 439 jsonBytes, err := json.MarshalIndent(jsonData, "", " ") 440 if err != nil { 441 return "", err 442 } 443 444 // Upload the bytes 445 remotePath := filepath.ToSlash(filepath.Join(p.config.StagingDir, "first-boot.json")) 446 if err := comm.Upload(remotePath, bytes.NewReader(jsonBytes), nil); err != nil { 447 return "", err 448 } 449 450 return remotePath, nil 451 } 452 453 func (p *Provisioner) createDir(ui packer.Ui, comm packer.Communicator, dir string) error { 454 ui.Message(fmt.Sprintf("Creating directory: %s", dir)) 455 456 cmd := &packer.RemoteCmd{Command: p.guestCommands.CreateDir(dir)} 457 if err := cmd.StartWithUi(comm, ui); err != nil { 458 return err 459 } 460 if cmd.ExitStatus != 0 { 461 return fmt.Errorf("Non-zero exit status. See output above for more info.") 462 } 463 464 // Chmod the directory to 0777 just so that we can access it as our user 465 cmd = &packer.RemoteCmd{Command: p.guestCommands.Chmod(dir, "0777")} 466 if err := cmd.StartWithUi(comm, ui); err != nil { 467 return err 468 } 469 if cmd.ExitStatus != 0 { 470 return fmt.Errorf("Non-zero exit status. See output above for more info.") 471 } 472 473 return nil 474 } 475 476 func (p *Provisioner) cleanNode(ui packer.Ui, comm packer.Communicator, node string, knifeConfigPath string) error { 477 ui.Say("Cleaning up chef node...") 478 args := []string{"node", "delete", node} 479 if err := p.knifeExec(ui, comm, node, knifeConfigPath, args); err != nil { 480 return fmt.Errorf("Failed to cleanup node: %s", err) 481 } 482 483 return nil 484 } 485 486 func (p *Provisioner) cleanClient(ui packer.Ui, comm packer.Communicator, node string, knifeConfigPath string) error { 487 ui.Say("Cleaning up chef client...") 488 args := []string{"client", "delete", node} 489 if err := p.knifeExec(ui, comm, node, knifeConfigPath, args); err != nil { 490 return fmt.Errorf("Failed to cleanup client: %s", err) 491 } 492 493 return nil 494 } 495 496 func (p *Provisioner) knifeExec(ui packer.Ui, comm packer.Communicator, node string, knifeConfigPath string, args []string) error { 497 flags := []string{ 498 "-y", 499 "-c", knifeConfigPath, 500 } 501 502 p.config.ctx.Data = &KnifeTemplate{ 503 Sudo: !p.config.PreventSudo, 504 Flags: strings.Join(flags, " "), 505 Args: strings.Join(args, " "), 506 } 507 508 command, err := interpolate.Render(p.config.KnifeCommand, &p.config.ctx) 509 if err != nil { 510 return err 511 } 512 513 cmd := &packer.RemoteCmd{Command: command} 514 if err := cmd.StartWithUi(comm, ui); err != nil { 515 return err 516 } 517 if cmd.ExitStatus != 0 { 518 return fmt.Errorf( 519 "Non-zero exit status. See output above for more info.\n\n"+ 520 "Command: %s", 521 command) 522 } 523 524 return nil 525 } 526 527 func (p *Provisioner) removeDir(ui packer.Ui, comm packer.Communicator, dir string) error { 528 ui.Message(fmt.Sprintf("Removing directory: %s", dir)) 529 530 cmd := &packer.RemoteCmd{Command: p.guestCommands.RemoveDir(dir)} 531 if err := cmd.StartWithUi(comm, ui); err != nil { 532 return err 533 } 534 535 return nil 536 } 537 538 func (p *Provisioner) executeChef(ui packer.Ui, comm packer.Communicator, config string, json string) error { 539 p.config.ctx.Data = &ExecuteTemplate{ 540 ConfigPath: config, 541 JsonPath: json, 542 Sudo: !p.config.PreventSudo, 543 } 544 command, err := interpolate.Render(p.config.ExecuteCommand, &p.config.ctx) 545 if err != nil { 546 return err 547 } 548 549 ui.Message(fmt.Sprintf("Executing Chef: %s", command)) 550 551 cmd := &packer.RemoteCmd{ 552 Command: command, 553 } 554 555 if err := cmd.StartWithUi(comm, ui); err != nil { 556 return err 557 } 558 559 if cmd.ExitStatus != 0 { 560 return fmt.Errorf("Non-zero exit status: %d", cmd.ExitStatus) 561 } 562 563 return nil 564 } 565 566 func (p *Provisioner) installChef(ui packer.Ui, comm packer.Communicator) error { 567 ui.Message("Installing Chef...") 568 569 p.config.ctx.Data = &InstallChefTemplate{ 570 Sudo: !p.config.PreventSudo, 571 } 572 command, err := interpolate.Render(p.config.InstallCommand, &p.config.ctx) 573 if err != nil { 574 return err 575 } 576 577 ui.Message(command) 578 579 cmd := &packer.RemoteCmd{Command: command} 580 if err := cmd.StartWithUi(comm, ui); err != nil { 581 return err 582 } 583 584 if cmd.ExitStatus != 0 { 585 return fmt.Errorf( 586 "Install script exited with non-zero exit status %d", cmd.ExitStatus) 587 } 588 589 return nil 590 } 591 592 func (p *Provisioner) deepJsonFix(key string, current interface{}) (interface{}, error) { 593 if current == nil { 594 return nil, nil 595 } 596 597 switch c := current.(type) { 598 case []interface{}: 599 val := make([]interface{}, len(c)) 600 for i, v := range c { 601 var err error 602 val[i], err = p.deepJsonFix(fmt.Sprintf("%s[%d]", key, i), v) 603 if err != nil { 604 return nil, err 605 } 606 } 607 608 return val, nil 609 case []uint8: 610 return string(c), nil 611 case map[interface{}]interface{}: 612 val := make(map[string]interface{}) 613 for k, v := range c { 614 ks, ok := k.(string) 615 if !ok { 616 return nil, fmt.Errorf("%s: key is not string", key) 617 } 618 619 var err error 620 val[ks], err = p.deepJsonFix( 621 fmt.Sprintf("%s.%s", key, ks), v) 622 if err != nil { 623 return nil, err 624 } 625 } 626 627 return val, nil 628 default: 629 return current, nil 630 } 631 } 632 633 func (p *Provisioner) processJsonUserVars() (map[string]interface{}, error) { 634 jsonBytes, err := json.Marshal(p.config.Json) 635 if err != nil { 636 // This really shouldn't happen since we literally just unmarshalled 637 panic(err) 638 } 639 640 // Copy the user variables so that we can restore them later, and 641 // make sure we make the quotes JSON-friendly in the user variables. 642 originalUserVars := make(map[string]string) 643 for k, v := range p.config.ctx.UserVariables { 644 originalUserVars[k] = v 645 } 646 647 // Make sure we reset them no matter what 648 defer func() { 649 p.config.ctx.UserVariables = originalUserVars 650 }() 651 652 // Make the current user variables JSON string safe. 653 for k, v := range p.config.ctx.UserVariables { 654 v = strings.Replace(v, `\`, `\\`, -1) 655 v = strings.Replace(v, `"`, `\"`, -1) 656 p.config.ctx.UserVariables[k] = v 657 } 658 659 // Process the bytes with the template processor 660 p.config.ctx.Data = nil 661 jsonBytesProcessed, err := interpolate.Render(string(jsonBytes), &p.config.ctx) 662 if err != nil { 663 return nil, err 664 } 665 666 var result map[string]interface{} 667 if err := json.Unmarshal([]byte(jsonBytesProcessed), &result); err != nil { 668 return nil, err 669 } 670 671 return result, nil 672 } 673 674 var DefaultConfigTemplate = ` 675 log_level :info 676 log_location STDOUT 677 chef_server_url "{{.ServerUrl}}" 678 client_key "{{.ClientKey}}" 679 {{if ne .EncryptedDataBagSecretPath ""}} 680 encrypted_data_bag_secret "{{.EncryptedDataBagSecretPath}}" 681 {{end}} 682 {{if ne .ValidationClientName ""}} 683 validation_client_name "{{.ValidationClientName}}" 684 {{else}} 685 validation_client_name "chef-validator" 686 {{end}} 687 {{if ne .ValidationKeyPath ""}} 688 validation_key "{{.ValidationKeyPath}}" 689 {{end}} 690 node_name "{{.NodeName}}" 691 {{if ne .ChefEnvironment ""}} 692 environment "{{.ChefEnvironment}}" 693 {{end}} 694 {{if ne .SslVerifyMode ""}} 695 ssl_verify_mode :{{.SslVerifyMode}} 696 {{end}} 697 ` 698 699 var DefaultKnifeTemplate = ` 700 log_level :info 701 log_location STDOUT 702 chef_server_url "{{.ServerUrl}}" 703 client_key "{{.ClientKey}}" 704 node_name "{{.NodeName}}" 705 {{if ne .SslVerifyMode ""}} 706 ssl_verify_mode :{{.SslVerifyMode}} 707 {{end}} 708 `