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 }