github.com/rvichery/terraform@v0.11.10/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 o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput) 118 d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData) 119 connState := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState) 120 121 p, err := decodeConfig(d) 122 if err != nil { 123 return err 124 } 125 126 // Get a new communicator 127 comm, err := communicator.New(connState) 128 if err != nil { 129 return err 130 } 131 132 retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout()) 133 defer cancel() 134 135 // Wait and retry until we establish the connection 136 err = communicator.Retry(retryCtx, func() error { 137 return comm.Connect(o) 138 }) 139 140 if err != nil { 141 return err 142 } 143 144 // Wait for the context to end and then disconnect 145 go func() { 146 <-ctx.Done() 147 comm.Disconnect() 148 }() 149 150 var src, dst string 151 152 o.Output("Provisioning with Salt...") 153 if !p.SkipBootstrap { 154 cmd := &remote.Cmd{ 155 // Fallback on wget if curl failed for any reason (such as not being installed) 156 Command: fmt.Sprintf("curl -L https://bootstrap.saltstack.com -o /tmp/install_salt.sh || wget -O /tmp/install_salt.sh https://bootstrap.saltstack.com"), 157 } 158 o.Output(fmt.Sprintf("Downloading saltstack bootstrap to /tmp/install_salt.sh")) 159 if err = comm.Start(cmd); err != nil { 160 err = fmt.Errorf("Unable to download Salt: %s", err) 161 } 162 163 if err := cmd.Wait(); err != nil { 164 return err 165 } 166 167 outR, outW := io.Pipe() 168 errR, errW := io.Pipe() 169 go copyOutput(o, outR) 170 go copyOutput(o, errR) 171 defer outW.Close() 172 defer errW.Close() 173 174 cmd = &remote.Cmd{ 175 Command: fmt.Sprintf("%s /tmp/install_salt.sh %s", p.sudo("sh"), p.BootstrapArgs), 176 Stdout: outW, 177 Stderr: errW, 178 } 179 180 o.Output(fmt.Sprintf("Installing Salt with command %s", cmd.Command)) 181 if err := comm.Start(cmd); err != nil { 182 return fmt.Errorf("Unable to install Salt: %s", err) 183 } 184 185 if err := cmd.Wait(); err != nil { 186 return err 187 } 188 } 189 190 o.Output(fmt.Sprintf("Creating remote temporary directory: %s", p.TempConfigDir)) 191 if err := p.createDir(o, comm, p.TempConfigDir); err != nil { 192 return fmt.Errorf("Error creating remote temporary directory: %s", err) 193 } 194 195 if p.MinionConfig != "" { 196 o.Output(fmt.Sprintf("Uploading minion config: %s", p.MinionConfig)) 197 src = p.MinionConfig 198 dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion")) 199 if err = p.uploadFile(o, comm, dst, src); err != nil { 200 return fmt.Errorf("Error uploading local minion config file to remote: %s", err) 201 } 202 203 // move minion config into /etc/salt 204 o.Output(fmt.Sprintf("Make sure directory %s exists", "/etc/salt")) 205 if err := p.createDir(o, comm, "/etc/salt"); err != nil { 206 return fmt.Errorf("Error creating remote salt configuration directory: %s", err) 207 } 208 src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion")) 209 dst = "/etc/salt/minion" 210 if err = p.moveFile(o, comm, dst, src); err != nil { 211 return fmt.Errorf("Unable to move %s/minion to /etc/salt/minion: %s", p.TempConfigDir, err) 212 } 213 } 214 215 o.Output(fmt.Sprintf("Uploading local state tree: %s", p.LocalStateTree)) 216 src = p.LocalStateTree 217 dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states")) 218 if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil { 219 return fmt.Errorf("Error uploading local state tree to remote: %s", err) 220 } 221 222 // move state tree from temporary directory 223 src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states")) 224 dst = p.RemoteStateTree 225 if err = p.removeDir(o, comm, dst); err != nil { 226 return fmt.Errorf("Unable to clear salt tree: %s", err) 227 } 228 if err = p.moveFile(o, comm, dst, src); err != nil { 229 return fmt.Errorf("Unable to move %s/states to %s: %s", p.TempConfigDir, dst, err) 230 } 231 232 if p.LocalPillarRoots != "" { 233 o.Output(fmt.Sprintf("Uploading local pillar roots: %s", p.LocalPillarRoots)) 234 src = p.LocalPillarRoots 235 dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar")) 236 if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil { 237 return fmt.Errorf("Error uploading local pillar roots to remote: %s", err) 238 } 239 240 // move pillar root from temporary directory 241 src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar")) 242 dst = p.RemotePillarRoots 243 244 if err = p.removeDir(o, comm, dst); err != nil { 245 return fmt.Errorf("Unable to clear pillar root: %s", err) 246 } 247 if err = p.moveFile(o, comm, dst, src); err != nil { 248 return fmt.Errorf("Unable to move %s/pillar to %s: %s", p.TempConfigDir, dst, err) 249 } 250 } 251 252 outR, outW := io.Pipe() 253 errR, errW := io.Pipe() 254 go copyOutput(o, outR) 255 go copyOutput(o, errR) 256 defer outW.Close() 257 defer errW.Close() 258 259 o.Output(fmt.Sprintf("Running: salt-call --local %s", p.CmdArgs)) 260 cmd := &remote.Cmd{ 261 Command: p.sudo(fmt.Sprintf("salt-call --local %s", p.CmdArgs)), 262 Stdout: outW, 263 Stderr: errW, 264 } 265 if err = comm.Start(cmd); err != nil { 266 err = fmt.Errorf("Error executing salt-call: %s", err) 267 } 268 269 if err := cmd.Wait(); err != nil { 270 return err 271 } 272 return nil 273 } 274 275 // Prepends sudo to supplied command if config says to 276 func (p *provisioner) sudo(cmd string) string { 277 if p.DisableSudo { 278 return cmd 279 } 280 281 return "sudo " + cmd 282 } 283 284 func validateDirConfig(path string, name string, required bool) error { 285 if required == true && path == "" { 286 return fmt.Errorf("%s cannot be empty", name) 287 } else if required == false && path == "" { 288 return nil 289 } 290 info, err := os.Stat(path) 291 if err != nil { 292 return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err) 293 } else if !info.IsDir() { 294 return fmt.Errorf("%s: path '%s' must point to a directory", name, path) 295 } 296 return nil 297 } 298 299 func validateFileConfig(path string, name string, required bool) error { 300 if required == true && path == "" { 301 return fmt.Errorf("%s cannot be empty", name) 302 } else if required == false && path == "" { 303 return nil 304 } 305 info, err := os.Stat(path) 306 if err != nil { 307 return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err) 308 } else if info.IsDir() { 309 return fmt.Errorf("%s: path '%s' must point to a file", name, path) 310 } 311 return nil 312 } 313 314 func (p *provisioner) uploadFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error { 315 f, err := os.Open(src) 316 if err != nil { 317 return fmt.Errorf("Error opening: %s", err) 318 } 319 defer f.Close() 320 321 if err = comm.Upload(dst, f); err != nil { 322 return fmt.Errorf("Error uploading %s: %s", src, err) 323 } 324 return nil 325 } 326 327 func (p *provisioner) moveFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error { 328 o.Output(fmt.Sprintf("Moving %s to %s", src, dst)) 329 cmd := &remote.Cmd{Command: fmt.Sprintf(p.sudo("mv %s %s"), src, dst)} 330 if err := comm.Start(cmd); err != nil { 331 return fmt.Errorf("Unable to move %s to %s: %s", src, dst, err) 332 } 333 if err := cmd.Wait(); err != nil { 334 return err 335 } 336 return nil 337 } 338 339 func (p *provisioner) createDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error { 340 o.Output(fmt.Sprintf("Creating directory: %s", dir)) 341 cmd := &remote.Cmd{ 342 Command: fmt.Sprintf("mkdir -p '%s'", dir), 343 } 344 if err := comm.Start(cmd); err != nil { 345 return err 346 } 347 348 if err := cmd.Wait(); err != nil { 349 return err 350 } 351 return nil 352 } 353 354 func (p *provisioner) removeDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error { 355 o.Output(fmt.Sprintf("Removing directory: %s", dir)) 356 cmd := &remote.Cmd{ 357 Command: fmt.Sprintf("rm -rf '%s'", dir), 358 } 359 if err := comm.Start(cmd); err != nil { 360 return err 361 } 362 if err := cmd.Wait(); err != nil { 363 return err 364 } 365 return nil 366 } 367 368 func (p *provisioner) uploadDir(o terraform.UIOutput, comm communicator.Communicator, dst, src string, ignore []string) error { 369 if err := p.createDir(o, comm, dst); err != nil { 370 return err 371 } 372 373 // Make sure there is a trailing "/" so that the directory isn't 374 // created on the other side. 375 if src[len(src)-1] != '/' { 376 src = src + "/" 377 } 378 return comm.UploadDir(dst, src) 379 } 380 381 // Validate checks if the required arguments are configured 382 func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) { 383 // require a salt state tree 384 localStateTreeTmp, ok := c.Get("local_state_tree") 385 var localStateTree string 386 if !ok { 387 es = append(es, 388 errors.New("Required local_state_tree is not set")) 389 } else { 390 localStateTree = localStateTreeTmp.(string) 391 } 392 err := validateDirConfig(localStateTree, "local_state_tree", true) 393 if err != nil { 394 es = append(es, err) 395 } 396 397 var localPillarRoots string 398 localPillarRootsTmp, ok := c.Get("local_pillar_roots") 399 if !ok { 400 localPillarRoots = "" 401 } else { 402 localPillarRoots = localPillarRootsTmp.(string) 403 } 404 405 err = validateDirConfig(localPillarRoots, "local_pillar_roots", false) 406 if err != nil { 407 es = append(es, err) 408 } 409 410 var minionConfig string 411 minionConfigTmp, ok := c.Get("minion_config_file") 412 if !ok { 413 minionConfig = "" 414 } else { 415 minionConfig = minionConfigTmp.(string) 416 } 417 err = validateFileConfig(minionConfig, "minion_config_file", false) 418 if err != nil { 419 es = append(es, err) 420 } 421 422 var remoteStateTree string 423 remoteStateTreeTmp, ok := c.Get("remote_state_tree") 424 if !ok { 425 remoteStateTree = "" 426 } else { 427 remoteStateTree = remoteStateTreeTmp.(string) 428 } 429 430 var remotePillarRoots string 431 remotePillarRootsTmp, ok := c.Get("remote_pillar_roots") 432 if !ok { 433 remotePillarRoots = "" 434 } else { 435 remotePillarRoots = remotePillarRootsTmp.(string) 436 } 437 438 if minionConfig != "" && (remoteStateTree != "" || remotePillarRoots != "") { 439 es = append(es, 440 errors.New("remote_state_tree and remote_pillar_roots only apply when minion_config_file is not used")) 441 } 442 443 if len(es) > 0 { 444 return ws, es 445 } 446 447 return ws, es 448 } 449 450 func decodeConfig(d *schema.ResourceData) (*provisioner, error) { 451 p := &provisioner{ 452 LocalStateTree: d.Get("local_state_tree").(string), 453 LogLevel: d.Get("log_level").(string), 454 SaltCallArgs: d.Get("salt_call_args").(string), 455 CmdArgs: d.Get("cmd_args").(string), 456 MinionConfig: d.Get("minion_config_file").(string), 457 CustomState: d.Get("custom_state").(string), 458 DisableSudo: d.Get("disable_sudo").(bool), 459 BootstrapArgs: d.Get("bootstrap_args").(string), 460 NoExitOnFailure: d.Get("no_exit_on_failure").(bool), 461 SkipBootstrap: d.Get("skip_bootstrap").(bool), 462 TempConfigDir: d.Get("temp_config_dir").(string), 463 RemotePillarRoots: d.Get("remote_pillar_roots").(string), 464 RemoteStateTree: d.Get("remote_state_tree").(string), 465 LocalPillarRoots: d.Get("local_pillar_roots").(string), 466 } 467 468 // build the command line args to pass onto salt 469 var cmdArgs bytes.Buffer 470 471 if p.CustomState == "" { 472 cmdArgs.WriteString(" state.highstate") 473 } else { 474 cmdArgs.WriteString(" state.sls ") 475 cmdArgs.WriteString(p.CustomState) 476 } 477 478 if p.MinionConfig == "" { 479 // pass --file-root and --pillar-root if no minion_config_file is supplied 480 if p.RemoteStateTree != "" { 481 cmdArgs.WriteString(" --file-root=") 482 cmdArgs.WriteString(p.RemoteStateTree) 483 } else { 484 cmdArgs.WriteString(" --file-root=") 485 cmdArgs.WriteString(DefaultStateTreeDir) 486 } 487 if p.RemotePillarRoots != "" { 488 cmdArgs.WriteString(" --pillar-root=") 489 cmdArgs.WriteString(p.RemotePillarRoots) 490 } else { 491 cmdArgs.WriteString(" --pillar-root=") 492 cmdArgs.WriteString(DefaultPillarRootDir) 493 } 494 } 495 496 if !p.NoExitOnFailure { 497 cmdArgs.WriteString(" --retcode-passthrough") 498 } 499 500 if p.LogLevel == "" { 501 cmdArgs.WriteString(" -l info") 502 } else { 503 cmdArgs.WriteString(" -l ") 504 cmdArgs.WriteString(p.LogLevel) 505 } 506 507 if p.SaltCallArgs != "" { 508 cmdArgs.WriteString(" ") 509 cmdArgs.WriteString(p.SaltCallArgs) 510 } 511 512 p.CmdArgs = cmdArgs.String() 513 514 return p, nil 515 } 516 517 func copyOutput( 518 o terraform.UIOutput, r io.Reader) { 519 lr := linereader.New(r) 520 for line := range lr.Ch { 521 o.Output(line) 522 } 523 }