terraform/builtin/provisioners/puppet/resource_provisioner.go

360 lines
8.6 KiB
Go

package puppet
import (
"bytes"
"context"
"fmt"
"io"
"strings"
"sync"
"time"
"github.com/hashicorp/terraform/builtin/provisioners/puppet/bolt"
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/go-linereader"
"gopkg.in/yaml.v2"
)
type provisioner struct {
Server string
ServerUser string
OSType string
Certname string
Environment string
Autosign bool
OpenSource bool
UseSudo bool
BoltTimeout time.Duration
CustomAttributes map[string]interface{}
ExtensionRequests map[string]interface{}
runPuppetAgent func() error
installPuppetAgent func() error
uploadFile func(f io.Reader, dir string, filename string) error
defaultCertname func() (string, error)
instanceState *terraform.InstanceState
output terraform.UIOutput
comm communicator.Communicator
outputWG sync.WaitGroup
}
type csrAttributes struct {
CustomAttributes map[string]string `yaml:"custom_attributes"`
ExtensionRequests map[string]string `yaml:"extension_requests"`
}
// Provisioner returns a Puppet resource provisioner.
func Provisioner() terraform.ResourceProvisioner {
return &schema.Provisioner{
Schema: map[string]*schema.Schema{
"server": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"server_user": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "root",
},
"os_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringInSlice([]string{"linux", "windows"}, false),
},
"use_sudo": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"autosign": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"open_source": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"certname": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"extension_requests": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
},
"custom_attributes": &schema.Schema{
Type: schema.TypeMap,
Optional: true,
},
"environment": &schema.Schema{
Type: schema.TypeString,
Default: "production",
Optional: true,
},
"bolt_timeout": &schema.Schema{
Type: schema.TypeString,
Default: "5m",
Optional: true,
ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) {
_, err := time.ParseDuration(val.(string))
if err != nil {
errs = append(errs, err)
}
return warns, errs
},
},
},
ApplyFunc: applyFn,
}
}
func applyFn(ctx context.Context) error {
output := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
state := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
configData := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
p, err := decodeConfig(configData)
if err != nil {
return err
}
p.instanceState = state
p.output = output
if p.OSType == "" {
switch connType := state.Ephemeral.ConnInfo["type"]; connType {
case "ssh", "": // The default connection type is ssh, so if the type is empty assume ssh
p.OSType = "linux"
case "winrm":
p.OSType = "windows"
default:
return fmt.Errorf("Unsupported connection type: %s", connType)
}
}
switch p.OSType {
case "linux":
p.runPuppetAgent = p.linuxRunPuppetAgent
p.installPuppetAgent = p.linuxInstallPuppetAgent
p.uploadFile = p.linuxUploadFile
p.defaultCertname = p.linuxDefaultCertname
case "windows":
p.runPuppetAgent = p.windowsRunPuppetAgent
p.installPuppetAgent = p.windowsInstallPuppetAgent
p.uploadFile = p.windowsUploadFile
p.UseSudo = false
p.defaultCertname = p.windowsDefaultCertname
default:
return fmt.Errorf("Unsupported OS type: %s", p.OSType)
}
comm, err := communicator.New(state)
if err != nil {
return err
}
retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
defer cancel()
err = communicator.Retry(retryCtx, func() error {
return comm.Connect(output)
})
if err != nil {
return err
}
defer comm.Disconnect()
p.comm = comm
if p.OpenSource {
p.installPuppetAgent = p.installPuppetAgentOpenSource
}
csrAttrs := new(csrAttributes)
csrAttrs.CustomAttributes = make(map[string]string)
for k, v := range p.CustomAttributes {
csrAttrs.CustomAttributes[k] = v.(string)
}
csrAttrs.ExtensionRequests = make(map[string]string)
for k, v := range p.ExtensionRequests {
csrAttrs.ExtensionRequests[k] = v.(string)
}
if p.Autosign {
if p.Certname == "" {
p.Certname, _ = p.defaultCertname()
}
autosignToken, err := p.generateAutosignToken(p.Certname)
if err != nil {
return fmt.Errorf("Failed to generate an autosign token: %s", err)
}
csrAttrs.CustomAttributes["challengePassword"] = autosignToken
}
if err = p.writeCSRAttributes(csrAttrs); err != nil {
return fmt.Errorf("Failed to write csr_attributes.yaml: %s", err)
}
if err = p.installPuppetAgent(); err != nil {
return err
}
if err = p.runPuppetAgent(); err != nil {
return err
}
return nil
}
func (p *provisioner) writeCSRAttributes(attrs *csrAttributes) (rerr error) {
content, err := yaml.Marshal(attrs)
if err != nil {
return fmt.Errorf("Failed to marshal CSR attributes to YAML: %s", err)
}
configDir := map[string]string{
"linux": "/etc/puppetlabs/puppet",
"windows": "C:\\ProgramData\\PuppetLabs\\Puppet\\etc",
}
return p.uploadFile(bytes.NewBuffer(content), configDir[p.OSType], "csr_attributes.yaml")
}
func (p *provisioner) generateAutosignToken(certname string) (string, error) {
task := "autosign::generate_token"
masterConnInfo := map[string]string{
"type": "ssh",
"host": p.Server,
"user": p.ServerUser,
}
result, err := bolt.Task(
masterConnInfo,
p.BoltTimeout,
p.ServerUser != "root",
task,
map[string]string{"certname": certname},
)
if err != nil {
return "", err
}
if result.Items[0].Status != "success" {
return "", fmt.Errorf("Bolt %s failed on %s: %v",
task,
result.Items[0].Node,
result.Items[0].Result["_error"],
)
}
return result.Items[0].Result["_output"], nil
}
func (p *provisioner) installPuppetAgentOpenSource() error {
task := "puppet_agent::install"
connType := p.instanceState.Ephemeral.ConnInfo["type"]
if connType == "" {
connType = "ssh"
}
agentConnInfo := map[string]string{
"type": connType,
"host": p.instanceState.Ephemeral.ConnInfo["host"],
"user": p.instanceState.Ephemeral.ConnInfo["user"],
"password": p.instanceState.Ephemeral.ConnInfo["password"], // Required on Windows only
}
result, err := bolt.Task(
agentConnInfo,
p.BoltTimeout,
p.UseSudo,
task,
nil,
)
if err != nil || result.Items[0].Status != "success" {
return fmt.Errorf("%s failed: %s\n%+v", task, err, result)
}
return nil
}
func (p *provisioner) runCommand(command string) (stdout string, err error) {
if p.UseSudo {
command = "sudo " + command
}
var stdoutBuffer bytes.Buffer
outR, outW := io.Pipe()
errR, errW := io.Pipe()
outTee := io.TeeReader(outR, &stdoutBuffer)
p.outputWG.Add(2)
go p.copyToOutput(outTee)
go p.copyToOutput(errR)
defer outW.Close()
defer errW.Close()
cmd := &remote.Cmd{
Command: command,
Stdout: outW,
Stderr: errW,
}
err = p.comm.Start(cmd)
if err != nil {
err = fmt.Errorf("Error executing command %q: %v", cmd.Command, err)
return stdout, err
}
err = cmd.Wait()
outW.Close()
errW.Close()
p.outputWG.Wait()
stdout = strings.TrimSpace(stdoutBuffer.String())
return stdout, err
}
func (p *provisioner) copyToOutput(reader io.Reader) {
defer p.outputWG.Done()
lr := linereader.New(reader)
for line := range lr.Ch {
p.output.Output(line)
}
}
func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
p := &provisioner{
UseSudo: d.Get("use_sudo").(bool),
Server: d.Get("server").(string),
ServerUser: d.Get("server_user").(string),
OSType: strings.ToLower(d.Get("os_type").(string)),
Autosign: d.Get("autosign").(bool),
OpenSource: d.Get("open_source").(bool),
Certname: strings.ToLower(d.Get("certname").(string)),
ExtensionRequests: d.Get("extension_requests").(map[string]interface{}),
CustomAttributes: d.Get("custom_attributes").(map[string]interface{}),
Environment: d.Get("environment").(string),
}
p.BoltTimeout, _ = time.ParseDuration(d.Get("bolt_timeout").(string))
return p, nil
}