github.com/lukahartwig/terraform@v0.11.4-0.20180302171601-664391c254ea/builtin/provisioners/salt-masterless/resource_provisioner.go (about) 1 // This package implements a provisioner for Terraform that executes a 2 // saltstack state within the remote machine 3 // 4 // Adapted from gitub.com/hashicorp/packer/provisioner/salt-masterless 5 6 package saltmasterless 7 8 import ( 9 "bytes" 10 "context" 11 "errors" 12 "fmt" 13 "io" 14 "os" 15 "path/filepath" 16 17 "github.com/hashicorp/terraform/communicator" 18 "github.com/hashicorp/terraform/communicator/remote" 19 "github.com/hashicorp/terraform/helper/schema" 20 "github.com/hashicorp/terraform/terraform" 21 linereader "github.com/mitchellh/go-linereader" 22 ) 23 24 type provisionFn func(terraform.UIOutput, communicator.Communicator) error 25 26 type provisioner struct { 27 SkipBootstrap bool 28 BootstrapArgs string 29 LocalStateTree string 30 DisableSudo bool 31 CustomState string 32 MinionConfig string 33 LocalPillarRoots string 34 RemoteStateTree string 35 RemotePillarRoots string 36 TempConfigDir string 37 NoExitOnFailure bool 38 LogLevel string 39 SaltCallArgs string 40 CmdArgs string 41 } 42 43 const DefaultStateTreeDir = "/srv/salt" 44 const DefaultPillarRootDir = "/srv/pillar" 45 46 // Provisioner returns a salt-masterless provisioner 47 func Provisioner() terraform.ResourceProvisioner { 48 return &schema.Provisioner{ 49 Schema: map[string]*schema.Schema{ 50 "local_state_tree": &schema.Schema{ 51 Type: schema.TypeString, 52 Required: true, 53 }, 54 "local_pillar_roots": &schema.Schema{ 55 Type: schema.TypeString, 56 Optional: true, 57 }, 58 "remote_state_tree": &schema.Schema{ 59 Type: schema.TypeString, 60 Optional: true, 61 }, 62 "remote_pillar_roots": &schema.Schema{ 63 Type: schema.TypeString, 64 Optional: true, 65 }, 66 "temp_config_dir": &schema.Schema{ 67 Type: schema.TypeString, 68 Optional: true, 69 Default: "/tmp/salt", 70 }, 71 "skip_bootstrap": &schema.Schema{ 72 Type: schema.TypeBool, 73 Optional: true, 74 }, 75 "no_exit_on_failure": &schema.Schema{ 76 Type: schema.TypeBool, 77 Optional: true, 78 }, 79 "bootstrap_args": &schema.Schema{ 80 Type: schema.TypeString, 81 Optional: true, 82 }, 83 "disable_sudo": &schema.Schema{ 84 Type: schema.TypeBool, 85 Optional: true, 86 }, 87 "custom_state": &schema.Schema{ 88 Type: schema.TypeString, 89 Optional: true, 90 }, 91 "minion_config_file": &schema.Schema{ 92 Type: schema.TypeString, 93 Optional: true, 94 }, 95 "cmd_args": &schema.Schema{ 96 Type: schema.TypeString, 97 Optional: true, 98 }, 99 "salt_call_args": &schema.Schema{ 100 Type: schema.TypeString, 101 Optional: true, 102 }, 103 "log_level": &schema.Schema{ 104 Type: schema.TypeString, 105 Optional: true, 106 }, 107 }, 108 109 ApplyFunc: applyFn, 110 ValidateFunc: validateFn, 111 } 112 } 113 114 // Apply executes the file provisioner 115 func applyFn(ctx context.Context) error { 116 // Decode the raw config for this provisioner 117 var err error 118 119 o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput) 120 d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData) 121 connState := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState) 122 123 p, err := decodeConfig(d) 124 if err != nil { 125 return err 126 } 127 128 // Get a new communicator 129 comm, err := communicator.New(connState) 130 if err != nil { 131 return err 132 } 133 134 ctx, cancelFunc := context.WithTimeout(ctx, comm.Timeout()) 135 defer cancelFunc() 136 137 // Wait for the context to end and then disconnect 138 go func() { 139 <-ctx.Done() 140 comm.Disconnect() 141 }() 142 143 // Wait and retry until we establish the connection 144 err = communicator.Retry(ctx, func() error { 145 return comm.Connect(o) 146 }) 147 148 if err != nil { 149 return err 150 } 151 152 var src, dst string 153 154 o.Output("Provisioning with Salt...") 155 if !p.SkipBootstrap { 156 cmd := &remote.Cmd{ 157 // Fallback on wget if curl failed for any reason (such as not being installed) 158 Command: fmt.Sprintf("curl -L https://bootstrap.saltstack.com -o /tmp/install_salt.sh || wget -O /tmp/install_salt.sh https://bootstrap.saltstack.com"), 159 } 160 o.Output(fmt.Sprintf("Downloading saltstack bootstrap to /tmp/install_salt.sh")) 161 if err = comm.Start(cmd); err != nil { 162 err = fmt.Errorf("Unable to download Salt: %s", err) 163 } 164 165 if err == nil { 166 cmd.Wait() 167 if cmd.ExitStatus != 0 { 168 err = fmt.Errorf("Curl exited with non-zero exit status: %d", cmd.ExitStatus) 169 } 170 } 171 172 outR, outW := io.Pipe() 173 errR, errW := io.Pipe() 174 outDoneCh := make(chan struct{}) 175 errDoneCh := make(chan struct{}) 176 go copyOutput(o, outR, outDoneCh) 177 go copyOutput(o, errR, errDoneCh) 178 cmd = &remote.Cmd{ 179 Command: fmt.Sprintf("%s /tmp/install_salt.sh %s", p.sudo("sh"), p.BootstrapArgs), 180 Stdout: outW, 181 Stderr: errW, 182 } 183 184 o.Output(fmt.Sprintf("Installing Salt with command %s", cmd.Command)) 185 if err = comm.Start(cmd); err != nil { 186 err = fmt.Errorf("Unable to install Salt: %s", err) 187 } 188 189 if err == nil { 190 cmd.Wait() 191 if cmd.ExitStatus != 0 { 192 err = fmt.Errorf("install_salt.sh exited with non-zero exit status: %d", cmd.ExitStatus) 193 } 194 } 195 // Wait for output to clean up 196 outW.Close() 197 errW.Close() 198 <-outDoneCh 199 <-errDoneCh 200 if err != nil { 201 return err 202 } 203 } 204 205 o.Output(fmt.Sprintf("Creating remote temporary directory: %s", p.TempConfigDir)) 206 if err := p.createDir(o, comm, p.TempConfigDir); err != nil { 207 return fmt.Errorf("Error creating remote temporary directory: %s", err) 208 } 209 210 if p.MinionConfig != "" { 211 o.Output(fmt.Sprintf("Uploading minion config: %s", p.MinionConfig)) 212 src = p.MinionConfig 213 dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion")) 214 if err = p.uploadFile(o, comm, dst, src); err != nil { 215 return fmt.Errorf("Error uploading local minion config file to remote: %s", err) 216 } 217 218 // move minion config into /etc/salt 219 o.Output(fmt.Sprintf("Make sure directory %s exists", "/etc/salt")) 220 if err := p.createDir(o, comm, "/etc/salt"); err != nil { 221 return fmt.Errorf("Error creating remote salt configuration directory: %s", err) 222 } 223 src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion")) 224 dst = "/etc/salt/minion" 225 if err = p.moveFile(o, comm, dst, src); err != nil { 226 return fmt.Errorf("Unable to move %s/minion to /etc/salt/minion: %s", p.TempConfigDir, err) 227 } 228 } 229 230 o.Output(fmt.Sprintf("Uploading local state tree: %s", p.LocalStateTree)) 231 src = p.LocalStateTree 232 dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states")) 233 if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil { 234 return fmt.Errorf("Error uploading local state tree to remote: %s", err) 235 } 236 237 // move state tree from temporary directory 238 src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states")) 239 dst = p.RemoteStateTree 240 if err = p.removeDir(o, comm, dst); err != nil { 241 return fmt.Errorf("Unable to clear salt tree: %s", err) 242 } 243 if err = p.moveFile(o, comm, dst, src); err != nil { 244 return fmt.Errorf("Unable to move %s/states to %s: %s", p.TempConfigDir, dst, err) 245 } 246 247 if p.LocalPillarRoots != "" { 248 o.Output(fmt.Sprintf("Uploading local pillar roots: %s", p.LocalPillarRoots)) 249 src = p.LocalPillarRoots 250 dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar")) 251 if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil { 252 return fmt.Errorf("Error uploading local pillar roots to remote: %s", err) 253 } 254 255 // move pillar root from temporary directory 256 src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar")) 257 dst = p.RemotePillarRoots 258 259 if err = p.removeDir(o, comm, dst); err != nil { 260 return fmt.Errorf("Unable to clear pillar root: %s", err) 261 } 262 if err = p.moveFile(o, comm, dst, src); err != nil { 263 return fmt.Errorf("Unable to move %s/pillar to %s: %s", p.TempConfigDir, dst, err) 264 } 265 } 266 267 outR, outW := io.Pipe() 268 errR, errW := io.Pipe() 269 outDoneCh := make(chan struct{}) 270 errDoneCh := make(chan struct{}) 271 272 go copyOutput(o, outR, outDoneCh) 273 go copyOutput(o, errR, errDoneCh) 274 o.Output(fmt.Sprintf("Running: salt-call --local %s", p.CmdArgs)) 275 cmd := &remote.Cmd{ 276 Command: p.sudo(fmt.Sprintf("salt-call --local %s", p.CmdArgs)), 277 Stdout: outW, 278 Stderr: errW, 279 } 280 if err = comm.Start(cmd); err != nil || cmd.ExitStatus != 0 { 281 if err == nil { 282 err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus) 283 } 284 285 err = fmt.Errorf("Error executing salt-call: %s", err) 286 } 287 if err == nil { 288 cmd.Wait() 289 if cmd.ExitStatus != 0 { 290 err = fmt.Errorf("Script exited with non-zero exit status: %d", cmd.ExitStatus) 291 } 292 } 293 // Wait for output to clean up 294 outW.Close() 295 errW.Close() 296 <-outDoneCh 297 <-errDoneCh 298 299 return err 300 } 301 302 // Prepends sudo to supplied command if config says to 303 func (p *provisioner) sudo(cmd string) string { 304 if p.DisableSudo { 305 return cmd 306 } 307 308 return "sudo " + cmd 309 } 310 311 func validateDirConfig(path string, name string, required bool) error { 312 if required == true && path == "" { 313 return fmt.Errorf("%s cannot be empty", name) 314 } else if required == false && path == "" { 315 return nil 316 } 317 info, err := os.Stat(path) 318 if err != nil { 319 return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err) 320 } else if !info.IsDir() { 321 return fmt.Errorf("%s: path '%s' must point to a directory", name, path) 322 } 323 return nil 324 } 325 326 func validateFileConfig(path string, name string, required bool) error { 327 if required == true && path == "" { 328 return fmt.Errorf("%s cannot be empty", name) 329 } else if required == false && path == "" { 330 return nil 331 } 332 info, err := os.Stat(path) 333 if err != nil { 334 return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err) 335 } else if info.IsDir() { 336 return fmt.Errorf("%s: path '%s' must point to a file", name, path) 337 } 338 return nil 339 } 340 341 func (p *provisioner) uploadFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error { 342 f, err := os.Open(src) 343 if err != nil { 344 return fmt.Errorf("Error opening: %s", err) 345 } 346 defer f.Close() 347 348 if err = comm.Upload(dst, f); err != nil { 349 return fmt.Errorf("Error uploading %s: %s", src, err) 350 } 351 return nil 352 } 353 354 func (p *provisioner) moveFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error { 355 o.Output(fmt.Sprintf("Moving %s to %s", src, dst)) 356 cmd := &remote.Cmd{Command: fmt.Sprintf(p.sudo("mv %s %s"), src, dst)} 357 if err := comm.Start(cmd); err != nil || cmd.ExitStatus != 0 { 358 if err == nil { 359 err = fmt.Errorf("Bad exit status: %d", cmd.ExitStatus) 360 } 361 362 return fmt.Errorf("Unable to move %s to %s: %s", src, dst, err) 363 } 364 return nil 365 } 366 367 func (p *provisioner) createDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error { 368 o.Output(fmt.Sprintf("Creating directory: %s", dir)) 369 cmd := &remote.Cmd{ 370 Command: fmt.Sprintf("mkdir -p '%s'", dir), 371 } 372 if err := comm.Start(cmd); err != nil { 373 return err 374 } 375 if cmd.ExitStatus != 0 { 376 return fmt.Errorf("Non-zero exit status.") 377 } 378 return nil 379 } 380 381 func (p *provisioner) removeDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error { 382 o.Output(fmt.Sprintf("Removing directory: %s", dir)) 383 cmd := &remote.Cmd{ 384 Command: fmt.Sprintf("rm -rf '%s'", dir), 385 } 386 if err := comm.Start(cmd); err != nil { 387 return err 388 } 389 if cmd.ExitStatus != 0 { 390 return fmt.Errorf("Non-zero exit status.") 391 } 392 return nil 393 } 394 395 func (p *provisioner) uploadDir(o terraform.UIOutput, comm communicator.Communicator, dst, src string, ignore []string) error { 396 if err := p.createDir(o, comm, dst); err != nil { 397 return err 398 } 399 400 // Make sure there is a trailing "/" so that the directory isn't 401 // created on the other side. 402 if src[len(src)-1] != '/' { 403 src = src + "/" 404 } 405 return comm.UploadDir(dst, src) 406 } 407 408 // Validate checks if the required arguments are configured 409 func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) { 410 // require a salt state tree 411 localStateTreeTmp, ok := c.Get("local_state_tree") 412 var localStateTree string 413 if !ok { 414 es = append(es, 415 errors.New("Required local_state_tree is not set")) 416 } else { 417 localStateTree = localStateTreeTmp.(string) 418 } 419 err := validateDirConfig(localStateTree, "local_state_tree", true) 420 if err != nil { 421 es = append(es, err) 422 } 423 424 var localPillarRoots string 425 localPillarRootsTmp, ok := c.Get("local_pillar_roots") 426 if !ok { 427 localPillarRoots = "" 428 } else { 429 localPillarRoots = localPillarRootsTmp.(string) 430 } 431 432 err = validateDirConfig(localPillarRoots, "local_pillar_roots", false) 433 if err != nil { 434 es = append(es, err) 435 } 436 437 var minionConfig string 438 minionConfigTmp, ok := c.Get("minion_config_file") 439 if !ok { 440 minionConfig = "" 441 } else { 442 minionConfig = minionConfigTmp.(string) 443 } 444 err = validateFileConfig(minionConfig, "minion_config_file", false) 445 if err != nil { 446 es = append(es, err) 447 } 448 449 var remoteStateTree string 450 remoteStateTreeTmp, ok := c.Get("remote_state_tree") 451 if !ok { 452 remoteStateTree = "" 453 } else { 454 remoteStateTree = remoteStateTreeTmp.(string) 455 } 456 457 var remotePillarRoots string 458 remotePillarRootsTmp, ok := c.Get("remote_pillar_roots") 459 if !ok { 460 remotePillarRoots = "" 461 } else { 462 remotePillarRoots = remotePillarRootsTmp.(string) 463 } 464 465 if minionConfig != "" && (remoteStateTree != "" || remotePillarRoots != "") { 466 es = append(es, 467 errors.New("remote_state_tree and remote_pillar_roots only apply when minion_config_file is not used")) 468 } 469 470 if len(es) > 0 { 471 return ws, es 472 } 473 474 return ws, es 475 } 476 477 func decodeConfig(d *schema.ResourceData) (*provisioner, error) { 478 p := &provisioner{ 479 LocalStateTree: d.Get("local_state_tree").(string), 480 LogLevel: d.Get("log_level").(string), 481 SaltCallArgs: d.Get("salt_call_args").(string), 482 CmdArgs: d.Get("cmd_args").(string), 483 MinionConfig: d.Get("minion_config_file").(string), 484 CustomState: d.Get("custom_state").(string), 485 DisableSudo: d.Get("disable_sudo").(bool), 486 BootstrapArgs: d.Get("bootstrap_args").(string), 487 NoExitOnFailure: d.Get("no_exit_on_failure").(bool), 488 SkipBootstrap: d.Get("skip_bootstrap").(bool), 489 TempConfigDir: d.Get("temp_config_dir").(string), 490 RemotePillarRoots: d.Get("remote_pillar_roots").(string), 491 RemoteStateTree: d.Get("remote_state_tree").(string), 492 LocalPillarRoots: d.Get("local_pillar_roots").(string), 493 } 494 495 // build the command line args to pass onto salt 496 var cmdArgs bytes.Buffer 497 498 if p.CustomState == "" { 499 cmdArgs.WriteString(" state.highstate") 500 } else { 501 cmdArgs.WriteString(" state.sls ") 502 cmdArgs.WriteString(p.CustomState) 503 } 504 505 if p.MinionConfig == "" { 506 // pass --file-root and --pillar-root if no minion_config_file is supplied 507 if p.RemoteStateTree != "" { 508 cmdArgs.WriteString(" --file-root=") 509 cmdArgs.WriteString(p.RemoteStateTree) 510 } else { 511 cmdArgs.WriteString(" --file-root=") 512 cmdArgs.WriteString(DefaultStateTreeDir) 513 } 514 if p.RemotePillarRoots != "" { 515 cmdArgs.WriteString(" --pillar-root=") 516 cmdArgs.WriteString(p.RemotePillarRoots) 517 } else { 518 cmdArgs.WriteString(" --pillar-root=") 519 cmdArgs.WriteString(DefaultPillarRootDir) 520 } 521 } 522 523 if !p.NoExitOnFailure { 524 cmdArgs.WriteString(" --retcode-passthrough") 525 } 526 527 if p.LogLevel == "" { 528 cmdArgs.WriteString(" -l info") 529 } else { 530 cmdArgs.WriteString(" -l ") 531 cmdArgs.WriteString(p.LogLevel) 532 } 533 534 if p.SaltCallArgs != "" { 535 cmdArgs.WriteString(" ") 536 cmdArgs.WriteString(p.SaltCallArgs) 537 } 538 539 p.CmdArgs = cmdArgs.String() 540 541 return p, nil 542 } 543 544 func copyOutput( 545 o terraform.UIOutput, r io.Reader, doneCh chan<- struct{}) { 546 defer close(doneCh) 547 lr := linereader.New(r) 548 for line := range lr.Ch { 549 o.Output(line) 550 } 551 }