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