github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/builtin/provisioners/puppet/resource_provisioner.go (about)

     1  package puppet
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"io"
     8  	"strings"
     9  	"time"
    10  
    11  	"github.com/hashicorp/terraform/builtin/provisioners/puppet/bolt"
    12  	"github.com/hashicorp/terraform/communicator"
    13  	"github.com/hashicorp/terraform/communicator/remote"
    14  	"github.com/hashicorp/terraform/helper/schema"
    15  	"github.com/hashicorp/terraform/helper/validation"
    16  	"github.com/hashicorp/terraform/terraform"
    17  	"github.com/mitchellh/go-linereader"
    18  	"gopkg.in/yaml.v2"
    19  )
    20  
    21  type provisioner struct {
    22  	Server            string
    23  	ServerUser        string
    24  	OSType            string
    25  	Certname          string
    26  	Environment       string
    27  	Autosign          bool
    28  	OpenSource        bool
    29  	UseSudo           bool
    30  	BoltTimeout       time.Duration
    31  	CustomAttributes  map[string]interface{}
    32  	ExtensionRequests map[string]interface{}
    33  
    34  	runPuppetAgent     func() error
    35  	installPuppetAgent func() error
    36  	uploadFile         func(f io.Reader, dir string, filename string) error
    37  	defaultCertname    func() (string, error)
    38  
    39  	instanceState *terraform.InstanceState
    40  	output        terraform.UIOutput
    41  	comm          communicator.Communicator
    42  }
    43  
    44  type csrAttributes struct {
    45  	CustomAttributes  map[string]string `yaml:"custom_attributes"`
    46  	ExtensionRequests map[string]string `yaml:"extension_requests"`
    47  }
    48  
    49  // Provisioner returns a Puppet resource provisioner.
    50  func Provisioner() terraform.ResourceProvisioner {
    51  	return &schema.Provisioner{
    52  		Schema: map[string]*schema.Schema{
    53  			"server": &schema.Schema{
    54  				Type:     schema.TypeString,
    55  				Required: true,
    56  			},
    57  			"server_user": &schema.Schema{
    58  				Type:     schema.TypeString,
    59  				Optional: true,
    60  				Default:  "root",
    61  			},
    62  			"os_type": &schema.Schema{
    63  				Type:         schema.TypeString,
    64  				Optional:     true,
    65  				ValidateFunc: validation.StringInSlice([]string{"linux", "windows"}, false),
    66  			},
    67  			"use_sudo": &schema.Schema{
    68  				Type:     schema.TypeBool,
    69  				Optional: true,
    70  				Default:  true,
    71  			},
    72  			"autosign": &schema.Schema{
    73  				Type:     schema.TypeBool,
    74  				Optional: true,
    75  				Default:  true,
    76  			},
    77  			"open_source": &schema.Schema{
    78  				Type:     schema.TypeBool,
    79  				Optional: true,
    80  				Default:  true,
    81  			},
    82  			"certname": &schema.Schema{
    83  				Type:     schema.TypeString,
    84  				Optional: true,
    85  			},
    86  			"extension_requests": &schema.Schema{
    87  				Type:     schema.TypeMap,
    88  				Optional: true,
    89  			},
    90  			"custom_attributes": &schema.Schema{
    91  				Type:     schema.TypeMap,
    92  				Optional: true,
    93  			},
    94  			"environment": &schema.Schema{
    95  				Type:     schema.TypeString,
    96  				Default:  "production",
    97  				Optional: true,
    98  			},
    99  			"bolt_timeout": &schema.Schema{
   100  				Type:     schema.TypeString,
   101  				Default:  "5m",
   102  				Optional: true,
   103  				ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) {
   104  					_, err := time.ParseDuration(val.(string))
   105  					if err != nil {
   106  						errs = append(errs, err)
   107  					}
   108  					return warns, errs
   109  				},
   110  			},
   111  		},
   112  		ApplyFunc: applyFn,
   113  	}
   114  }
   115  
   116  func applyFn(ctx context.Context) error {
   117  	output := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
   118  	state := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
   119  	configData := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
   120  
   121  	p, err := decodeConfig(configData)
   122  	if err != nil {
   123  		return err
   124  	}
   125  
   126  	p.instanceState = state
   127  	p.output = output
   128  
   129  	if p.OSType == "" {
   130  		switch connType := state.Ephemeral.ConnInfo["type"]; connType {
   131  		case "ssh", "": // The default connection type is ssh, so if the type is empty assume ssh
   132  			p.OSType = "linux"
   133  		case "winrm":
   134  			p.OSType = "windows"
   135  		default:
   136  			return fmt.Errorf("Unsupported connection type: %s", connType)
   137  		}
   138  	}
   139  
   140  	switch p.OSType {
   141  	case "linux":
   142  		p.runPuppetAgent = p.linuxRunPuppetAgent
   143  		p.installPuppetAgent = p.linuxInstallPuppetAgent
   144  		p.uploadFile = p.linuxUploadFile
   145  		p.defaultCertname = p.linuxDefaultCertname
   146  	case "windows":
   147  		p.runPuppetAgent = p.windowsRunPuppetAgent
   148  		p.installPuppetAgent = p.windowsInstallPuppetAgent
   149  		p.uploadFile = p.windowsUploadFile
   150  		p.UseSudo = false
   151  		p.defaultCertname = p.windowsDefaultCertname
   152  	default:
   153  		return fmt.Errorf("Unsupported OS type: %s", p.OSType)
   154  	}
   155  
   156  	comm, err := communicator.New(state)
   157  	if err != nil {
   158  		return err
   159  	}
   160  
   161  	retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
   162  	defer cancel()
   163  
   164  	err = communicator.Retry(retryCtx, func() error {
   165  		return comm.Connect(output)
   166  	})
   167  	if err != nil {
   168  		return err
   169  	}
   170  	defer comm.Disconnect()
   171  
   172  	p.comm = comm
   173  
   174  	if p.OpenSource {
   175  		p.installPuppetAgent = p.installPuppetAgentOpenSource
   176  	}
   177  
   178  	csrAttrs := new(csrAttributes)
   179  	csrAttrs.CustomAttributes = make(map[string]string)
   180  	for k, v := range p.CustomAttributes {
   181  		csrAttrs.CustomAttributes[k] = v.(string)
   182  	}
   183  
   184  	csrAttrs.ExtensionRequests = make(map[string]string)
   185  	for k, v := range p.ExtensionRequests {
   186  		csrAttrs.ExtensionRequests[k] = v.(string)
   187  	}
   188  
   189  	if p.Autosign {
   190  		if p.Certname == "" {
   191  			p.Certname, _ = p.defaultCertname()
   192  		}
   193  
   194  		autosignToken, err := p.generateAutosignToken(p.Certname)
   195  		if err != nil {
   196  			return fmt.Errorf("Failed to generate an autosign token: %s", err)
   197  		}
   198  		csrAttrs.CustomAttributes["challengePassword"] = autosignToken
   199  	}
   200  
   201  	if err = p.writeCSRAttributes(csrAttrs); err != nil {
   202  		return fmt.Errorf("Failed to write csr_attributes.yaml: %s", err)
   203  	}
   204  
   205  	if err = p.installPuppetAgent(); err != nil {
   206  		return err
   207  	}
   208  
   209  	if err = p.runPuppetAgent(); err != nil {
   210  		return err
   211  	}
   212  
   213  	return nil
   214  }
   215  
   216  func (p *provisioner) writeCSRAttributes(attrs *csrAttributes) (rerr error) {
   217  	content, err := yaml.Marshal(attrs)
   218  	if err != nil {
   219  		return fmt.Errorf("Failed to marshal CSR attributes to YAML: %s", err)
   220  	}
   221  
   222  	configDir := map[string]string{
   223  		"linux":   "/etc/puppetlabs/puppet",
   224  		"windows": "C:\\ProgramData\\PuppetLabs\\Puppet\\etc",
   225  	}
   226  
   227  	return p.uploadFile(bytes.NewBuffer(content), configDir[p.OSType], "csr_attributes.yaml")
   228  }
   229  
   230  func (p *provisioner) generateAutosignToken(certname string) (string, error) {
   231  	task := "autosign::generate_token"
   232  
   233  	masterConnInfo := map[string]string{
   234  		"type": "ssh",
   235  		"host": p.Server,
   236  		"user": p.ServerUser,
   237  	}
   238  
   239  	result, err := bolt.Task(
   240  		masterConnInfo,
   241  		p.BoltTimeout,
   242  		p.ServerUser != "root",
   243  		task,
   244  		map[string]string{"certname": certname},
   245  	)
   246  	if err != nil {
   247  		return "", err
   248  	}
   249  
   250  	if result.Items[0].Status != "success" {
   251  		return "", fmt.Errorf("Bolt %s failed on %s: %v",
   252  			task,
   253  			result.Items[0].Node,
   254  			result.Items[0].Result["_error"],
   255  		)
   256  	}
   257  
   258  	return result.Items[0].Result["_output"], nil
   259  }
   260  
   261  func (p *provisioner) installPuppetAgentOpenSource() error {
   262  	task := "puppet_agent::install"
   263  
   264  	connType := p.instanceState.Ephemeral.ConnInfo["type"]
   265  	if connType == "" {
   266  		connType = "ssh"
   267  	}
   268  
   269  	agentConnInfo := map[string]string{
   270  		"type":     connType,
   271  		"host":     p.instanceState.Ephemeral.ConnInfo["host"],
   272  		"user":     p.instanceState.Ephemeral.ConnInfo["user"],
   273  		"password": p.instanceState.Ephemeral.ConnInfo["password"], // Required on Windows only
   274  	}
   275  
   276  	result, err := bolt.Task(
   277  		agentConnInfo,
   278  		p.BoltTimeout,
   279  		p.UseSudo,
   280  		task,
   281  		nil,
   282  	)
   283  
   284  	if err != nil || result.Items[0].Status != "success" {
   285  		return fmt.Errorf("%s failed: %s\n%+v", task, err, result)
   286  	}
   287  
   288  	return nil
   289  }
   290  
   291  func (p *provisioner) runCommand(command string) (stdout string, err error) {
   292  	if p.UseSudo {
   293  		command = "sudo " + command
   294  	}
   295  
   296  	var stdoutBuffer bytes.Buffer
   297  	outR, outW := io.Pipe()
   298  	errR, errW := io.Pipe()
   299  	outTee := io.TeeReader(outR, &stdoutBuffer)
   300  	go p.copyToOutput(outTee)
   301  	go p.copyToOutput(errR)
   302  	defer outW.Close()
   303  	defer errW.Close()
   304  
   305  	cmd := &remote.Cmd{
   306  		Command: command,
   307  		Stdout:  outW,
   308  		Stderr:  errW,
   309  	}
   310  
   311  	err = p.comm.Start(cmd)
   312  	if err != nil {
   313  		err = fmt.Errorf("Error executing command %q: %v", cmd.Command, err)
   314  		return stdout, err
   315  	}
   316  
   317  	err = cmd.Wait()
   318  	stdout = strings.TrimSpace(stdoutBuffer.String())
   319  
   320  	return stdout, err
   321  }
   322  
   323  func (p *provisioner) copyToOutput(reader io.Reader) {
   324  	lr := linereader.New(reader)
   325  	for line := range lr.Ch {
   326  		p.output.Output(line)
   327  	}
   328  }
   329  
   330  func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
   331  	p := &provisioner{
   332  		UseSudo:           d.Get("use_sudo").(bool),
   333  		Server:            d.Get("server").(string),
   334  		ServerUser:        d.Get("server_user").(string),
   335  		OSType:            strings.ToLower(d.Get("os_type").(string)),
   336  		Autosign:          d.Get("autosign").(bool),
   337  		OpenSource:        d.Get("open_source").(bool),
   338  		Certname:          strings.ToLower(d.Get("certname").(string)),
   339  		ExtensionRequests: d.Get("extension_requests").(map[string]interface{}),
   340  		CustomAttributes:  d.Get("custom_attributes").(map[string]interface{}),
   341  		Environment:       d.Get("environment").(string),
   342  	}
   343  	p.BoltTimeout, _ = time.ParseDuration(d.Get("bolt_timeout").(string))
   344  
   345  	return p, nil
   346  }