provisioner: new Puppet provisioner (#18851)
* Basic Puppet provisioner * (fixup) fix snake_case use in Bolt * (fixup) Remove unused ValidateFunc * (fixup) Check bolt result status * (lint) go fmt * Requested changes * Remove PE autodetection * Apply suggestions from @svanharmelen Co-Authored-By: rodjek <tim@sharpe.id.au> * Tag all JSON fields in bolt output * Defer comm.Disconnect() as suggested * Make bolt timeout configurable * Update builtin/provisioners/puppet/resource_provisioner.go Co-Authored-By: rodjek <tim@sharpe.id.au> * Make extension_requests and custom_attributes configurable
This commit is contained in:
parent
0abb0a05fb
commit
615110e13e
|
@ -0,0 +1,12 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/terraform/builtin/provisioners/puppet"
|
||||||
|
"github.com/hashicorp/terraform/plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
plugin.Serve(&plugin.ServeOpts{
|
||||||
|
ProvisionerFunc: puppet.Provisioner,
|
||||||
|
})
|
||||||
|
}
|
|
@ -0,0 +1,74 @@
|
||||||
|
package bolt
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Result struct {
|
||||||
|
Items []struct {
|
||||||
|
Node string `json:"node"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Result map[string]string `json:"result"`
|
||||||
|
} `json:"items"`
|
||||||
|
NodeCount int `json:"node_count"`
|
||||||
|
ElapsedTime int `json:"elapsed_time"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func runCommand(command string, timeout time.Duration) ([]byte, error) {
|
||||||
|
var cmdargs []string
|
||||||
|
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
cmdargs = []string{"cmd", "/C"}
|
||||||
|
} else {
|
||||||
|
cmdargs = []string{"/bin/sh", "-c"}
|
||||||
|
}
|
||||||
|
cmdargs = append(cmdargs, command)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(ctx, cmdargs[0], cmdargs[1:]...)
|
||||||
|
return cmd.Output()
|
||||||
|
}
|
||||||
|
|
||||||
|
func Task(connInfo map[string]string, timeout time.Duration, sudo bool, task string, args map[string]string) (*Result, error) {
|
||||||
|
cmdargs := []string{
|
||||||
|
"bolt", "task", "run", "--nodes", connInfo["type"] + "://" + connInfo["host"], "-u", connInfo["user"],
|
||||||
|
}
|
||||||
|
|
||||||
|
if connInfo["type"] == "winrm" {
|
||||||
|
cmdargs = append(cmdargs, "-p", "\""+connInfo["password"]+"\"", "--no-ssl")
|
||||||
|
} else {
|
||||||
|
if sudo {
|
||||||
|
cmdargs = append(cmdargs, "--run-as", "root")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdargs = append(cmdargs, "--no-host-key-check")
|
||||||
|
}
|
||||||
|
|
||||||
|
cmdargs = append(cmdargs, "--format", "json", "--connect-timeout", "120", task)
|
||||||
|
|
||||||
|
if args != nil {
|
||||||
|
for key, value := range args {
|
||||||
|
cmdargs = append(cmdargs, strings.Join([]string{key, value}, "="))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := runCommand(strings.Join(cmdargs, " "), timeout)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Bolt: \"%s\": %s: %s", strings.Join(cmdargs, " "), out, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := new(Result)
|
||||||
|
if err = json.Unmarshal(out, result); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
|
@ -0,0 +1,65 @@
|
||||||
|
package puppet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/communicator/remote"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *provisioner) linuxUploadFile(f io.Reader, dir string, filename string) error {
|
||||||
|
_, err := p.runCommand("mkdir -p " + dir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to make directory %s: %s", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = p.comm.Upload("/tmp/"+filename, f)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to upload %s to /tmp: %s", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.runCommand(fmt.Sprintf("mv /tmp/%s %s/%s", filename, dir, filename))
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provisioner) linuxDefaultCertname() (string, error) {
|
||||||
|
certname, err := p.runCommand("hostname -f")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return certname, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provisioner) linuxInstallPuppetAgent() error {
|
||||||
|
_, err := p.runCommand(fmt.Sprintf("curl -kO https://%s:8140/packages/current/install.bash", p.Server))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.runCommand("bash -- ./install.bash --puppet-service-ensure stopped")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.runCommand("rm -f install.bash")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provisioner) linuxRunPuppetAgent() error {
|
||||||
|
_, err := p.runCommand(fmt.Sprintf(
|
||||||
|
"/opt/puppetlabs/puppet/bin/puppet agent --test --server %s --environment %s",
|
||||||
|
p.Server,
|
||||||
|
p.Environment,
|
||||||
|
))
|
||||||
|
|
||||||
|
// Puppet exits 2 if changes have been successfully made.
|
||||||
|
if err != nil {
|
||||||
|
errStruct, _ := err.(*remote.ExitError)
|
||||||
|
if errStruct.ExitStatus == 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,379 @@
|
||||||
|
package puppet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/communicator"
|
||||||
|
"github.com/hashicorp/terraform/communicator/remote"
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResourceProvisioner_linuxUploadFile(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
Config map[string]interface{}
|
||||||
|
Commands map[string]bool
|
||||||
|
CommandFunc func(*remote.Cmd) error
|
||||||
|
ExpectedError bool
|
||||||
|
Uploads map[string]string
|
||||||
|
File io.Reader
|
||||||
|
Dir string
|
||||||
|
Filename string
|
||||||
|
}{
|
||||||
|
"Successful upload": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
Commands: map[string]bool{
|
||||||
|
"mkdir -p /etc/puppetlabs/puppet": true,
|
||||||
|
"mv /tmp/csr_attributes.yaml /etc/puppetlabs/puppet/csr_attributes.yaml": true,
|
||||||
|
},
|
||||||
|
Uploads: map[string]string{
|
||||||
|
"/tmp/csr_attributes.yaml": "",
|
||||||
|
},
|
||||||
|
Dir: "/etc/puppetlabs/puppet",
|
||||||
|
Filename: "csr_attributes.yaml",
|
||||||
|
File: strings.NewReader(""),
|
||||||
|
},
|
||||||
|
"Failure when creating the directory": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
Commands: map[string]bool{
|
||||||
|
"mkdir -p /etc/puppetlabs/puppet": true,
|
||||||
|
},
|
||||||
|
Dir: "/etc/puppetlabs/puppet",
|
||||||
|
Filename: "csr_attributes.yaml",
|
||||||
|
File: strings.NewReader(""),
|
||||||
|
CommandFunc: func(r *remote.Cmd) error {
|
||||||
|
r.SetExitStatus(1, &remote.ExitError{
|
||||||
|
Command: "mkdir -p /etc/puppetlabs/puppet",
|
||||||
|
ExitStatus: 1,
|
||||||
|
Err: nil,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, tc := range cases {
|
||||||
|
p, err := decodeConfig(
|
||||||
|
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := new(communicator.MockCommunicator)
|
||||||
|
c.Commands = tc.Commands
|
||||||
|
c.Uploads = tc.Uploads
|
||||||
|
if tc.CommandFunc != nil {
|
||||||
|
c.CommandFunc = tc.CommandFunc
|
||||||
|
}
|
||||||
|
p.comm = c
|
||||||
|
p.output = new(terraform.MockUIOutput)
|
||||||
|
|
||||||
|
err = p.linuxUploadFile(tc.File, tc.Dir, tc.Filename)
|
||||||
|
if tc.ExpectedError {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error, but no error returned")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test %q failed: %v", k, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceProvisioner_linuxDefaultCertname(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
Config map[string]interface{}
|
||||||
|
Commands map[string]bool
|
||||||
|
CommandFunc func(*remote.Cmd) error
|
||||||
|
ExpectedError bool
|
||||||
|
}{
|
||||||
|
"No sudo": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
Commands: map[string]bool{
|
||||||
|
"hostname -f": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"With sudo": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": true,
|
||||||
|
},
|
||||||
|
Commands: map[string]bool{
|
||||||
|
"sudo hostname -f": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Failed execution": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
Commands: map[string]bool{
|
||||||
|
"hostname -f": true,
|
||||||
|
},
|
||||||
|
CommandFunc: func(r *remote.Cmd) error {
|
||||||
|
if r.Command == "hostname -f" {
|
||||||
|
r.SetExitStatus(1, &remote.ExitError{
|
||||||
|
Command: "hostname -f",
|
||||||
|
ExitStatus: 1,
|
||||||
|
Err: nil,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
r.SetExitStatus(0, nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, tc := range cases {
|
||||||
|
p, err := decodeConfig(
|
||||||
|
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := new(communicator.MockCommunicator)
|
||||||
|
c.Commands = tc.Commands
|
||||||
|
if tc.CommandFunc != nil {
|
||||||
|
c.CommandFunc = tc.CommandFunc
|
||||||
|
}
|
||||||
|
p.comm = c
|
||||||
|
p.output = new(terraform.MockUIOutput)
|
||||||
|
|
||||||
|
_, err = p.linuxDefaultCertname()
|
||||||
|
if tc.ExpectedError {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error, but no error returned")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test %q failed: %v", k, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceProvisioner_linuxInstallPuppetAgent(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
Config map[string]interface{}
|
||||||
|
Commands map[string]bool
|
||||||
|
CommandFunc func(*remote.Cmd) error
|
||||||
|
ExpectedError bool
|
||||||
|
}{
|
||||||
|
"Everything runs succcessfully": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
Commands: map[string]bool{
|
||||||
|
"curl -kO https://puppet.test.com:8140/packages/current/install.bash": true,
|
||||||
|
"bash -- ./install.bash --puppet-service-ensure stopped": true,
|
||||||
|
"rm -f install.bash": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Respects the use_sudo config flag": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": true,
|
||||||
|
},
|
||||||
|
Commands: map[string]bool{
|
||||||
|
"sudo curl -kO https://puppet.test.com:8140/packages/current/install.bash": true,
|
||||||
|
"sudo bash -- ./install.bash --puppet-service-ensure stopped": true,
|
||||||
|
"sudo rm -f install.bash": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"When the curl command fails": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
Commands: map[string]bool{
|
||||||
|
"curl -kO https://puppet.test.com:8140/packages/current/install.bash": true,
|
||||||
|
"bash -- ./install.bash --puppet-service-ensure stopped": false,
|
||||||
|
"rm -f install.bash": false,
|
||||||
|
},
|
||||||
|
CommandFunc: func(r *remote.Cmd) error {
|
||||||
|
if r.Command == "curl -kO https://puppet.test.com:8140/packages/current/install.bash" {
|
||||||
|
r.SetExitStatus(1, &remote.ExitError{
|
||||||
|
Command: "curl -kO https://puppet.test.com:8140/packages/current/install.bash",
|
||||||
|
ExitStatus: 1,
|
||||||
|
Err: nil,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
r.SetExitStatus(0, nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
"When the install script fails": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
Commands: map[string]bool{
|
||||||
|
"curl -kO https://puppet.test.com:8140/packages/current/install.bash": true,
|
||||||
|
"bash -- ./install.bash --puppet-service-ensure stopped": true,
|
||||||
|
"rm -f install.bash": false,
|
||||||
|
},
|
||||||
|
CommandFunc: func(r *remote.Cmd) error {
|
||||||
|
if r.Command == "bash -- ./install.bash --puppet-service-ensure stopped" {
|
||||||
|
r.SetExitStatus(1, &remote.ExitError{
|
||||||
|
Command: "bash -- ./install.bash --puppet-service-ensure stopped",
|
||||||
|
ExitStatus: 1,
|
||||||
|
Err: nil,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
r.SetExitStatus(0, nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
"When the cleanup rm fails": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
Commands: map[string]bool{
|
||||||
|
"curl -kO https://puppet.test.com:8140/packages/current/install.bash": true,
|
||||||
|
"bash -- ./install.bash --puppet-service-ensure stopped": true,
|
||||||
|
"rm -f install.bash": true,
|
||||||
|
},
|
||||||
|
CommandFunc: func(r *remote.Cmd) error {
|
||||||
|
if r.Command == "rm -f install.bash" {
|
||||||
|
r.SetExitStatus(1, &remote.ExitError{
|
||||||
|
Command: "rm -f install.bash",
|
||||||
|
ExitStatus: 1,
|
||||||
|
Err: nil,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
r.SetExitStatus(0, nil)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, tc := range cases {
|
||||||
|
p, err := decodeConfig(
|
||||||
|
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := new(communicator.MockCommunicator)
|
||||||
|
c.Commands = tc.Commands
|
||||||
|
if tc.CommandFunc != nil {
|
||||||
|
c.CommandFunc = tc.CommandFunc
|
||||||
|
}
|
||||||
|
p.comm = c
|
||||||
|
p.output = new(terraform.MockUIOutput)
|
||||||
|
|
||||||
|
err = p.linuxInstallPuppetAgent()
|
||||||
|
if tc.ExpectedError {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error, but no error returned")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test %q failed: %v", k, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceProvisioner_linuxRunPuppetAgent(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
Config map[string]interface{}
|
||||||
|
Commands map[string]bool
|
||||||
|
CommandFunc func(*remote.Cmd) error
|
||||||
|
ExpectedError bool
|
||||||
|
}{
|
||||||
|
"When puppet returns 0": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
Commands: map[string]bool{
|
||||||
|
"/opt/puppetlabs/puppet/bin/puppet agent --test --server puppet.test.com --environment production": true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"When puppet returns 2 (changes applied without error)": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
CommandFunc: func(r *remote.Cmd) error {
|
||||||
|
r.SetExitStatus(2, &remote.ExitError{
|
||||||
|
Command: "/opt/puppetlabs/puppet/bin/puppet agent --test --server puppet.test.com",
|
||||||
|
ExitStatus: 2,
|
||||||
|
Err: nil,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExpectedError: false,
|
||||||
|
},
|
||||||
|
"When puppet returns something not 0 or 2": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
CommandFunc: func(r *remote.Cmd) error {
|
||||||
|
r.SetExitStatus(1, &remote.ExitError{
|
||||||
|
Command: "/opt/puppetlabs/puppet/bin/puppet agent --test --server puppet.test.com",
|
||||||
|
ExitStatus: 1,
|
||||||
|
Err: nil,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, tc := range cases {
|
||||||
|
p, err := decodeConfig(
|
||||||
|
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := new(communicator.MockCommunicator)
|
||||||
|
c.Commands = tc.Commands
|
||||||
|
if tc.CommandFunc != nil {
|
||||||
|
c.CommandFunc = tc.CommandFunc
|
||||||
|
}
|
||||||
|
p.comm = c
|
||||||
|
p.output = new(terraform.MockUIOutput)
|
||||||
|
|
||||||
|
err = p.linuxRunPuppetAgent()
|
||||||
|
if tc.ExpectedError {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error, but no error returned")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test %q failed: %v", k, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,332 @@
|
||||||
|
package puppet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"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
|
||||||
|
}
|
||||||
|
|
||||||
|
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", "":
|
||||||
|
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 {
|
||||||
|
result, err := bolt.Task(
|
||||||
|
p.instanceState.Ephemeral.ConnInfo,
|
||||||
|
p.BoltTimeout,
|
||||||
|
p.UseSudo,
|
||||||
|
"puppet_agent::install",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil || result.Items[0].Status != "success" {
|
||||||
|
return fmt.Errorf("puppet_agent::install failed: %s\n%+v", 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)
|
||||||
|
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()
|
||||||
|
stdout = strings.TrimSpace(stdoutBuffer.String())
|
||||||
|
|
||||||
|
return stdout, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provisioner) copyToOutput(reader io.Reader) {
|
||||||
|
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
|
||||||
|
}
|
|
@ -0,0 +1,129 @@
|
||||||
|
package puppet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/config"
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResourceProvisioner_impl(t *testing.T) {
|
||||||
|
var _ terraform.ResourceProvisioner = Provisioner()
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisioner(t *testing.T) {
|
||||||
|
if err := Provisioner().(*schema.Provisioner).InternalValidate(); err != nil {
|
||||||
|
t.Fatalf("err: %s", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisioner_Validate_good_server(t *testing.T) {
|
||||||
|
c := testConfig(t, map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
})
|
||||||
|
|
||||||
|
warn, errs := Provisioner().Validate(c)
|
||||||
|
if len(warn) > 0 {
|
||||||
|
t.Fatalf("Warnings: %v", warn)
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("Errors: %v", errs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisioner_Validate_bad_no_server(t *testing.T) {
|
||||||
|
c := testConfig(t, map[string]interface{}{})
|
||||||
|
|
||||||
|
warn, errs := Provisioner().Validate(c)
|
||||||
|
if len(warn) > 0 {
|
||||||
|
t.Fatalf("Warnings: %v", warn)
|
||||||
|
}
|
||||||
|
if len(errs) == 0 {
|
||||||
|
t.Fatalf("Should have errors")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisioner_Validate_bad_os_type(t *testing.T) {
|
||||||
|
c := testConfig(t, map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"os_type": "OS/2",
|
||||||
|
})
|
||||||
|
|
||||||
|
warn, errs := Provisioner().Validate(c)
|
||||||
|
if len(warn) > 0 {
|
||||||
|
t.Fatalf("Warnings: %v", warn)
|
||||||
|
}
|
||||||
|
if len(errs) == 0 {
|
||||||
|
t.Fatalf("Should have errors")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisioner_Validate_good_os_type_linux(t *testing.T) {
|
||||||
|
c := testConfig(t, map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"os_type": "linux",
|
||||||
|
})
|
||||||
|
|
||||||
|
warn, errs := Provisioner().Validate(c)
|
||||||
|
if len(warn) > 0 {
|
||||||
|
t.Fatalf("Warnings: %v", warn)
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("Errors: %v", errs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisioner_Validate_good_os_type_windows(t *testing.T) {
|
||||||
|
c := testConfig(t, map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"os_type": "windows",
|
||||||
|
})
|
||||||
|
|
||||||
|
warn, errs := Provisioner().Validate(c)
|
||||||
|
if len(warn) > 0 {
|
||||||
|
t.Fatalf("Warnings: %v", warn)
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("Errors: %v", errs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisioner_Validate_bad_bolt_timeout(t *testing.T) {
|
||||||
|
c := testConfig(t, map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"bolt_timeout": "123oeau",
|
||||||
|
})
|
||||||
|
|
||||||
|
warn, errs := Provisioner().Validate(c)
|
||||||
|
if len(warn) > 0 {
|
||||||
|
t.Fatalf("Warnings: %v", warn)
|
||||||
|
}
|
||||||
|
if len(errs) == 0 {
|
||||||
|
t.Fatalf("Should have errors")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProvisioner_Validate_good_bolt_timeout(t *testing.T) {
|
||||||
|
c := testConfig(t, map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"bolt_timeout": "123m",
|
||||||
|
})
|
||||||
|
|
||||||
|
warn, errs := Provisioner().Validate(c)
|
||||||
|
if len(warn) > 0 {
|
||||||
|
t.Fatalf("Warnings: %v", warn)
|
||||||
|
}
|
||||||
|
if len(errs) > 0 {
|
||||||
|
t.Fatalf("Errors: %v", warn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig {
|
||||||
|
r, err := config.NewRawConfig(c)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bad: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return terraform.NewResourceConfig(r)
|
||||||
|
}
|
|
@ -0,0 +1,71 @@
|
||||||
|
package puppet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/communicator/remote"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
getHostByName = "([System.Net.Dns]::GetHostByName(($env:computerName))).Hostname"
|
||||||
|
domainQuery = "(Get-WmiObject -Query 'select DNSDomain from Win32_NetworkAdapterConfiguration where IPEnabled = True').DNSDomain"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *provisioner) windowsUploadFile(f io.Reader, dir string, filename string) error {
|
||||||
|
_, err := p.runCommand("powershell.exe new-item -itemtype directory -force -path " + dir)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to make directory %s: %s", dir, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return p.comm.Upload(dir+"\\"+filename, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provisioner) windowsDefaultCertname() (string, error) {
|
||||||
|
certname, err := p.runCommand(fmt.Sprintf(`powershell -Command "& {%s}"`, getHostByName))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sometimes System.Net.Dns::GetHostByName does not return a full FQDN, so
|
||||||
|
// we have to look up the domain separately.
|
||||||
|
if strings.Contains(certname, ".") {
|
||||||
|
return certname, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
domain, err := p.runCommand(fmt.Sprintf(`powershell -Command "& {%s}"`, domainQuery))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.ToLower(certname + "." + domain), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provisioner) windowsInstallPuppetAgent() error {
|
||||||
|
_, err := p.runCommand(fmt.Sprintf(
|
||||||
|
`powershell -Command "& {[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}; `+
|
||||||
|
`(New-Object System.Net.WebClient).DownloadFile('https://%s:8140/packages/current/install.ps1', `+
|
||||||
|
`'install.ps1')}"`,
|
||||||
|
p.Server,
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = p.runCommand(`powershell -Command "& .\install.ps1 -PuppetServiceEnsure stopped"`)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *provisioner) windowsRunPuppetAgent() error {
|
||||||
|
_, err := p.runCommand(fmt.Sprintf("puppet agent --test --server %s --environment %s", p.Server, p.Environment))
|
||||||
|
if err != nil {
|
||||||
|
errStruct, _ := err.(*remote.ExitError)
|
||||||
|
if errStruct.ExitStatus == 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
|
@ -0,0 +1,393 @@
|
||||||
|
package puppet
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/terraform/communicator"
|
||||||
|
"github.com/hashicorp/terraform/communicator/remote"
|
||||||
|
"github.com/hashicorp/terraform/helper/schema"
|
||||||
|
"github.com/hashicorp/terraform/terraform"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
getHostByNameCmd = `powershell -Command "& {([System.Net.Dns]::GetHostByName(($env:computerName))).Hostname}"`
|
||||||
|
domainQueryCmd = `powershell -Command "& {(Get-WmiObject -Query 'select DNSDomain from Win32_NetworkAdapterConfiguration where IPEnabled = True').DNSDomain}"`
|
||||||
|
downloadInstallerCmd = `powershell -Command "& {[Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}; (New-Object System.Net.WebClient).DownloadFile('https://puppet.test.com:8140/packages/current/install.ps1', 'install.ps1')}"`
|
||||||
|
runInstallerCmd = `powershell -Command "& .\install.ps1 -PuppetServiceEnsure stopped"`
|
||||||
|
runPuppetCmd = "puppet agent --test --server puppet.test.com --environment production"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestResourceProvisioner_windowsUploadFile(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
Config map[string]interface{}
|
||||||
|
Commands map[string]bool
|
||||||
|
CommandFunc func(*remote.Cmd) error
|
||||||
|
ExpectedError bool
|
||||||
|
Uploads map[string]string
|
||||||
|
File io.Reader
|
||||||
|
Dir string
|
||||||
|
Filename string
|
||||||
|
}{
|
||||||
|
"Successful upload": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
Commands: map[string]bool{
|
||||||
|
`powershell.exe new-item -itemtype directory -force -path C:\ProgramData\PuppetLabs\puppet\etc`: true,
|
||||||
|
},
|
||||||
|
Uploads: map[string]string{
|
||||||
|
`C:\ProgramData\PuppetLabs\puppet\etc\csr_attributes.yaml`: "",
|
||||||
|
},
|
||||||
|
Dir: `C:\ProgramData\PuppetLabs\puppet\etc`,
|
||||||
|
Filename: "csr_attributes.yaml",
|
||||||
|
File: strings.NewReader(""),
|
||||||
|
},
|
||||||
|
"Failure when creating the directory": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
Commands: map[string]bool{
|
||||||
|
`powershell.exe new-item -itemtype directory -force -path C:\ProgramData\PuppetLabs\puppet\etc`: true,
|
||||||
|
},
|
||||||
|
Dir: `C:\ProgramData\PuppetLabs\puppet\etc`,
|
||||||
|
Filename: "csr_attributes.yaml",
|
||||||
|
File: strings.NewReader(""),
|
||||||
|
CommandFunc: func(r *remote.Cmd) error {
|
||||||
|
r.SetExitStatus(1, &remote.ExitError{
|
||||||
|
Command: `powershell.exe new-item -itemtype directory -force -path C:\ProgramData\PuppetLabs\puppet\etc`,
|
||||||
|
ExitStatus: 1,
|
||||||
|
Err: nil,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, tc := range cases {
|
||||||
|
p, err := decodeConfig(
|
||||||
|
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := new(communicator.MockCommunicator)
|
||||||
|
c.Commands = tc.Commands
|
||||||
|
c.Uploads = tc.Uploads
|
||||||
|
if tc.CommandFunc != nil {
|
||||||
|
c.CommandFunc = tc.CommandFunc
|
||||||
|
}
|
||||||
|
p.comm = c
|
||||||
|
p.output = new(terraform.MockUIOutput)
|
||||||
|
|
||||||
|
err = p.windowsUploadFile(tc.File, tc.Dir, tc.Filename)
|
||||||
|
if tc.ExpectedError {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error, but no error returned")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test %q failed: %v", k, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceProvisioner_windowsDefaultCertname(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
Config map[string]interface{}
|
||||||
|
Commands map[string]bool
|
||||||
|
CommandFunc func(*remote.Cmd) error
|
||||||
|
ExpectedError bool
|
||||||
|
}{
|
||||||
|
"GetHostByName failure": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
CommandFunc: func(r *remote.Cmd) error {
|
||||||
|
switch r.Command {
|
||||||
|
case getHostByNameCmd:
|
||||||
|
r.SetExitStatus(1, &remote.ExitError{
|
||||||
|
Command: getHostByNameCmd,
|
||||||
|
ExitStatus: 1,
|
||||||
|
Err: nil,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Command not found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
"GetHostByName returns FQDN": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
CommandFunc: func(r *remote.Cmd) error {
|
||||||
|
switch r.Command {
|
||||||
|
case getHostByNameCmd:
|
||||||
|
r.Stdout.Write([]byte("example.test.com\n"))
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
r.SetExitStatus(0, nil)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Command not found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"GetHostByName returns hostname, DNSDomain query succeeds": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
CommandFunc: func(r *remote.Cmd) error {
|
||||||
|
switch r.Command {
|
||||||
|
case getHostByNameCmd:
|
||||||
|
r.Stdout.Write([]byte("example\n"))
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
r.SetExitStatus(0, nil)
|
||||||
|
case domainQueryCmd:
|
||||||
|
r.Stdout.Write([]byte("test.com\n"))
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
r.SetExitStatus(0, nil)
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Command not found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"GetHostByName returns hostname, DNSDomain query fails": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
CommandFunc: func(r *remote.Cmd) error {
|
||||||
|
switch r.Command {
|
||||||
|
case getHostByNameCmd:
|
||||||
|
r.Stdout.Write([]byte("example\n"))
|
||||||
|
time.Sleep(200 * time.Millisecond)
|
||||||
|
r.SetExitStatus(0, nil)
|
||||||
|
case domainQueryCmd:
|
||||||
|
r.SetExitStatus(1, &remote.ExitError{
|
||||||
|
Command: domainQueryCmd,
|
||||||
|
ExitStatus: 1,
|
||||||
|
Err: nil,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Command not found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, tc := range cases {
|
||||||
|
p, err := decodeConfig(
|
||||||
|
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := new(communicator.MockCommunicator)
|
||||||
|
c.Commands = tc.Commands
|
||||||
|
if tc.CommandFunc != nil {
|
||||||
|
c.CommandFunc = tc.CommandFunc
|
||||||
|
}
|
||||||
|
p.comm = c
|
||||||
|
p.output = new(terraform.MockUIOutput)
|
||||||
|
|
||||||
|
_, err = p.windowsDefaultCertname()
|
||||||
|
if tc.ExpectedError {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error, but no error returned")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test %q failed: %v", k, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceProvisioner_windowsInstallPuppetAgent(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
Config map[string]interface{}
|
||||||
|
Commands map[string]bool
|
||||||
|
CommandFunc func(*remote.Cmd) error
|
||||||
|
ExpectedError bool
|
||||||
|
}{
|
||||||
|
"Everything runs succcessfully": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
Commands: map[string]bool{
|
||||||
|
downloadInstallerCmd: true,
|
||||||
|
runInstallerCmd: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"Installer download fails": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": true,
|
||||||
|
},
|
||||||
|
CommandFunc: func(r *remote.Cmd) error {
|
||||||
|
switch r.Command {
|
||||||
|
case downloadInstallerCmd:
|
||||||
|
r.SetExitStatus(1, &remote.ExitError{
|
||||||
|
Command: downloadInstallerCmd,
|
||||||
|
ExitStatus: 1,
|
||||||
|
Err: nil,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Command not found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
"Install script fails": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
CommandFunc: func(r *remote.Cmd) error {
|
||||||
|
switch r.Command {
|
||||||
|
case downloadInstallerCmd:
|
||||||
|
r.SetExitStatus(0, nil)
|
||||||
|
case runInstallerCmd:
|
||||||
|
r.SetExitStatus(1, &remote.ExitError{
|
||||||
|
Command: runInstallerCmd,
|
||||||
|
ExitStatus: 1,
|
||||||
|
Err: nil,
|
||||||
|
})
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Command not found!")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, tc := range cases {
|
||||||
|
p, err := decodeConfig(
|
||||||
|
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := new(communicator.MockCommunicator)
|
||||||
|
c.Commands = tc.Commands
|
||||||
|
if tc.CommandFunc != nil {
|
||||||
|
c.CommandFunc = tc.CommandFunc
|
||||||
|
}
|
||||||
|
p.comm = c
|
||||||
|
p.output = new(terraform.MockUIOutput)
|
||||||
|
|
||||||
|
err = p.windowsInstallPuppetAgent()
|
||||||
|
if tc.ExpectedError {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error, but no error returned")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test %q failed: %v", k, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestResourceProvisioner_windowsRunPuppetAgent(t *testing.T) {
|
||||||
|
cases := map[string]struct {
|
||||||
|
Config map[string]interface{}
|
||||||
|
Commands map[string]bool
|
||||||
|
CommandFunc func(*remote.Cmd) error
|
||||||
|
ExpectedError bool
|
||||||
|
}{
|
||||||
|
"When puppet returns 0": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
Commands: map[string]bool{
|
||||||
|
runPuppetCmd: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"When puppet returns 2 (changes applied without error)": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
CommandFunc: func(r *remote.Cmd) error {
|
||||||
|
r.SetExitStatus(2, &remote.ExitError{
|
||||||
|
Command: runPuppetCmd,
|
||||||
|
ExitStatus: 2,
|
||||||
|
Err: nil,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"When puppet returns something not 0 or 2": {
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"server": "puppet.test.com",
|
||||||
|
"use_sudo": false,
|
||||||
|
},
|
||||||
|
CommandFunc: func(r *remote.Cmd) error {
|
||||||
|
r.SetExitStatus(1, &remote.ExitError{
|
||||||
|
Command: runPuppetCmd,
|
||||||
|
ExitStatus: 1,
|
||||||
|
Err: nil,
|
||||||
|
})
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
ExpectedError: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, tc := range cases {
|
||||||
|
p, err := decodeConfig(
|
||||||
|
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c := new(communicator.MockCommunicator)
|
||||||
|
c.Commands = tc.Commands
|
||||||
|
if tc.CommandFunc != nil {
|
||||||
|
c.CommandFunc = tc.CommandFunc
|
||||||
|
}
|
||||||
|
p.comm = c
|
||||||
|
p.output = new(terraform.MockUIOutput)
|
||||||
|
|
||||||
|
err = p.windowsRunPuppetAgent()
|
||||||
|
if tc.ExpectedError {
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Expected error, but no error returned")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Test %q failed: %v", k, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,6 +8,7 @@ import (
|
||||||
fileprovisioner "github.com/hashicorp/terraform/builtin/provisioners/file"
|
fileprovisioner "github.com/hashicorp/terraform/builtin/provisioners/file"
|
||||||
habitatprovisioner "github.com/hashicorp/terraform/builtin/provisioners/habitat"
|
habitatprovisioner "github.com/hashicorp/terraform/builtin/provisioners/habitat"
|
||||||
localexecprovisioner "github.com/hashicorp/terraform/builtin/provisioners/local-exec"
|
localexecprovisioner "github.com/hashicorp/terraform/builtin/provisioners/local-exec"
|
||||||
|
puppetprovisioner "github.com/hashicorp/terraform/builtin/provisioners/puppet"
|
||||||
remoteexecprovisioner "github.com/hashicorp/terraform/builtin/provisioners/remote-exec"
|
remoteexecprovisioner "github.com/hashicorp/terraform/builtin/provisioners/remote-exec"
|
||||||
saltmasterlessprovisioner "github.com/hashicorp/terraform/builtin/provisioners/salt-masterless"
|
saltmasterlessprovisioner "github.com/hashicorp/terraform/builtin/provisioners/salt-masterless"
|
||||||
|
|
||||||
|
@ -21,6 +22,7 @@ var InternalProvisioners = map[string]plugin.ProvisionerFunc{
|
||||||
"file": fileprovisioner.Provisioner,
|
"file": fileprovisioner.Provisioner,
|
||||||
"habitat": habitatprovisioner.Provisioner,
|
"habitat": habitatprovisioner.Provisioner,
|
||||||
"local-exec": localexecprovisioner.Provisioner,
|
"local-exec": localexecprovisioner.Provisioner,
|
||||||
|
"puppet": puppetprovisioner.Provisioner,
|
||||||
"remote-exec": remoteexecprovisioner.Provisioner,
|
"remote-exec": remoteexecprovisioner.Provisioner,
|
||||||
"salt-masterless": saltmasterlessprovisioner.Provisioner,
|
"salt-masterless": saltmasterlessprovisioner.Provisioner,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue