Remove vendor provisioners and add fmt Make target

Remove chef, habitat, puppet, and salt-masterless provsioners,
which follows their deprecation. Update the documentatin for these
provisioners to clarify that they have been removed from later versions
of Terraform. Adds the fmt Make target back and updates fmtcheck script
for correctness.
This commit is contained in:
Pam Selle 2020-11-16 14:15:03 -05:00
parent c1d30401c5
commit e39e0e3d04
34 changed files with 16 additions and 6175 deletions

View File

@ -1,12 +0,0 @@
package main
import (
"github.com/hashicorp/terraform/builtin/provisioners/chef"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProvisionerFunc: chef.Provisioner,
})
}

View File

@ -1,12 +0,0 @@
package main
import (
"github.com/hashicorp/terraform/builtin/provisioners/habitat"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProvisionerFunc: habitat.Provisioner,
})
}

View File

@ -1,12 +0,0 @@
package main
import (
"github.com/hashicorp/terraform/builtin/provisioners/puppet"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProvisionerFunc: puppet.Provisioner,
})
}

View File

@ -1,12 +0,0 @@
package main
import (
"github.com/hashicorp/terraform/builtin/provisioners/salt-masterless"
"github.com/hashicorp/terraform/plugin"
)
func main() {
plugin.Serve(&plugin.ServeOpts{
ProvisionerFunc: saltmasterless.Provisioner,
})
}

View File

@ -1,115 +0,0 @@
package chef
import (
"fmt"
"path"
"strings"
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/terraform"
)
const (
chmod = "find %s -maxdepth 1 -type f -exec /bin/chmod %d {} +"
installURL = "https://omnitruck.chef.io/install.sh"
)
func (p *provisioner) linuxInstallChefClient(o terraform.UIOutput, comm communicator.Communicator) error {
// Build up the command prefix
prefix := ""
if p.HTTPProxy != "" {
prefix += fmt.Sprintf("http_proxy='%s' ", p.HTTPProxy)
}
if p.HTTPSProxy != "" {
prefix += fmt.Sprintf("https_proxy='%s' ", p.HTTPSProxy)
}
if len(p.NOProxy) > 0 {
prefix += fmt.Sprintf("no_proxy='%s' ", strings.Join(p.NOProxy, ","))
}
// First download the install.sh script from Chef
err := p.runCommand(o, comm, fmt.Sprintf("%scurl -LO %s", prefix, installURL))
if err != nil {
return err
}
// Then execute the install.sh scrip to download and install Chef Client
err = p.runCommand(o, comm, fmt.Sprintf("%sbash ./install.sh -v %q -c %s", prefix, p.Version, p.Channel))
if err != nil {
return err
}
// And finally cleanup the install.sh script again
return p.runCommand(o, comm, fmt.Sprintf("%srm -f install.sh", prefix))
}
func (p *provisioner) linuxCreateConfigFiles(o terraform.UIOutput, comm communicator.Communicator) error {
// Make sure the config directory exists
if err := p.runCommand(o, comm, "mkdir -p "+linuxConfDir); err != nil {
return err
}
// Make sure we have enough rights to upload the files if using sudo
if p.useSudo {
if err := p.runCommand(o, comm, "chmod 777 "+linuxConfDir); err != nil {
return err
}
if err := p.runCommand(o, comm, fmt.Sprintf(chmod, linuxConfDir, 666)); err != nil {
return err
}
}
if err := p.deployConfigFiles(o, comm, linuxConfDir); err != nil {
return err
}
if len(p.OhaiHints) > 0 {
// Make sure the hits directory exists
hintsDir := path.Join(linuxConfDir, "ohai/hints")
if err := p.runCommand(o, comm, "mkdir -p "+hintsDir); err != nil {
return err
}
// Make sure we have enough rights to upload the hints if using sudo
if p.useSudo {
if err := p.runCommand(o, comm, "chmod 777 "+hintsDir); err != nil {
return err
}
if err := p.runCommand(o, comm, fmt.Sprintf(chmod, hintsDir, 666)); err != nil {
return err
}
}
if err := p.deployOhaiHints(o, comm, hintsDir); err != nil {
return err
}
// When done copying the hints restore the rights and make sure root is owner
if p.useSudo {
if err := p.runCommand(o, comm, "chmod 755 "+hintsDir); err != nil {
return err
}
if err := p.runCommand(o, comm, fmt.Sprintf(chmod, hintsDir, 600)); err != nil {
return err
}
if err := p.runCommand(o, comm, "chown -R root:root "+hintsDir); err != nil {
return err
}
}
}
// When done copying all files restore the rights and make sure root is owner
if p.useSudo {
if err := p.runCommand(o, comm, "chmod 755 "+linuxConfDir); err != nil {
return err
}
if err := p.runCommand(o, comm, fmt.Sprintf(chmod, linuxConfDir, 600)); err != nil {
return err
}
if err := p.runCommand(o, comm, "chown -R root:root "+linuxConfDir); err != nil {
return err
}
}
return nil
}

View File

@ -1,330 +0,0 @@
package chef
import (
"fmt"
"path"
"testing"
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
func TestResourceProvider_linuxInstallChefClient(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
Commands map[string]bool
}{
"Sudo": {
Config: map[string]interface{}{
"node_name": "nodename1",
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
},
Commands: map[string]bool{
"sudo curl -LO https://omnitruck.chef.io/install.sh": true,
"sudo bash ./install.sh -v \"\" -c stable": true,
"sudo rm -f install.sh": true,
},
},
"NoSudo": {
Config: map[string]interface{}{
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"secret_key": "SECRET-KEY",
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
},
Commands: map[string]bool{
"curl -LO https://omnitruck.chef.io/install.sh": true,
"bash ./install.sh -v \"\" -c stable": true,
"rm -f install.sh": true,
},
},
"HTTPProxy": {
Config: map[string]interface{}{
"http_proxy": "http://proxy.local",
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
},
Commands: map[string]bool{
"http_proxy='http://proxy.local' curl -LO https://omnitruck.chef.io/install.sh": true,
"http_proxy='http://proxy.local' bash ./install.sh -v \"\" -c stable": true,
"http_proxy='http://proxy.local' rm -f install.sh": true,
},
},
"HTTPSProxy": {
Config: map[string]interface{}{
"https_proxy": "https://proxy.local",
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
},
Commands: map[string]bool{
"https_proxy='https://proxy.local' curl -LO https://omnitruck.chef.io/install.sh": true,
"https_proxy='https://proxy.local' bash ./install.sh -v \"\" -c stable": true,
"https_proxy='https://proxy.local' rm -f install.sh": true,
},
},
"NoProxy": {
Config: map[string]interface{}{
"http_proxy": "http://proxy.local",
"no_proxy": []interface{}{"http://local.local", "http://local.org"},
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
},
Commands: map[string]bool{
"http_proxy='http://proxy.local' no_proxy='http://local.local,http://local.org' " +
"curl -LO https://omnitruck.chef.io/install.sh": true,
"http_proxy='http://proxy.local' no_proxy='http://local.local,http://local.org' " +
"bash ./install.sh -v \"\" -c stable": true,
"http_proxy='http://proxy.local' no_proxy='http://local.local,http://local.org' " +
"rm -f install.sh": true,
},
},
"Version": {
Config: map[string]interface{}{
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
"version": "11.18.6",
},
Commands: map[string]bool{
"curl -LO https://omnitruck.chef.io/install.sh": true,
"bash ./install.sh -v \"11.18.6\" -c stable": true,
"rm -f install.sh": true,
},
},
"Channel": {
Config: map[string]interface{}{
"channel": "current",
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
"version": "11.18.6",
},
Commands: map[string]bool{
"curl -LO https://omnitruck.chef.io/install.sh": true,
"bash ./install.sh -v \"11.18.6\" -c current": true,
"rm -f install.sh": true,
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
p.useSudo = !p.PreventSudo
err = p.linuxInstallChefClient(o, c)
if err != nil {
t.Fatalf("Test %q failed: %v", k, err)
}
}
}
func TestResourceProvider_linuxCreateConfigFiles(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
Commands map[string]bool
Uploads map[string]string
}{
"Sudo": {
Config: map[string]interface{}{
"ohai_hints": []interface{}{"testdata/ohaihint.json"},
"node_name": "nodename1",
"run_list": []interface{}{"cookbook::recipe"},
"secret_key": "SECRET-KEY",
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
},
Commands: map[string]bool{
"sudo mkdir -p " + linuxConfDir: true,
"sudo chmod 777 " + linuxConfDir: true,
"sudo " + fmt.Sprintf(chmod, linuxConfDir, 666): true,
"sudo mkdir -p " + path.Join(linuxConfDir, "ohai/hints"): true,
"sudo chmod 777 " + path.Join(linuxConfDir, "ohai/hints"): true,
"sudo " + fmt.Sprintf(chmod, path.Join(linuxConfDir, "ohai/hints"), 666): true,
"sudo chmod 755 " + path.Join(linuxConfDir, "ohai/hints"): true,
"sudo " + fmt.Sprintf(chmod, path.Join(linuxConfDir, "ohai/hints"), 600): true,
"sudo chown -R root:root " + path.Join(linuxConfDir, "ohai/hints"): true,
"sudo chmod 755 " + linuxConfDir: true,
"sudo " + fmt.Sprintf(chmod, linuxConfDir, 600): true,
"sudo chown -R root:root " + linuxConfDir: true,
},
Uploads: map[string]string{
linuxConfDir + "/client.rb": defaultLinuxClientConf,
linuxConfDir + "/encrypted_data_bag_secret": "SECRET-KEY",
linuxConfDir + "/first-boot.json": `{"run_list":["cookbook::recipe"]}`,
linuxConfDir + "/ohai/hints/ohaihint.json": "OHAI-HINT-FILE",
linuxConfDir + "/bob.pem": "USER-KEY",
},
},
"NoSudo": {
Config: map[string]interface{}{
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"secret_key": "SECRET-KEY",
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
},
Commands: map[string]bool{
"mkdir -p " + linuxConfDir: true,
},
Uploads: map[string]string{
linuxConfDir + "/client.rb": defaultLinuxClientConf,
linuxConfDir + "/encrypted_data_bag_secret": "SECRET-KEY",
linuxConfDir + "/first-boot.json": `{"run_list":["cookbook::recipe"]}`,
linuxConfDir + "/bob.pem": "USER-KEY",
},
},
"Proxy": {
Config: map[string]interface{}{
"http_proxy": "http://proxy.local",
"https_proxy": "https://proxy.local",
"no_proxy": []interface{}{"http://local.local", "https://local.local"},
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"secret_key": "SECRET-KEY",
"server_url": "https://chef.local",
"ssl_verify_mode": "verify_none",
"user_name": "bob",
"user_key": "USER-KEY",
},
Commands: map[string]bool{
"mkdir -p " + linuxConfDir: true,
},
Uploads: map[string]string{
linuxConfDir + "/client.rb": proxyLinuxClientConf,
linuxConfDir + "/encrypted_data_bag_secret": "SECRET-KEY",
linuxConfDir + "/first-boot.json": `{"run_list":["cookbook::recipe"]}`,
linuxConfDir + "/bob.pem": "USER-KEY",
},
},
"Attributes JSON": {
Config: map[string]interface{}{
"attributes_json": `{"key1":{"subkey1":{"subkey2a":["val1","val2","val3"],` +
`"subkey2b":{"subkey3":"value3"}}},"key2":"value2"}`,
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"secret_key": "SECRET-KEY",
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
},
Commands: map[string]bool{
"mkdir -p " + linuxConfDir: true,
},
Uploads: map[string]string{
linuxConfDir + "/client.rb": defaultLinuxClientConf,
linuxConfDir + "/encrypted_data_bag_secret": "SECRET-KEY",
linuxConfDir + "/bob.pem": "USER-KEY",
linuxConfDir + "/first-boot.json": `{"key1":{"subkey1":{"subkey2a":["val1","val2","val3"],` +
`"subkey2b":{"subkey3":"value3"}}},"key2":"value2","run_list":["cookbook::recipe"]}`,
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
c.Uploads = tc.Uploads
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
p.useSudo = !p.PreventSudo
err = p.linuxCreateConfigFiles(o, c)
if err != nil {
t.Fatalf("Test %q failed: %v", k, err)
}
}
}
const defaultLinuxClientConf = `log_location STDOUT
chef_server_url "https://chef.local/"
node_name "nodename1"`
const proxyLinuxClientConf = `log_location STDOUT
chef_server_url "https://chef.local/"
node_name "nodename1"
http_proxy "http://proxy.local"
ENV['http_proxy'] = "http://proxy.local"
ENV['HTTP_PROXY'] = "http://proxy.local"
https_proxy "https://proxy.local"
ENV['https_proxy'] = "https://proxy.local"
ENV['HTTPS_PROXY'] = "https://proxy.local"
no_proxy "http://local.local,https://local.local"
ENV['no_proxy'] = "http://local.local,https://local.local"
ssl_verify_mode :verify_none`

View File

@ -1,904 +0,0 @@
package chef
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"log"
"os"
"path"
"regexp"
"strconv"
"strings"
"sync"
"text/template"
"time"
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/go-homedir"
"github.com/mitchellh/go-linereader"
)
const (
clienrb = "client.rb"
defaultEnv = "_default"
firstBoot = "first-boot.json"
logfileDir = "logfiles"
linuxChefCmd = "chef-client"
linuxConfDir = "/etc/chef"
linuxNoOutput = "> /dev/null 2>&1"
linuxGemCmd = "/opt/chef/embedded/bin/gem"
linuxKnifeCmd = "knife"
secretKey = "encrypted_data_bag_secret"
windowsChefCmd = "cmd /c chef-client"
windowsConfDir = "C:/chef"
windowsNoOutput = "> nul 2>&1"
windowsGemCmd = "C:/opscode/chef/embedded/bin/gem"
windowsKnifeCmd = "cmd /c knife"
)
const clientConf = `
log_location STDOUT
chef_server_url "{{ .ServerURL }}"
node_name "{{ .NodeName }}"
{{ if .UsePolicyfile }}
use_policyfile true
policy_group "{{ .PolicyGroup }}"
policy_name "{{ .PolicyName }}"
{{ end -}}
{{ if .HTTPProxy }}
http_proxy "{{ .HTTPProxy }}"
ENV['http_proxy'] = "{{ .HTTPProxy }}"
ENV['HTTP_PROXY'] = "{{ .HTTPProxy }}"
{{ end -}}
{{ if .HTTPSProxy }}
https_proxy "{{ .HTTPSProxy }}"
ENV['https_proxy'] = "{{ .HTTPSProxy }}"
ENV['HTTPS_PROXY'] = "{{ .HTTPSProxy }}"
{{ end -}}
{{ if .NOProxy }}
no_proxy "{{ join .NOProxy "," }}"
ENV['no_proxy'] = "{{ join .NOProxy "," }}"
{{ end -}}
{{ if .SSLVerifyMode }}
ssl_verify_mode {{ .SSLVerifyMode }}
{{- end -}}
{{ if .DisableReporting }}
enable_reporting false
{{ end -}}
{{ if .ClientOptions }}
{{ join .ClientOptions "\n" }}
{{ end }}
`
type provisionFn func(terraform.UIOutput, communicator.Communicator) error
type provisioner struct {
Attributes map[string]interface{}
Channel string
ClientOptions []string
DisableReporting bool
Environment string
FetchChefCertificates bool
LogToFile bool
UsePolicyfile bool
PolicyGroup string
PolicyName string
HTTPProxy string
HTTPSProxy string
MaxRetries int
NamedRunList string
NOProxy []string
NodeName string
OhaiHints []string
OSType string
RecreateClient bool
PreventSudo bool
RetryOnExitCode map[int]bool
RunList []string
SecretKey string
ServerURL string
SkipInstall bool
SkipRegister bool
SSLVerifyMode string
UserName string
UserKey string
Vaults map[string][]string
Version string
WaitForRetry time.Duration
cleanupUserKeyCmd string
createConfigFiles provisionFn
installChefClient provisionFn
fetchChefCertificates provisionFn
generateClientKey provisionFn
configureVaults provisionFn
runChefClient provisionFn
useSudo bool
}
// Provisioner returns a Chef provisioner
func Provisioner() terraform.ResourceProvisioner {
return &schema.Provisioner{
Schema: map[string]*schema.Schema{
"node_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"server_url": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"user_name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"user_key": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"attributes_json": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"channel": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "stable",
},
"client_options": &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
},
"disable_reporting": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"environment": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: defaultEnv,
},
"fetch_chef_certificates": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"log_to_file": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"use_policyfile": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"policy_group": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"policy_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"http_proxy": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"https_proxy": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"max_retries": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 0,
},
"no_proxy": &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
},
"named_run_list": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"ohai_hints": &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
},
"os_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"prevent_sudo": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"recreate_client": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"retry_on_exit_code": &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeInt},
Optional: true,
},
"run_list": &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
},
"secret_key": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"skip_install": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"skip_register": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"ssl_verify_mode": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"vault_json": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"version": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"wait_for_retry": &schema.Schema{
Type: schema.TypeInt,
Optional: true,
Default: 30,
},
},
ApplyFunc: applyFn,
ValidateFunc: validateFn,
}
}
// TODO: Support context cancelling (Provisioner Stop)
func applyFn(ctx context.Context) error {
o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
s := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
// Decode the provisioner config
p, err := decodeConfig(d)
if err != nil {
return err
}
if p.OSType == "" {
switch t := s.Ephemeral.ConnInfo["type"]; t {
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", t)
}
}
// Set some values based on the targeted OS
switch p.OSType {
case "linux":
p.cleanupUserKeyCmd = fmt.Sprintf("rm -f %s", path.Join(linuxConfDir, p.UserName+".pem"))
p.createConfigFiles = p.linuxCreateConfigFiles
p.installChefClient = p.linuxInstallChefClient
p.fetchChefCertificates = p.fetchChefCertificatesFunc(linuxKnifeCmd, linuxConfDir)
p.generateClientKey = p.generateClientKeyFunc(linuxKnifeCmd, linuxConfDir, linuxNoOutput)
p.configureVaults = p.configureVaultsFunc(linuxGemCmd, linuxKnifeCmd, linuxConfDir)
p.runChefClient = p.runChefClientFunc(linuxChefCmd, linuxConfDir)
p.useSudo = !p.PreventSudo && s.Ephemeral.ConnInfo["user"] != "root"
case "windows":
p.cleanupUserKeyCmd = fmt.Sprintf("cd %s && del /F /Q %s", windowsConfDir, p.UserName+".pem")
p.createConfigFiles = p.windowsCreateConfigFiles
p.installChefClient = p.windowsInstallChefClient
p.fetchChefCertificates = p.fetchChefCertificatesFunc(windowsKnifeCmd, windowsConfDir)
p.generateClientKey = p.generateClientKeyFunc(windowsKnifeCmd, windowsConfDir, windowsNoOutput)
p.configureVaults = p.configureVaultsFunc(windowsGemCmd, windowsKnifeCmd, windowsConfDir)
p.runChefClient = p.runChefClientFunc(windowsChefCmd, windowsConfDir)
p.useSudo = false
default:
return fmt.Errorf("Unsupported os type: %s", p.OSType)
}
// Get a new communicator
comm, err := communicator.New(s)
if err != nil {
return err
}
retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
defer cancel()
// Wait and retry until we establish the connection
err = communicator.Retry(retryCtx, func() error {
return comm.Connect(o)
})
if err != nil {
return err
}
defer comm.Disconnect()
// Make sure we always delete the user key from the new node!
var once sync.Once
cleanupUserKey := func() {
o.Output("Cleanup user key...")
if err := p.runCommand(o, comm, p.cleanupUserKeyCmd); err != nil {
o.Output("WARNING: Failed to cleanup user key on new node: " + err.Error())
}
}
defer once.Do(cleanupUserKey)
if !p.SkipInstall {
if err := p.installChefClient(o, comm); err != nil {
return err
}
}
o.Output("Creating configuration files...")
if err := p.createConfigFiles(o, comm); err != nil {
return err
}
if !p.SkipRegister {
if p.FetchChefCertificates {
o.Output("Fetch Chef certificates...")
if err := p.fetchChefCertificates(o, comm); err != nil {
return err
}
}
o.Output("Generate the private key...")
if err := p.generateClientKey(o, comm); err != nil {
return err
}
}
if p.Vaults != nil {
o.Output("Configure Chef vaults...")
if err := p.configureVaults(o, comm); err != nil {
return err
}
}
// Cleanup the user key before we run Chef-Client to prevent issues
// with rights caused by changing settings during the run.
once.Do(cleanupUserKey)
o.Output("Starting initial Chef-Client run...")
for attempt := 0; attempt <= p.MaxRetries; attempt++ {
// We need a new retry context for each attempt, to make sure
// they all get the correct timeout.
retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
defer cancel()
// Make sure to (re)connect before trying to run Chef-Client.
if err := communicator.Retry(retryCtx, func() error {
return comm.Connect(o)
}); err != nil {
return err
}
err = p.runChefClient(o, comm)
if err == nil {
return nil
}
// Allow RFC062 Exit Codes:
// https://github.com/chef/chef-rfc/blob/master/rfc062-exit-status.md
exitError, ok := err.(*remote.ExitError)
if !ok {
return err
}
switch exitError.ExitStatus {
case 35:
o.Output("Reboot has been scheduled in the run state")
err = nil
case 37:
o.Output("Reboot needs to be completed")
err = nil
case 213:
o.Output("Chef has exited during a client upgrade")
err = nil
}
if !p.RetryOnExitCode[exitError.ExitStatus] {
return err
}
if attempt < p.MaxRetries {
o.Output(fmt.Sprintf("Waiting %s before retrying Chef-Client run...", p.WaitForRetry))
time.Sleep(p.WaitForRetry)
}
}
return err
}
func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) {
usePolicyFile := false
if usePolicyFileRaw, ok := c.Get("use_policyfile"); ok {
switch usePolicyFileRaw := usePolicyFileRaw.(type) {
case bool:
usePolicyFile = usePolicyFileRaw
case string:
usePolicyFileBool, err := strconv.ParseBool(usePolicyFileRaw)
if err != nil {
return ws, append(es, errors.New("\"use_policyfile\" must be a boolean"))
}
usePolicyFile = usePolicyFileBool
default:
return ws, append(es, errors.New("\"use_policyfile\" must be a boolean"))
}
}
if !usePolicyFile && !c.IsSet("run_list") {
es = append(es, errors.New("\"run_list\": required field is not set"))
}
if usePolicyFile && !c.IsSet("policy_name") {
es = append(es, errors.New("using policyfile, but \"policy_name\" not set"))
}
if usePolicyFile && !c.IsSet("policy_group") {
es = append(es, errors.New("using policyfile, but \"policy_group\" not set"))
}
return ws, es
}
func (p *provisioner) deployConfigFiles(o terraform.UIOutput, comm communicator.Communicator, confDir string) error {
// Copy the user key to the new instance
pk := strings.NewReader(p.UserKey)
if err := comm.Upload(path.Join(confDir, p.UserName+".pem"), pk); err != nil {
return fmt.Errorf("Uploading user key failed: %v", err)
}
if p.SecretKey != "" {
// Copy the secret key to the new instance
s := strings.NewReader(p.SecretKey)
if err := comm.Upload(path.Join(confDir, secretKey), s); err != nil {
return fmt.Errorf("Uploading %s failed: %v", secretKey, err)
}
}
// Make sure the SSLVerifyMode value is written as a symbol
if p.SSLVerifyMode != "" && !strings.HasPrefix(p.SSLVerifyMode, ":") {
p.SSLVerifyMode = fmt.Sprintf(":%s", p.SSLVerifyMode)
}
// Make strings.Join available for use within the template
funcMap := template.FuncMap{
"join": strings.Join,
}
// Create a new template and parse the client config into it
t := template.Must(template.New(clienrb).Funcs(funcMap).Parse(clientConf))
var buf bytes.Buffer
err := t.Execute(&buf, p)
if err != nil {
return fmt.Errorf("Error executing %s template: %s", clienrb, err)
}
// Copy the client config to the new instance
if err = comm.Upload(path.Join(confDir, clienrb), &buf); err != nil {
return fmt.Errorf("Uploading %s failed: %v", clienrb, err)
}
// Create a map with first boot settings
fb := make(map[string]interface{})
if p.Attributes != nil {
fb = p.Attributes
}
// Check if the run_list was also in the attributes and if so log a warning
// that it will be overwritten with the value of the run_list argument.
if _, found := fb["run_list"]; found {
log.Printf("[WARN] Found a 'run_list' specified in the configured attributes! " +
"This value will be overwritten by the value of the `run_list` argument!")
}
// Add the initial runlist to the first boot settings
if !p.UsePolicyfile {
fb["run_list"] = p.RunList
}
// Marshal the first boot settings to JSON
d, err := json.Marshal(fb)
if err != nil {
return fmt.Errorf("Failed to create %s data: %s", firstBoot, err)
}
// Copy the first-boot.json to the new instance
if err := comm.Upload(path.Join(confDir, firstBoot), bytes.NewReader(d)); err != nil {
return fmt.Errorf("Uploading %s failed: %v", firstBoot, err)
}
return nil
}
func (p *provisioner) deployOhaiHints(o terraform.UIOutput, comm communicator.Communicator, hintDir string) error {
for _, hint := range p.OhaiHints {
// Open the hint file
f, err := os.Open(hint)
if err != nil {
return err
}
defer f.Close()
// Copy the hint to the new instance
if err := comm.Upload(path.Join(hintDir, path.Base(hint)), f); err != nil {
return fmt.Errorf("Uploading %s failed: %v", path.Base(hint), err)
}
}
return nil
}
func (p *provisioner) fetchChefCertificatesFunc(
knifeCmd string,
confDir string) func(terraform.UIOutput, communicator.Communicator) error {
return func(o terraform.UIOutput, comm communicator.Communicator) error {
clientrb := path.Join(confDir, clienrb)
cmd := fmt.Sprintf("%s ssl fetch -c %s", knifeCmd, clientrb)
return p.runCommand(o, comm, cmd)
}
}
func (p *provisioner) generateClientKeyFunc(knifeCmd string, confDir string, noOutput string) provisionFn {
return func(o terraform.UIOutput, comm communicator.Communicator) error {
options := fmt.Sprintf("-c %s -u %s --key %s",
path.Join(confDir, clienrb),
p.UserName,
path.Join(confDir, p.UserName+".pem"),
)
// See if we already have a node object
getNodeCmd := fmt.Sprintf("%s node show %s %s %s", knifeCmd, p.NodeName, options, noOutput)
node := p.runCommand(o, comm, getNodeCmd) == nil
// See if we already have a client object
getClientCmd := fmt.Sprintf("%s client show %s %s %s", knifeCmd, p.NodeName, options, noOutput)
client := p.runCommand(o, comm, getClientCmd) == nil
// If we have a client, we can only continue if we are to recreate the client
if client && !p.RecreateClient {
return fmt.Errorf(
"Chef client %q already exists, set recreate_client=true to automatically recreate the client", p.NodeName)
}
// If the node exists, try to delete it
if node {
deleteNodeCmd := fmt.Sprintf("%s node delete %s -y %s",
knifeCmd,
p.NodeName,
options,
)
if err := p.runCommand(o, comm, deleteNodeCmd); err != nil {
return err
}
}
// If the client exists, try to delete it
if client {
deleteClientCmd := fmt.Sprintf("%s client delete %s -y %s",
knifeCmd,
p.NodeName,
options,
)
if err := p.runCommand(o, comm, deleteClientCmd); err != nil {
return err
}
}
// Create the new client object
createClientCmd := fmt.Sprintf("%s client create %s -d -f %s %s",
knifeCmd,
p.NodeName,
path.Join(confDir, "client.pem"),
options,
)
return p.runCommand(o, comm, createClientCmd)
}
}
func (p *provisioner) configureVaultsFunc(gemCmd string, knifeCmd string, confDir string) provisionFn {
return func(o terraform.UIOutput, comm communicator.Communicator) error {
if err := p.runCommand(o, comm, fmt.Sprintf("%s install chef-vault", gemCmd)); err != nil {
return err
}
options := fmt.Sprintf("-c %s -u %s --key %s",
path.Join(confDir, clienrb),
p.UserName,
path.Join(confDir, p.UserName+".pem"),
)
// if client gets recreated, remove (old) client (with old keys) from vaults/items
// otherwise, the (new) client (with new keys) will not be able to decrypt the vault
if p.RecreateClient {
for vault, items := range p.Vaults {
for _, item := range items {
deleteCmd := fmt.Sprintf("%s vault remove %s %s -C \"%s\" -M client %s",
knifeCmd,
vault,
item,
p.NodeName,
options,
)
if err := p.runCommand(o, comm, deleteCmd); err != nil {
return err
}
}
}
}
for vault, items := range p.Vaults {
for _, item := range items {
updateCmd := fmt.Sprintf("%s vault update %s %s -C %s -M client %s",
knifeCmd,
vault,
item,
p.NodeName,
options,
)
if err := p.runCommand(o, comm, updateCmd); err != nil {
return err
}
}
}
return nil
}
}
func (p *provisioner) runChefClientFunc(chefCmd string, confDir string) provisionFn {
return func(o terraform.UIOutput, comm communicator.Communicator) error {
fb := path.Join(confDir, firstBoot)
var cmd string
// Policyfiles do not support chef environments, so don't pass the `-E` flag.
switch {
case p.UsePolicyfile && p.NamedRunList == "":
cmd = fmt.Sprintf("%s -j %q", chefCmd, fb)
case p.UsePolicyfile && p.NamedRunList != "":
cmd = fmt.Sprintf("%s -j %q -n %q", chefCmd, fb, p.NamedRunList)
default:
cmd = fmt.Sprintf("%s -j %q -E %q", chefCmd, fb, p.Environment)
}
if p.LogToFile {
if err := os.MkdirAll(logfileDir, 0755); err != nil {
return fmt.Errorf("Error creating logfile directory %s: %v", logfileDir, err)
}
logFile := path.Join(logfileDir, p.NodeName)
f, err := os.Create(path.Join(logFile))
if err != nil {
return fmt.Errorf("Error creating logfile %s: %v", logFile, err)
}
f.Close()
o.Output("Writing Chef Client output to " + logFile)
o = p
}
return p.runCommand(o, comm, cmd)
}
}
// Output implementation of terraform.UIOutput interface
func (p *provisioner) Output(output string) {
logFile := path.Join(logfileDir, p.NodeName)
f, err := os.OpenFile(logFile, os.O_APPEND|os.O_WRONLY, 0666)
if err != nil {
log.Printf("Error creating logfile %s: %v", logFile, err)
return
}
defer f.Close()
// These steps are needed to remove any ANSI escape codes used to colorize
// the output and to make sure we have proper line endings before writing
// the string to the logfile.
re := regexp.MustCompile(`\x1b\[[0-9;]+m`)
output = re.ReplaceAllString(output, "")
output = strings.Replace(output, "\r", "\n", -1)
if _, err := f.WriteString(output); err != nil {
log.Printf("Error writing output to logfile %s: %v", logFile, err)
}
if err := f.Sync(); err != nil {
log.Printf("Error saving logfile %s to disk: %v", logFile, err)
}
}
// runCommand is used to run already prepared commands
func (p *provisioner) runCommand(o terraform.UIOutput, comm communicator.Communicator, command string) error {
// Unless prevented, prefix the command with sudo
if p.useSudo {
command = "sudo " + command
}
outR, outW := io.Pipe()
errR, errW := io.Pipe()
go p.copyOutput(o, outR)
go p.copyOutput(o, errR)
defer outW.Close()
defer errW.Close()
cmd := &remote.Cmd{
Command: command,
Stdout: outW,
Stderr: errW,
}
err := comm.Start(cmd)
if err != nil {
return fmt.Errorf("Error executing command %q: %v", cmd.Command, err)
}
if err := cmd.Wait(); err != nil {
return err
}
return nil
}
func (p *provisioner) copyOutput(o terraform.UIOutput, r io.Reader) {
lr := linereader.New(r)
for line := range lr.Ch {
o.Output(line)
}
}
func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
p := &provisioner{
Channel: d.Get("channel").(string),
ClientOptions: getStringList(d.Get("client_options")),
DisableReporting: d.Get("disable_reporting").(bool),
Environment: d.Get("environment").(string),
FetchChefCertificates: d.Get("fetch_chef_certificates").(bool),
LogToFile: d.Get("log_to_file").(bool),
UsePolicyfile: d.Get("use_policyfile").(bool),
PolicyGroup: d.Get("policy_group").(string),
PolicyName: d.Get("policy_name").(string),
HTTPProxy: d.Get("http_proxy").(string),
HTTPSProxy: d.Get("https_proxy").(string),
NOProxy: getStringList(d.Get("no_proxy")),
MaxRetries: d.Get("max_retries").(int),
NamedRunList: d.Get("named_run_list").(string),
NodeName: d.Get("node_name").(string),
OhaiHints: getStringList(d.Get("ohai_hints")),
OSType: d.Get("os_type").(string),
RecreateClient: d.Get("recreate_client").(bool),
PreventSudo: d.Get("prevent_sudo").(bool),
RetryOnExitCode: getRetryOnExitCodes(d),
RunList: getStringList(d.Get("run_list")),
SecretKey: d.Get("secret_key").(string),
ServerURL: d.Get("server_url").(string),
SkipInstall: d.Get("skip_install").(bool),
SkipRegister: d.Get("skip_register").(bool),
SSLVerifyMode: d.Get("ssl_verify_mode").(string),
UserName: d.Get("user_name").(string),
UserKey: d.Get("user_key").(string),
Version: d.Get("version").(string),
WaitForRetry: time.Duration(d.Get("wait_for_retry").(int)) * time.Second,
}
// Make sure the supplied URL has a trailing slash
p.ServerURL = strings.TrimSuffix(p.ServerURL, "/") + "/"
for i, hint := range p.OhaiHints {
hintPath, err := homedir.Expand(hint)
if err != nil {
return nil, fmt.Errorf("Error expanding the path %s: %v", hint, err)
}
p.OhaiHints[i] = hintPath
}
if attrs, ok := d.GetOk("attributes_json"); ok {
var m map[string]interface{}
if err := json.Unmarshal([]byte(attrs.(string)), &m); err != nil {
return nil, fmt.Errorf("Error parsing attributes_json: %v", err)
}
p.Attributes = m
}
if vaults, ok := d.GetOk("vault_json"); ok {
var m map[string]interface{}
if err := json.Unmarshal([]byte(vaults.(string)), &m); err != nil {
return nil, fmt.Errorf("Error parsing vault_json: %v", err)
}
v := make(map[string][]string)
for vault, items := range m {
switch items := items.(type) {
case []interface{}:
for _, item := range items {
if item, ok := item.(string); ok {
v[vault] = append(v[vault], item)
}
}
case interface{}:
if item, ok := items.(string); ok {
v[vault] = append(v[vault], item)
}
}
}
p.Vaults = v
}
return p, nil
}
func getRetryOnExitCodes(d *schema.ResourceData) map[int]bool {
result := make(map[int]bool)
v, ok := d.GetOk("retry_on_exit_code")
if !ok || v == nil {
// Use default exit codes
result[35] = true
result[37] = true
result[213] = true
return result
}
switch v := v.(type) {
case []interface{}:
for _, vv := range v {
if vv, ok := vv.(int); ok {
result[vv] = true
}
}
return result
default:
panic(fmt.Sprintf("Unsupported type: %T", v))
}
}
func getStringList(v interface{}) []string {
var result []string
switch v := v.(type) {
case nil:
return result
case []interface{}:
for _, vv := range v {
if vv, ok := vv.(string); ok {
result = append(result, vv)
}
}
return result
default:
panic(fmt.Sprintf("Unsupported type: %T", v))
}
}

View File

@ -1,435 +0,0 @@
package chef
import (
"fmt"
"path"
"testing"
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/configs/hcl2shim"
"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 TestResourceProvider_Validate_good(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"environment": "_default",
"node_name": "nodename1",
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) > 0 {
t.Fatalf("Errors: %v", errs)
}
}
func TestResourceProvider_Validate_bad(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"invalid": "nope",
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) == 0 {
t.Fatalf("Should have errors")
}
}
// Test that the JSON attributes with an unknown value don't
// validate.
func TestResourceProvider_Validate_computedValues(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"environment": "_default",
"node_name": "nodename1",
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
"attributes_json": hcl2shim.UnknownVariableValue,
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) > 0 {
t.Fatalf("Errors: %v", errs)
}
}
func TestResourceProvider_runChefClient(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
ChefCmd string
ConfDir string
Commands map[string]bool
}{
"Sudo": {
Config: map[string]interface{}{
"node_name": "nodename1",
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
},
ChefCmd: linuxChefCmd,
ConfDir: linuxConfDir,
Commands: map[string]bool{
fmt.Sprintf(`sudo %s -j %q -E "_default"`,
linuxChefCmd,
path.Join(linuxConfDir, "first-boot.json")): true,
},
},
"NoSudo": {
Config: map[string]interface{}{
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
},
ChefCmd: linuxChefCmd,
ConfDir: linuxConfDir,
Commands: map[string]bool{
fmt.Sprintf(`%s -j %q -E "_default"`,
linuxChefCmd,
path.Join(linuxConfDir, "first-boot.json")): true,
},
},
"Environment": {
Config: map[string]interface{}{
"environment": "production",
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
},
ChefCmd: windowsChefCmd,
ConfDir: windowsConfDir,
Commands: map[string]bool{
fmt.Sprintf(`%s -j %q -E "production"`,
windowsChefCmd,
path.Join(windowsConfDir, "first-boot.json")): true,
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
p.runChefClient = p.runChefClientFunc(tc.ChefCmd, tc.ConfDir)
p.useSudo = !p.PreventSudo
err = p.runChefClient(o, c)
if err != nil {
t.Fatalf("Test %q failed: %v", k, err)
}
}
}
func TestResourceProvider_fetchChefCertificates(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
KnifeCmd string
ConfDir string
Commands map[string]bool
}{
"Sudo": {
Config: map[string]interface{}{
"fetch_chef_certificates": true,
"node_name": "nodename1",
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
},
KnifeCmd: linuxKnifeCmd,
ConfDir: linuxConfDir,
Commands: map[string]bool{
fmt.Sprintf(`sudo %s ssl fetch -c %s`,
linuxKnifeCmd,
path.Join(linuxConfDir, "client.rb")): true,
},
},
"NoSudo": {
Config: map[string]interface{}{
"fetch_chef_certificates": true,
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
},
KnifeCmd: windowsKnifeCmd,
ConfDir: windowsConfDir,
Commands: map[string]bool{
fmt.Sprintf(`%s ssl fetch -c %s`,
windowsKnifeCmd,
path.Join(windowsConfDir, "client.rb")): true,
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
p.fetchChefCertificates = p.fetchChefCertificatesFunc(tc.KnifeCmd, tc.ConfDir)
p.useSudo = !p.PreventSudo
err = p.fetchChefCertificates(o, c)
if err != nil {
t.Fatalf("Test %q failed: %v", k, err)
}
}
}
func TestResourceProvider_configureVaults(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
GemCmd string
KnifeCmd string
ConfDir string
Commands map[string]bool
}{
"Linux Vault string": {
Config: map[string]interface{}{
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
"vault_json": `{"vault1": "item1"}`,
},
GemCmd: linuxGemCmd,
KnifeCmd: linuxKnifeCmd,
ConfDir: linuxConfDir,
Commands: map[string]bool{
fmt.Sprintf("%s install chef-vault", linuxGemCmd): true,
fmt.Sprintf("%s vault update vault1 item1 -C nodename1 -M client -c %s/client.rb "+
"-u bob --key %s/bob.pem", linuxKnifeCmd, linuxConfDir, linuxConfDir): true,
},
},
"Linux Vault []string": {
Config: map[string]interface{}{
"fetch_chef_certificates": true,
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
"vault_json": `{"vault1": ["item1", "item2"]}`,
},
GemCmd: linuxGemCmd,
KnifeCmd: linuxKnifeCmd,
ConfDir: linuxConfDir,
Commands: map[string]bool{
fmt.Sprintf("%s install chef-vault", linuxGemCmd): true,
fmt.Sprintf("%s vault update vault1 item1 -C nodename1 -M client -c %s/client.rb "+
"-u bob --key %s/bob.pem", linuxKnifeCmd, linuxConfDir, linuxConfDir): true,
fmt.Sprintf("%s vault update vault1 item2 -C nodename1 -M client -c %s/client.rb "+
"-u bob --key %s/bob.pem", linuxKnifeCmd, linuxConfDir, linuxConfDir): true,
},
},
"Linux Vault []string (recreate-client for vault)": {
Config: map[string]interface{}{
"fetch_chef_certificates": true,
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
"vault_json": `{"vault1": ["item1", "item2"]}`,
"recreate_client": true,
},
GemCmd: linuxGemCmd,
KnifeCmd: linuxKnifeCmd,
ConfDir: linuxConfDir,
Commands: map[string]bool{
fmt.Sprintf("%s install chef-vault", linuxGemCmd): true,
fmt.Sprintf("%s vault remove vault1 item1 -C \"nodename1\" -M client -c %s/client.rb "+
"-u bob --key %s/bob.pem", linuxKnifeCmd, linuxConfDir, linuxConfDir): true,
fmt.Sprintf("%s vault remove vault1 item2 -C \"nodename1\" -M client -c %s/client.rb "+
"-u bob --key %s/bob.pem", linuxKnifeCmd, linuxConfDir, linuxConfDir): true,
fmt.Sprintf("%s vault update vault1 item1 -C nodename1 -M client -c %s/client.rb "+
"-u bob --key %s/bob.pem", linuxKnifeCmd, linuxConfDir, linuxConfDir): true,
fmt.Sprintf("%s vault update vault1 item2 -C nodename1 -M client -c %s/client.rb "+
"-u bob --key %s/bob.pem", linuxKnifeCmd, linuxConfDir, linuxConfDir): true,
},
},
"Windows Vault string": {
Config: map[string]interface{}{
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
"vault_json": `{"vault1": "item1"}`,
},
GemCmd: windowsGemCmd,
KnifeCmd: windowsKnifeCmd,
ConfDir: windowsConfDir,
Commands: map[string]bool{
fmt.Sprintf("%s install chef-vault", windowsGemCmd): true,
fmt.Sprintf("%s vault update vault1 item1 -C nodename1 -M client -c %s/client.rb "+
"-u bob --key %s/bob.pem", windowsKnifeCmd, windowsConfDir, windowsConfDir): true,
},
},
"Windows Vault []string": {
Config: map[string]interface{}{
"fetch_chef_certificates": true,
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
"vault_json": `{"vault1": ["item1", "item2"]}`,
},
GemCmd: windowsGemCmd,
KnifeCmd: windowsKnifeCmd,
ConfDir: windowsConfDir,
Commands: map[string]bool{
fmt.Sprintf("%s install chef-vault", windowsGemCmd): true,
fmt.Sprintf("%s vault update vault1 item1 -C nodename1 -M client -c %s/client.rb "+
"-u bob --key %s/bob.pem", windowsKnifeCmd, windowsConfDir, windowsConfDir): true,
fmt.Sprintf("%s vault update vault1 item2 -C nodename1 -M client -c %s/client.rb "+
"-u bob --key %s/bob.pem", windowsKnifeCmd, windowsConfDir, windowsConfDir): true,
},
},
"Windows Vault [] string (recreate-client for vault)": {
Config: map[string]interface{}{
"fetch_chef_certificates": true,
"node_name": "nodename1",
"prevent_sudo": true,
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
"vault_json": `{"vault1": ["item1", "item2"]}`,
"recreate_client": true,
},
GemCmd: windowsGemCmd,
KnifeCmd: windowsKnifeCmd,
ConfDir: windowsConfDir,
Commands: map[string]bool{
fmt.Sprintf("%s install chef-vault", windowsGemCmd): true,
fmt.Sprintf("%s vault remove vault1 item1 -C \"nodename1\" -M client -c %s/client.rb "+
"-u bob --key %s/bob.pem", windowsKnifeCmd, windowsConfDir, windowsConfDir): true,
fmt.Sprintf("%s vault remove vault1 item2 -C \"nodename1\" -M client -c %s/client.rb "+
"-u bob --key %s/bob.pem", windowsKnifeCmd, windowsConfDir, windowsConfDir): true,
fmt.Sprintf("%s vault update vault1 item1 -C nodename1 -M client -c %s/client.rb "+
"-u bob --key %s/bob.pem", windowsKnifeCmd, windowsConfDir, windowsConfDir): true,
fmt.Sprintf("%s vault update vault1 item2 -C nodename1 -M client -c %s/client.rb "+
"-u bob --key %s/bob.pem", windowsKnifeCmd, windowsConfDir, windowsConfDir): true,
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
p.configureVaults = p.configureVaultsFunc(tc.GemCmd, tc.KnifeCmd, tc.ConfDir)
p.useSudo = !p.PreventSudo
err = p.configureVaults(o, c)
if err != nil {
t.Fatalf("Test %q failed: %v", k, err)
}
}
}
func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig {
return terraform.NewResourceConfigRaw(c)
}

View File

@ -1 +0,0 @@
OHAI-HINT-FILE

View File

@ -1,84 +0,0 @@
package chef
import (
"fmt"
"path"
"strings"
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/terraform"
)
const installScript = `
$winver = [System.Environment]::OSVersion.Version | %% {"{0}.{1}" -f $_.Major,$_.Minor}
switch ($winver)
{
"6.0" {$machine_os = "2008"}
"6.1" {$machine_os = "2008r2"}
"6.2" {$machine_os = "2012"}
"6.3" {$machine_os = "2012"}
default {$machine_os = "2008r2"}
}
if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"}
$url = "http://omnitruck.chef.io/%s/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v=%s"
$dest = [System.IO.Path]::GetTempFileName()
$dest = [System.IO.Path]::ChangeExtension($dest, ".msi")
$downloader = New-Object System.Net.WebClient
$http_proxy = '%s'
if ($http_proxy -ne '') {
$no_proxy = '%s'
if ($no_proxy -eq ''){
$no_proxy = "127.0.0.1"
}
$proxy = New-Object System.Net.WebProxy($http_proxy, $true, ,$no_proxy.Split(','))
$downloader.proxy = $proxy
}
Write-Host 'Downloading Chef Client...'
$downloader.DownloadFile($url, $dest)
Write-Host 'Installing Chef Client...'
Start-Process -FilePath msiexec -ArgumentList /qn, /i, $dest -Wait
`
func (p *provisioner) windowsInstallChefClient(o terraform.UIOutput, comm communicator.Communicator) error {
script := path.Join(path.Dir(comm.ScriptPath()), "ChefClient.ps1")
content := fmt.Sprintf(installScript, p.Channel, p.Version, p.HTTPProxy, strings.Join(p.NOProxy, ","))
// Copy the script to the new instance
if err := comm.UploadScript(script, strings.NewReader(content)); err != nil {
return fmt.Errorf("Uploading client.rb failed: %v", err)
}
// Execute the script to install Chef Client
installCmd := fmt.Sprintf("powershell -NoProfile -ExecutionPolicy Bypass -File %s", script)
return p.runCommand(o, comm, installCmd)
}
func (p *provisioner) windowsCreateConfigFiles(o terraform.UIOutput, comm communicator.Communicator) error {
// Make sure the config directory exists
cmd := fmt.Sprintf("cmd /c if not exist %q mkdir %q", windowsConfDir, windowsConfDir)
if err := p.runCommand(o, comm, cmd); err != nil {
return err
}
if len(p.OhaiHints) > 0 {
// Make sure the hits directory exists
hintsDir := path.Join(windowsConfDir, "ohai/hints")
cmd := fmt.Sprintf("cmd /c if not exist %q mkdir %q", hintsDir, hintsDir)
if err := p.runCommand(o, comm, cmd); err != nil {
return err
}
if err := p.deployOhaiHints(o, comm, hintsDir); err != nil {
return err
}
}
return p.deployConfigFiles(o, comm, windowsConfDir)
}

View File

@ -1,394 +0,0 @@
package chef
import (
"fmt"
"path"
"testing"
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
func TestResourceProvider_windowsInstallChefClient(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
Commands map[string]bool
UploadScripts map[string]string
}{
"Default": {
Config: map[string]interface{}{
"node_name": "nodename1",
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
},
Commands: map[string]bool{
"powershell -NoProfile -ExecutionPolicy Bypass -File ChefClient.ps1": true,
},
UploadScripts: map[string]string{
"ChefClient.ps1": defaultWindowsInstallScript,
},
},
"Proxy": {
Config: map[string]interface{}{
"http_proxy": "http://proxy.local",
"no_proxy": []interface{}{"http://local.local", "http://local.org"},
"node_name": "nodename1",
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
},
Commands: map[string]bool{
"powershell -NoProfile -ExecutionPolicy Bypass -File ChefClient.ps1": true,
},
UploadScripts: map[string]string{
"ChefClient.ps1": proxyWindowsInstallScript,
},
},
"Version": {
Config: map[string]interface{}{
"node_name": "nodename1",
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
"version": "11.18.6",
},
Commands: map[string]bool{
"powershell -NoProfile -ExecutionPolicy Bypass -File ChefClient.ps1": true,
},
UploadScripts: map[string]string{
"ChefClient.ps1": versionWindowsInstallScript,
},
},
"Channel": {
Config: map[string]interface{}{
"channel": "current",
"node_name": "nodename1",
"run_list": []interface{}{"cookbook::recipe"},
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
"version": "11.18.6",
},
Commands: map[string]bool{
"powershell -NoProfile -ExecutionPolicy Bypass -File ChefClient.ps1": true,
},
UploadScripts: map[string]string{
"ChefClient.ps1": channelWindowsInstallScript,
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
c.UploadScripts = tc.UploadScripts
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
p.useSudo = false
err = p.windowsInstallChefClient(o, c)
if err != nil {
t.Fatalf("Test %q failed: %v", k, err)
}
}
}
func TestResourceProvider_windowsCreateConfigFiles(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
Commands map[string]bool
Uploads map[string]string
}{
"Default": {
Config: map[string]interface{}{
"ohai_hints": []interface{}{"testdata/ohaihint.json"},
"node_name": "nodename1",
"run_list": []interface{}{"cookbook::recipe"},
"secret_key": "SECRET-KEY",
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
},
Commands: map[string]bool{
fmt.Sprintf("cmd /c if not exist %q mkdir %q", windowsConfDir, windowsConfDir): true,
fmt.Sprintf("cmd /c if not exist %q mkdir %q",
path.Join(windowsConfDir, "ohai/hints"),
path.Join(windowsConfDir, "ohai/hints")): true,
},
Uploads: map[string]string{
windowsConfDir + "/client.rb": defaultWindowsClientConf,
windowsConfDir + "/encrypted_data_bag_secret": "SECRET-KEY",
windowsConfDir + "/first-boot.json": `{"run_list":["cookbook::recipe"]}`,
windowsConfDir + "/ohai/hints/ohaihint.json": "OHAI-HINT-FILE",
windowsConfDir + "/bob.pem": "USER-KEY",
},
},
"Proxy": {
Config: map[string]interface{}{
"http_proxy": "http://proxy.local",
"https_proxy": "https://proxy.local",
"no_proxy": []interface{}{"http://local.local", "https://local.local"},
"node_name": "nodename1",
"run_list": []interface{}{"cookbook::recipe"},
"secret_key": "SECRET-KEY",
"server_url": "https://chef.local",
"ssl_verify_mode": "verify_none",
"user_name": "bob",
"user_key": "USER-KEY",
},
Commands: map[string]bool{
fmt.Sprintf("cmd /c if not exist %q mkdir %q", windowsConfDir, windowsConfDir): true,
},
Uploads: map[string]string{
windowsConfDir + "/client.rb": proxyWindowsClientConf,
windowsConfDir + "/first-boot.json": `{"run_list":["cookbook::recipe"]}`,
windowsConfDir + "/encrypted_data_bag_secret": "SECRET-KEY",
windowsConfDir + "/bob.pem": "USER-KEY",
},
},
"Attributes JSON": {
Config: map[string]interface{}{
"attributes_json": `{"key1":{"subkey1":{"subkey2a":["val1","val2","val3"],` +
`"subkey2b":{"subkey3":"value3"}}},"key2":"value2"}`,
"node_name": "nodename1",
"run_list": []interface{}{"cookbook::recipe"},
"secret_key": "SECRET-KEY",
"server_url": "https://chef.local",
"user_name": "bob",
"user_key": "USER-KEY",
},
Commands: map[string]bool{
fmt.Sprintf("cmd /c if not exist %q mkdir %q", windowsConfDir, windowsConfDir): true,
},
Uploads: map[string]string{
windowsConfDir + "/client.rb": defaultWindowsClientConf,
windowsConfDir + "/encrypted_data_bag_secret": "SECRET-KEY",
windowsConfDir + "/bob.pem": "USER-KEY",
windowsConfDir + "/first-boot.json": `{"key1":{"subkey1":{"subkey2a":["val1","val2","val3"],` +
`"subkey2b":{"subkey3":"value3"}}},"key2":"value2","run_list":["cookbook::recipe"]}`,
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
c.Uploads = tc.Uploads
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
p.useSudo = false
err = p.windowsCreateConfigFiles(o, c)
if err != nil {
t.Fatalf("Test %q failed: %v", k, err)
}
}
}
const defaultWindowsInstallScript = `
$winver = [System.Environment]::OSVersion.Version | % {"{0}.{1}" -f $_.Major,$_.Minor}
switch ($winver)
{
"6.0" {$machine_os = "2008"}
"6.1" {$machine_os = "2008r2"}
"6.2" {$machine_os = "2012"}
"6.3" {$machine_os = "2012"}
default {$machine_os = "2008r2"}
}
if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"}
$url = "http://omnitruck.chef.io/stable/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v="
$dest = [System.IO.Path]::GetTempFileName()
$dest = [System.IO.Path]::ChangeExtension($dest, ".msi")
$downloader = New-Object System.Net.WebClient
$http_proxy = ''
if ($http_proxy -ne '') {
$no_proxy = ''
if ($no_proxy -eq ''){
$no_proxy = "127.0.0.1"
}
$proxy = New-Object System.Net.WebProxy($http_proxy, $true, ,$no_proxy.Split(','))
$downloader.proxy = $proxy
}
Write-Host 'Downloading Chef Client...'
$downloader.DownloadFile($url, $dest)
Write-Host 'Installing Chef Client...'
Start-Process -FilePath msiexec -ArgumentList /qn, /i, $dest -Wait
`
const proxyWindowsInstallScript = `
$winver = [System.Environment]::OSVersion.Version | % {"{0}.{1}" -f $_.Major,$_.Minor}
switch ($winver)
{
"6.0" {$machine_os = "2008"}
"6.1" {$machine_os = "2008r2"}
"6.2" {$machine_os = "2012"}
"6.3" {$machine_os = "2012"}
default {$machine_os = "2008r2"}
}
if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"}
$url = "http://omnitruck.chef.io/stable/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v="
$dest = [System.IO.Path]::GetTempFileName()
$dest = [System.IO.Path]::ChangeExtension($dest, ".msi")
$downloader = New-Object System.Net.WebClient
$http_proxy = 'http://proxy.local'
if ($http_proxy -ne '') {
$no_proxy = 'http://local.local,http://local.org'
if ($no_proxy -eq ''){
$no_proxy = "127.0.0.1"
}
$proxy = New-Object System.Net.WebProxy($http_proxy, $true, ,$no_proxy.Split(','))
$downloader.proxy = $proxy
}
Write-Host 'Downloading Chef Client...'
$downloader.DownloadFile($url, $dest)
Write-Host 'Installing Chef Client...'
Start-Process -FilePath msiexec -ArgumentList /qn, /i, $dest -Wait
`
const versionWindowsInstallScript = `
$winver = [System.Environment]::OSVersion.Version | % {"{0}.{1}" -f $_.Major,$_.Minor}
switch ($winver)
{
"6.0" {$machine_os = "2008"}
"6.1" {$machine_os = "2008r2"}
"6.2" {$machine_os = "2012"}
"6.3" {$machine_os = "2012"}
default {$machine_os = "2008r2"}
}
if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"}
$url = "http://omnitruck.chef.io/stable/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v=11.18.6"
$dest = [System.IO.Path]::GetTempFileName()
$dest = [System.IO.Path]::ChangeExtension($dest, ".msi")
$downloader = New-Object System.Net.WebClient
$http_proxy = ''
if ($http_proxy -ne '') {
$no_proxy = ''
if ($no_proxy -eq ''){
$no_proxy = "127.0.0.1"
}
$proxy = New-Object System.Net.WebProxy($http_proxy, $true, ,$no_proxy.Split(','))
$downloader.proxy = $proxy
}
Write-Host 'Downloading Chef Client...'
$downloader.DownloadFile($url, $dest)
Write-Host 'Installing Chef Client...'
Start-Process -FilePath msiexec -ArgumentList /qn, /i, $dest -Wait
`
const channelWindowsInstallScript = `
$winver = [System.Environment]::OSVersion.Version | % {"{0}.{1}" -f $_.Major,$_.Minor}
switch ($winver)
{
"6.0" {$machine_os = "2008"}
"6.1" {$machine_os = "2008r2"}
"6.2" {$machine_os = "2012"}
"6.3" {$machine_os = "2012"}
default {$machine_os = "2008r2"}
}
if ([System.IntPtr]::Size -eq 4) {$machine_arch = "i686"} else {$machine_arch = "x86_64"}
$url = "http://omnitruck.chef.io/current/chef/download?p=windows&pv=$machine_os&m=$machine_arch&v=11.18.6"
$dest = [System.IO.Path]::GetTempFileName()
$dest = [System.IO.Path]::ChangeExtension($dest, ".msi")
$downloader = New-Object System.Net.WebClient
$http_proxy = ''
if ($http_proxy -ne '') {
$no_proxy = ''
if ($no_proxy -eq ''){
$no_proxy = "127.0.0.1"
}
$proxy = New-Object System.Net.WebProxy($http_proxy, $true, ,$no_proxy.Split(','))
$downloader.proxy = $proxy
}
Write-Host 'Downloading Chef Client...'
$downloader.DownloadFile($url, $dest)
Write-Host 'Installing Chef Client...'
Start-Process -FilePath msiexec -ArgumentList /qn, /i, $dest -Wait
`
const defaultWindowsClientConf = `log_location STDOUT
chef_server_url "https://chef.local/"
node_name "nodename1"`
const proxyWindowsClientConf = `log_location STDOUT
chef_server_url "https://chef.local/"
node_name "nodename1"
http_proxy "http://proxy.local"
ENV['http_proxy'] = "http://proxy.local"
ENV['HTTP_PROXY'] = "http://proxy.local"
https_proxy "https://proxy.local"
ENV['https_proxy'] = "https://proxy.local"
ENV['HTTPS_PROXY'] = "https://proxy.local"
no_proxy "http://local.local,https://local.local"
ENV['no_proxy'] = "http://local.local,https://local.local"
ssl_verify_mode :verify_none`

View File

@ -1,377 +0,0 @@
package habitat
import (
"bytes"
"errors"
"fmt"
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/terraform"
"path"
"path/filepath"
"strings"
"text/template"
)
const installURL = "https://raw.githubusercontent.com/habitat-sh/habitat/master/components/hab/install.sh"
const systemdUnit = `[Unit]
Description=Habitat Supervisor
[Service]
ExecStart=/bin/hab sup run{{ .SupOptions }}
Restart=on-failure
{{ if .GatewayAuthToken -}}
Environment="HAB_SUP_GATEWAY_AUTH_TOKEN={{ .GatewayAuthToken }}"
{{ end -}}
{{ if .BuilderAuthToken -}}
Environment="HAB_AUTH_TOKEN={{ .BuilderAuthToken }}"
{{ end -}}
[Install]
WantedBy=default.target
`
func (p *provisioner) linuxInstallHabitat(o terraform.UIOutput, comm communicator.Communicator) error {
// Download the hab installer
if err := p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("curl --silent -L0 %s > install.sh", installURL))); err != nil {
return err
}
// Run the install script
var command string
if p.Version == "" {
command = fmt.Sprintf("bash ./install.sh ")
} else {
command = fmt.Sprintf("bash ./install.sh -v %s", p.Version)
}
if err := p.runCommand(o, comm, p.linuxGetCommand(command)); err != nil {
return err
}
// Accept the license
if p.AcceptLicense {
var cmd string
if p.UseSudo == true {
cmd = "env HAB_LICENSE=accept sudo -E /bin/bash -c 'hab -V'"
} else {
cmd = "env HAB_LICENSE=accept /bin/bash -c 'hab -V'"
}
if err := p.runCommand(o, comm, cmd); err != nil {
return err
}
}
// Create the hab user
if err := p.createHabUser(o, comm); err != nil {
return err
}
// Cleanup the installer
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("rm -f install.sh")))
}
func (p *provisioner) createHabUser(o terraform.UIOutput, comm communicator.Communicator) error {
var addUser bool
// Install busybox to get us the user tools we need
if err := p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("hab install core/busybox"))); err != nil {
return err
}
// Check for existing hab user
if err := p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("hab pkg exec core/busybox id hab"))); err != nil {
o.Output("No existing hab user detected, creating...")
addUser = true
}
if addUser {
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("hab pkg exec core/busybox adduser -D -g \"\" hab")))
}
return nil
}
func (p *provisioner) linuxStartHabitat(o terraform.UIOutput, comm communicator.Communicator) error {
// Install the supervisor first
var command string
if p.Version == "" {
command += p.linuxGetCommand(fmt.Sprintf("hab install core/hab-sup"))
} else {
command += p.linuxGetCommand(fmt.Sprintf("hab install core/hab-sup/%s", p.Version))
}
if err := p.runCommand(o, comm, command); err != nil {
return err
}
// Build up supervisor options
options := ""
if p.PermanentPeer {
options += " --permanent-peer"
}
if p.ListenCtl != "" {
options += fmt.Sprintf(" --listen-ctl %s", p.ListenCtl)
}
if p.ListenGossip != "" {
options += fmt.Sprintf(" --listen-gossip %s", p.ListenGossip)
}
if p.ListenHTTP != "" {
options += fmt.Sprintf(" --listen-http %s", p.ListenHTTP)
}
if p.Peer != "" {
options += fmt.Sprintf(" %s", p.Peer)
}
if len(p.Peers) > 0 {
if len(p.Peers) == 1 {
options += fmt.Sprintf(" --peer %s", p.Peers[0])
} else {
options += fmt.Sprintf(" --peer %s", strings.Join(p.Peers, " --peer "))
}
}
if p.RingKey != "" {
options += fmt.Sprintf(" --ring %s", p.RingKey)
}
if p.URL != "" {
options += fmt.Sprintf(" --url %s", p.URL)
}
if p.Channel != "" {
options += fmt.Sprintf(" --channel %s", p.Channel)
}
if p.Events != "" {
options += fmt.Sprintf(" --events %s", p.Events)
}
if p.Organization != "" {
options += fmt.Sprintf(" --org %s", p.Organization)
}
if p.HttpDisable == true {
options += fmt.Sprintf(" --http-disable")
}
if p.AutoUpdate == true {
options += fmt.Sprintf(" --auto-update")
}
p.SupOptions = options
// Start hab depending on service type
switch p.ServiceType {
case "unmanaged":
return p.linuxStartHabitatUnmanaged(o, comm, options)
case "systemd":
return p.linuxStartHabitatSystemd(o, comm, options)
default:
return errors.New("unsupported service type")
}
}
// This func is a little different than the others since we need to expose HAB_AUTH_TOKEN to a shell
// sub-process that's actually running the supervisor.
func (p *provisioner) linuxStartHabitatUnmanaged(o terraform.UIOutput, comm communicator.Communicator, options string) error {
var token string
// Create the sup directory for the log file
if err := p.runCommand(o, comm, p.linuxGetCommand("mkdir -p /hab/sup/default && chmod o+w /hab/sup/default")); err != nil {
return err
}
// Set HAB_AUTH_TOKEN if provided
if p.BuilderAuthToken != "" {
token = fmt.Sprintf("env HAB_AUTH_TOKEN=%s ", p.BuilderAuthToken)
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("(%ssetsid hab sup run%s > /hab/sup/default/sup.log 2>&1 <&1 &) ; sleep 1", token, options)))
}
func (p *provisioner) linuxStartHabitatSystemd(o terraform.UIOutput, comm communicator.Communicator, options string) error {
// Create a new template and parse the client config into it
unitString := template.Must(template.New("hab-supervisor.service").Parse(systemdUnit))
var buf bytes.Buffer
err := unitString.Execute(&buf, p)
if err != nil {
return fmt.Errorf("error executing %s.service template: %s", p.ServiceName, err)
}
if err := p.linuxUploadSystemdUnit(o, comm, &buf); err != nil {
return err
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("systemctl enable %s && systemctl start %s", p.ServiceName, p.ServiceName)))
}
func (p *provisioner) linuxUploadSystemdUnit(o terraform.UIOutput, comm communicator.Communicator, contents *bytes.Buffer) error {
destination := fmt.Sprintf("/etc/systemd/system/%s.service", p.ServiceName)
if p.UseSudo {
tempPath := fmt.Sprintf("/tmp/%s.service", p.ServiceName)
if err := comm.Upload(tempPath, contents); err != nil {
return err
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("mv %s %s", tempPath, destination)))
}
return comm.Upload(destination, contents)
}
func (p *provisioner) linuxUploadRingKey(o terraform.UIOutput, comm communicator.Communicator) error {
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf(`echo -e "%s" | hab ring key import`, p.RingKeyContent)))
}
func (p *provisioner) linuxUploadCtlSecret(o terraform.UIOutput, comm communicator.Communicator) error {
destination := fmt.Sprintf("/hab/sup/default/CTL_SECRET")
// Create the destination directory
err := p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("mkdir -p %s", filepath.Dir(destination))))
if err != nil {
return err
}
keyContent := strings.NewReader(p.CtlSecret)
if p.UseSudo {
tempPath := fmt.Sprintf("/tmp/CTL_SECRET")
if err := comm.Upload(tempPath, keyContent); err != nil {
return err
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("chown root:root %s && chmod 0600 %s && mv %s %s", tempPath, tempPath, tempPath, destination)))
}
return comm.Upload(destination, keyContent)
}
//
// Habitat Services
//
func (p *provisioner) linuxStartHabitatService(o terraform.UIOutput, comm communicator.Communicator, service Service) error {
var options string
if err := p.linuxInstallHabitatPackage(o, comm, service); err != nil {
return err
}
if err := p.uploadUserTOML(o, comm, service); err != nil {
return err
}
// Upload service group key
if service.ServiceGroupKey != "" {
err := p.uploadServiceGroupKey(o, comm, service.ServiceGroupKey)
if err != nil {
return err
}
}
if service.Topology != "" {
options += fmt.Sprintf(" --topology %s", service.Topology)
}
if service.Strategy != "" {
options += fmt.Sprintf(" --strategy %s", service.Strategy)
}
if service.Channel != "" {
options += fmt.Sprintf(" --channel %s", service.Channel)
}
if service.URL != "" {
options += fmt.Sprintf(" --url %s", service.URL)
}
if service.Group != "" {
options += fmt.Sprintf(" --group %s", service.Group)
}
for _, bind := range service.Binds {
options += fmt.Sprintf(" --bind %s", bind.toBindString())
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("hab svc load %s %s", service.Name, options)))
}
// In the future we'll remove the dedicated install once the synchronous load feature in hab-sup is
// available. Until then we install here to provide output and a noisy failure mechanism because
// if you install with the pkg load, it occurs asynchronously and fails quietly.
func (p *provisioner) linuxInstallHabitatPackage(o terraform.UIOutput, comm communicator.Communicator, service Service) error {
var options string
if service.Channel != "" {
options += fmt.Sprintf(" --channel %s", service.Channel)
}
if service.URL != "" {
options += fmt.Sprintf(" --url %s", service.URL)
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("hab pkg install %s %s", service.Name, options)))
}
func (p *provisioner) uploadServiceGroupKey(o terraform.UIOutput, comm communicator.Communicator, key string) error {
keyName := strings.Split(key, "\n")[1]
o.Output("Uploading service group key: " + keyName)
keyFileName := fmt.Sprintf("%s.box.key", keyName)
destPath := path.Join("/hab/cache/keys", keyFileName)
keyContent := strings.NewReader(key)
if p.UseSudo {
tempPath := path.Join("/tmp", keyFileName)
if err := comm.Upload(tempPath, keyContent); err != nil {
return err
}
return p.runCommand(o, comm, p.linuxGetCommand(fmt.Sprintf("mv %s %s", tempPath, destPath)))
}
return comm.Upload(destPath, keyContent)
}
func (p *provisioner) uploadUserTOML(o terraform.UIOutput, comm communicator.Communicator, service Service) error {
// Create the hab svc directory to lay down the user.toml before loading the service
o.Output("Uploading user.toml for service: " + service.Name)
destDir := fmt.Sprintf("/hab/user/%s/config", service.getPackageName(service.Name))
command := p.linuxGetCommand(fmt.Sprintf("mkdir -p %s", destDir))
if err := p.runCommand(o, comm, command); err != nil {
return err
}
userToml := strings.NewReader(service.UserTOML)
if p.UseSudo {
checksum := service.getServiceNameChecksum()
if err := comm.Upload(fmt.Sprintf("/tmp/user-%s.toml", checksum), userToml); err != nil {
return err
}
command = p.linuxGetCommand(fmt.Sprintf("chmod o-r /tmp/user-%s.toml && mv /tmp/user-%s.toml %s/user.toml", checksum, checksum, destDir))
return p.runCommand(o, comm, command)
}
return comm.Upload(path.Join(destDir, "user.toml"), userToml)
}
func (p *provisioner) linuxGetCommand(command string) string {
// Always set HAB_NONINTERACTIVE & HAB_NOCOLORING
env := fmt.Sprintf("env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true")
// Set builder auth token
if p.BuilderAuthToken != "" {
env += fmt.Sprintf(" HAB_AUTH_TOKEN=%s", p.BuilderAuthToken)
}
if p.UseSudo {
command = fmt.Sprintf("%s sudo -E /bin/bash -c '%s'", env, command)
} else {
command = fmt.Sprintf("%s /bin/bash -c '%s'", env, command)
}
return command
}

View File

@ -1,348 +0,0 @@
package habitat
import (
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
"testing"
)
const linuxDefaultSystemdUnitFileContents = `[Unit]
Description=Habitat Supervisor
[Service]
ExecStart=/bin/hab sup run --peer host1 --peer 1.2.3.4 --auto-update
Restart=on-failure
[Install]
WantedBy=default.target`
const linuxCustomSystemdUnitFileContents = `[Unit]
Description=Habitat Supervisor
[Service]
ExecStart=/bin/hab sup run --listen-ctl 192.168.0.1:8443 --listen-gossip 192.168.10.1:9443 --listen-http 192.168.20.1:8080 --peer host1 --peer host2 --peer 1.2.3.4 --peer 5.6.7.8 --peer foo.example.com
Restart=on-failure
Environment="HAB_SUP_GATEWAY_AUTH_TOKEN=ea7-beef"
Environment="HAB_AUTH_TOKEN=dead-beef"
[Install]
WantedBy=default.target`
func TestLinuxProvisioner_linuxInstallHabitat(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
Commands map[string]bool
}{
"Installation with sudo": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": true,
"use_sudo": true,
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'curl --silent -L0 https://raw.githubusercontent.com/habitat-sh/habitat/master/components/hab/install.sh > install.sh'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'bash ./install.sh -v 0.79.1'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab install core/busybox'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab pkg exec core/busybox adduser -D -g \"\" hab'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'rm -f install.sh'": true,
},
},
"Installation without sudo": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": true,
"use_sudo": false,
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'curl --silent -L0 https://raw.githubusercontent.com/habitat-sh/habitat/master/components/hab/install.sh > install.sh'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'bash ./install.sh -v 0.79.1'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'hab install core/busybox'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'hab pkg exec core/busybox adduser -D -g \"\" hab'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'rm -f install.sh'": true,
},
},
"Installation with Habitat license acceptance": {
Config: map[string]interface{}{
"version": "0.81.0",
"accept_license": true,
"auto_update": true,
"use_sudo": true,
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'curl --silent -L0 https://raw.githubusercontent.com/habitat-sh/habitat/master/components/hab/install.sh > install.sh'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'bash ./install.sh -v 0.81.0'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab install core/busybox'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab pkg exec core/busybox adduser -D -g \"\" hab'": true,
"env HAB_LICENSE=accept sudo -E /bin/bash -c 'hab -V'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'rm -f install.sh'": true,
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
err = p.linuxInstallHabitat(o, c)
if err != nil {
t.Fatalf("Test %q failed: %v", k, err)
}
}
}
func TestLinuxProvisioner_linuxStartHabitat(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
Commands map[string]bool
Uploads map[string]string
}{
"Start systemd Habitat with sudo": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": true,
"use_sudo": true,
"service_name": "hab-sup",
"peer": "--peer host1",
"peers": []interface{}{"1.2.3.4"},
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab install core/hab-sup/0.79.1'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'systemctl enable hab-sup && systemctl start hab-sup'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'mv /tmp/hab-sup.service /etc/systemd/system/hab-sup.service'": true,
},
Uploads: map[string]string{
"/tmp/hab-sup.service": linuxDefaultSystemdUnitFileContents,
},
},
"Start systemd Habitat without sudo": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": true,
"use_sudo": false,
"service_name": "hab-sup",
"peer": "--peer host1",
"peers": []interface{}{"1.2.3.4"},
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'hab install core/hab-sup/0.79.1'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true /bin/bash -c 'systemctl enable hab-sup && systemctl start hab-sup'": true,
},
Uploads: map[string]string{
"/etc/systemd/system/hab-sup.service": linuxDefaultSystemdUnitFileContents,
},
},
"Start unmanaged Habitat with sudo": {
Config: map[string]interface{}{
"version": "0.81.0",
"license": "accept-no-persist",
"auto_update": true,
"use_sudo": true,
"service_type": "unmanaged",
"peer": "--peer host1",
"peers": []interface{}{"1.2.3.4"},
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab install core/hab-sup/0.81.0'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'mkdir -p /hab/sup/default && chmod o+w /hab/sup/default'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c '(setsid hab sup run --peer host1 --peer 1.2.3.4 --auto-update > /hab/sup/default/sup.log 2>&1 <&1 &) ; sleep 1'": true,
},
Uploads: map[string]string{
"/etc/systemd/system/hab-sup.service": linuxDefaultSystemdUnitFileContents,
},
},
"Start Habitat with custom config": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": false,
"use_sudo": true,
"service_name": "hab-sup",
"peer": "--peer host1 --peer host2",
"peers": []interface{}{"1.2.3.4", "5.6.7.8", "foo.example.com"},
"listen_ctl": "192.168.0.1:8443",
"listen_gossip": "192.168.10.1:9443",
"listen_http": "192.168.20.1:8080",
"builder_auth_token": "dead-beef",
"gateway_auth_token": "ea7-beef",
"ctl_secret": "bad-beef",
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true HAB_AUTH_TOKEN=dead-beef sudo -E /bin/bash -c 'hab install core/hab-sup/0.79.1'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true HAB_AUTH_TOKEN=dead-beef sudo -E /bin/bash -c 'systemctl enable hab-sup && systemctl start hab-sup'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true HAB_AUTH_TOKEN=dead-beef sudo -E /bin/bash -c 'mv /tmp/hab-sup.service /etc/systemd/system/hab-sup.service'": true,
},
Uploads: map[string]string{
"/tmp/hab-sup.service": linuxCustomSystemdUnitFileContents,
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
c.Uploads = tc.Uploads
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
err = p.linuxStartHabitat(o, c)
if err != nil {
t.Fatalf("Test %q failed: %v", k, err)
}
}
}
func TestLinuxProvisioner_linuxUploadRingKey(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
Commands map[string]bool
}{
"Upload ring key": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": true,
"use_sudo": true,
"service_name": "hab-sup",
"peers": []interface{}{"1.2.3.4"},
"ring_key": "test-ring",
"ring_key_content": "dead-beef",
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'echo -e \"dead-beef\" | hab ring key import'": true,
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
err = p.linuxUploadRingKey(o, c)
if err != nil {
t.Fatalf("Test %q failed: %v", k, err)
}
}
}
func TestLinuxProvisioner_linuxStartHabitatService(t *testing.T) {
cases := map[string]struct {
Config map[string]interface{}
Commands map[string]bool
Uploads map[string]string
}{
"Start Habitat service with sudo": {
Config: map[string]interface{}{
"version": "0.79.1",
"auto_update": false,
"use_sudo": true,
"service_name": "hab-sup",
"peers": []interface{}{"1.2.3.4"},
"ring_key": "test-ring",
"ring_key_content": "dead-beef",
"service": []interface{}{
map[string]interface{}{
"name": "core/foo",
"topology": "standalone",
"strategy": "none",
"channel": "stable",
"user_toml": "[config]\nlisten = 0.0.0.0:8080",
"bind": []interface{}{
map[string]interface{}{
"alias": "backend",
"service": "bar",
"group": "default",
},
},
},
map[string]interface{}{
"name": "core/bar",
"topology": "standalone",
"strategy": "rolling",
"channel": "staging",
"user_toml": "[config]\nlisten = 0.0.0.0:443",
},
},
},
Commands: map[string]bool{
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab pkg install core/foo --channel stable'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'mkdir -p /hab/user/foo/config'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'chmod o-r /tmp/user-a5b83ec1b302d109f41852ae17379f75c36dff9bc598aae76b6f7c9cd425fd76.toml && mv /tmp/user-a5b83ec1b302d109f41852ae17379f75c36dff9bc598aae76b6f7c9cd425fd76.toml /hab/user/foo/config/user.toml'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab svc load core/foo --topology standalone --strategy none --channel stable --bind backend:bar.default'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab pkg install core/bar --channel staging'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'mkdir -p /hab/user/bar/config'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'chmod o-r /tmp/user-6466ae3283ae1bd4737b00367bc676c6465b25682169ea5f7da222f3f078a5bf.toml && mv /tmp/user-6466ae3283ae1bd4737b00367bc676c6465b25682169ea5f7da222f3f078a5bf.toml /hab/user/bar/config/user.toml'": true,
"env HAB_NONINTERACTIVE=true HAB_NOCOLORING=true sudo -E /bin/bash -c 'hab svc load core/bar --topology standalone --strategy rolling --channel staging'": true,
},
Uploads: map[string]string{
"/tmp/user-a5b83ec1b302d109f41852ae17379f75c36dff9bc598aae76b6f7c9cd425fd76.toml": "[config]\nlisten = 0.0.0.0:8080",
"/tmp/user-6466ae3283ae1bd4737b00367bc676c6465b25682169ea5f7da222f3f078a5bf.toml": "[config]\nlisten = 0.0.0.0:443",
},
},
}
o := new(terraform.MockUIOutput)
c := new(communicator.MockCommunicator)
for k, tc := range cases {
c.Commands = tc.Commands
c.Uploads = tc.Uploads
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, tc.Config),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
var errs []error
for _, s := range p.Services {
err = p.linuxStartHabitatService(o, c, s)
if err != nil {
errs = append(errs, err)
}
}
if len(errs) > 0 {
for _, e := range errs {
t.Logf("Test %q failed: %v", k, e)
t.Fail()
}
}
}
}

View File

@ -1,572 +0,0 @@
package habitat
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"io"
"net/url"
"strings"
version "github.com/hashicorp/go-version"
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/configs/hcl2shim"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/helper/validation"
"github.com/hashicorp/terraform/terraform"
"github.com/mitchellh/go-linereader"
)
type provisioner struct {
Version string
AutoUpdate bool
HttpDisable bool
Services []Service
PermanentPeer bool
ListenCtl string
ListenGossip string
ListenHTTP string
Peer string
Peers []string
RingKey string
RingKeyContent string
CtlSecret string
SkipInstall bool
UseSudo bool
ServiceType string
ServiceName string
URL string
Channel string
Events string
Organization string
GatewayAuthToken string
BuilderAuthToken string
SupOptions string
AcceptLicense bool
installHabitat provisionFn
startHabitat provisionFn
uploadRingKey provisionFn
uploadCtlSecret provisionFn
startHabitatService provisionServiceFn
osType string
}
type provisionFn func(terraform.UIOutput, communicator.Communicator) error
type provisionServiceFn func(terraform.UIOutput, communicator.Communicator, Service) error
func Provisioner() terraform.ResourceProvisioner {
return &schema.Provisioner{
Schema: map[string]*schema.Schema{
"version": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"auto_update": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"http_disable": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"peer": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"peers": &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
},
"service_type": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "systemd",
ValidateFunc: validation.StringInSlice([]string{"systemd", "unmanaged"}, false),
},
"service_name": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "hab-supervisor",
},
"use_sudo": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: true,
},
"accept_license": &schema.Schema{
Type: schema.TypeBool,
Required: true,
},
"permanent_peer": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
Default: false,
},
"listen_ctl": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"listen_gossip": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"listen_http": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"ring_key": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"ring_key_content": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"ctl_secret": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"url": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) {
u, err := url.Parse(val.(string))
if err != nil {
errs = append(errs, fmt.Errorf("invalid URL specified for %q: %v", key, err))
}
if u.Scheme == "" {
errs = append(errs, fmt.Errorf("invalid URL specified for %q (scheme must be specified)", key))
}
return warns, errs
},
},
"channel": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"events": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"organization": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"gateway_auth_token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"builder_auth_token": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"service": &schema.Schema{
Type: schema.TypeSet,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"name": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"binds": &schema.Schema{
Type: schema.TypeList,
Elem: &schema.Schema{Type: schema.TypeString},
Optional: true,
},
"bind": &schema.Schema{
Type: schema.TypeSet,
Elem: &schema.Resource{
Schema: map[string]*schema.Schema{
"alias": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"service": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"group": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
},
},
Optional: true,
},
"topology": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringInSlice([]string{"leader", "standalone"}, false),
},
"user_toml": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"strategy": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: validation.StringInSlice([]string{"none", "rolling", "at-once"}, false),
},
"channel": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"group": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"url": &schema.Schema{
Type: schema.TypeString,
Optional: true,
ValidateFunc: func(val interface{}, key string) (warns []string, errs []error) {
u, err := url.Parse(val.(string))
if err != nil {
errs = append(errs, fmt.Errorf("invalid URL specified for %q: %v", key, err))
}
if u.Scheme == "" {
errs = append(errs, fmt.Errorf("invalid URL specified for %q (scheme must be specified)", key))
}
return warns, errs
},
},
"application": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"environment": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"service_key": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
},
Optional: true,
},
},
ApplyFunc: applyFn,
ValidateFunc: validateFn,
}
}
func applyFn(ctx context.Context) error {
o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
s := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
p, err := decodeConfig(d)
if err != nil {
return err
}
// Automatically determine the OS type
switch t := s.Ephemeral.ConnInfo["type"]; t {
case "ssh", "":
p.osType = "linux"
case "winrm":
p.osType = "windows"
default:
return fmt.Errorf("unsupported connection type: %s", t)
}
switch p.osType {
case "linux":
p.installHabitat = p.linuxInstallHabitat
p.uploadRingKey = p.linuxUploadRingKey
p.uploadCtlSecret = p.linuxUploadCtlSecret
p.startHabitat = p.linuxStartHabitat
p.startHabitatService = p.linuxStartHabitatService
case "windows":
return fmt.Errorf("windows is not supported yet for the habitat provisioner")
default:
return fmt.Errorf("unsupported os type: %s", p.osType)
}
// Get a new communicator
comm, err := communicator.New(s)
if err != nil {
return err
}
retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
defer cancel()
// Wait and retry until we establish the connection
err = communicator.Retry(retryCtx, func() error {
return comm.Connect(o)
})
if err != nil {
return err
}
defer comm.Disconnect()
if !p.SkipInstall {
o.Output("Installing habitat...")
if err := p.installHabitat(o, comm); err != nil {
return err
}
}
if p.RingKeyContent != "" {
o.Output("Uploading supervisor ring key...")
if err := p.uploadRingKey(o, comm); err != nil {
return err
}
}
if p.CtlSecret != "" {
o.Output("Uploading ctl secret...")
if err := p.uploadCtlSecret(o, comm); err != nil {
return err
}
}
o.Output("Starting the habitat supervisor...")
if err := p.startHabitat(o, comm); err != nil {
return err
}
if p.Services != nil {
for _, service := range p.Services {
o.Output("Starting service: " + service.Name)
if err := p.startHabitatService(o, comm, service); err != nil {
return err
}
}
}
return nil
}
func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) {
ringKeyContent, ok := c.Get("ring_key_content")
if ok && ringKeyContent != "" && ringKeyContent != hcl2shim.UnknownVariableValue {
ringKey, ringOk := c.Get("ring_key")
if ringOk && ringKey == "" {
es = append(es, errors.New("if ring_key_content is specified, ring_key must be specified as well"))
}
}
v, ok := c.Get("version")
if ok && v != nil && strings.TrimSpace(v.(string)) != "" {
if _, err := version.NewVersion(v.(string)); err != nil {
es = append(es, errors.New(v.(string)+" is not a valid version."))
}
}
acceptLicense, ok := c.Get("accept_license")
if ok && !acceptLicense.(bool) {
if v != nil && strings.TrimSpace(v.(string)) != "" {
versionOld, _ := version.NewVersion("0.79.0")
versionRequired, _ := version.NewVersion(v.(string))
if versionRequired.GreaterThan(versionOld) {
es = append(es, errors.New("Habitat end user license agreement needs to be accepted, set the accept_license argument to true to accept"))
}
} else { // blank means latest version
es = append(es, errors.New("Habitat end user license agreement needs to be accepted, set the accept_license argument to true to accept"))
}
}
// Validate service level configs
services, ok := c.Get("service")
if ok {
data, dataOk := services.(string)
if dataOk {
es = append(es, fmt.Errorf("service '%v': must be a block", data))
}
}
return ws, es
}
type Service struct {
Name string
Strategy string
Topology string
Channel string
Group string
URL string
Binds []Bind
BindStrings []string
UserTOML string
AppName string
Environment string
ServiceGroupKey string
}
func (s *Service) getPackageName(fullName string) string {
return strings.Split(fullName, "/")[1]
}
func (s *Service) getServiceNameChecksum() string {
return fmt.Sprintf("%x", sha256.Sum256([]byte(s.Name)))
}
type Bind struct {
Alias string
Service string
Group string
}
func (b *Bind) toBindString() string {
return fmt.Sprintf("%s:%s.%s", b.Alias, b.Service, b.Group)
}
func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
p := &provisioner{
Version: d.Get("version").(string),
AutoUpdate: d.Get("auto_update").(bool),
HttpDisable: d.Get("http_disable").(bool),
Peer: d.Get("peer").(string),
Peers: getPeers(d.Get("peers").([]interface{})),
Services: getServices(d.Get("service").(*schema.Set).List()),
UseSudo: d.Get("use_sudo").(bool),
AcceptLicense: d.Get("accept_license").(bool),
ServiceType: d.Get("service_type").(string),
ServiceName: d.Get("service_name").(string),
RingKey: d.Get("ring_key").(string),
RingKeyContent: d.Get("ring_key_content").(string),
CtlSecret: d.Get("ctl_secret").(string),
PermanentPeer: d.Get("permanent_peer").(bool),
ListenCtl: d.Get("listen_ctl").(string),
ListenGossip: d.Get("listen_gossip").(string),
ListenHTTP: d.Get("listen_http").(string),
URL: d.Get("url").(string),
Channel: d.Get("channel").(string),
Events: d.Get("events").(string),
Organization: d.Get("organization").(string),
BuilderAuthToken: d.Get("builder_auth_token").(string),
GatewayAuthToken: d.Get("gateway_auth_token").(string),
}
return p, nil
}
func getPeers(v []interface{}) []string {
peers := make([]string, 0, len(v))
for _, rawPeerData := range v {
peers = append(peers, rawPeerData.(string))
}
return peers
}
func getServices(v []interface{}) []Service {
services := make([]Service, 0, len(v))
for _, rawServiceData := range v {
serviceData := rawServiceData.(map[string]interface{})
name := (serviceData["name"].(string))
strategy := (serviceData["strategy"].(string))
topology := (serviceData["topology"].(string))
channel := (serviceData["channel"].(string))
group := (serviceData["group"].(string))
url := (serviceData["url"].(string))
app := (serviceData["application"].(string))
env := (serviceData["environment"].(string))
userToml := (serviceData["user_toml"].(string))
serviceGroupKey := (serviceData["service_key"].(string))
var bindStrings []string
binds := getBinds(serviceData["bind"].(*schema.Set).List())
for _, b := range serviceData["binds"].([]interface{}) {
bind, err := getBindFromString(b.(string))
if err != nil {
return nil
}
binds = append(binds, bind)
}
service := Service{
Name: name,
Strategy: strategy,
Topology: topology,
Channel: channel,
Group: group,
URL: url,
UserTOML: userToml,
BindStrings: bindStrings,
Binds: binds,
AppName: app,
Environment: env,
ServiceGroupKey: serviceGroupKey,
}
services = append(services, service)
}
return services
}
func getBinds(v []interface{}) []Bind {
binds := make([]Bind, 0, len(v))
for _, rawBindData := range v {
bindData := rawBindData.(map[string]interface{})
alias := bindData["alias"].(string)
service := bindData["service"].(string)
group := bindData["group"].(string)
bind := Bind{
Alias: alias,
Service: service,
Group: group,
}
binds = append(binds, bind)
}
return binds
}
func (p *provisioner) copyOutput(o terraform.UIOutput, r io.Reader) {
lr := linereader.New(r)
for line := range lr.Ch {
o.Output(line)
}
}
func (p *provisioner) runCommand(o terraform.UIOutput, comm communicator.Communicator, command string) error {
outR, outW := io.Pipe()
errR, errW := io.Pipe()
go p.copyOutput(o, outR)
go p.copyOutput(o, errR)
defer outW.Close()
defer errW.Close()
cmd := &remote.Cmd{
Command: command,
Stdout: outW,
Stderr: errW,
}
if err := comm.Start(cmd); err != nil {
return fmt.Errorf("error executing command %q: %v", cmd.Command, err)
}
if err := cmd.Wait(); err != nil {
return err
}
return nil
}
func getBindFromString(bind string) (Bind, error) {
t := strings.FieldsFunc(bind, func(d rune) bool {
switch d {
case ':', '.':
return true
}
return false
})
if len(t) != 3 {
return Bind{}, errors.New("invalid bind specification: " + bind)
}
return Bind{Alias: t[0], Service: t[1], Group: t[2]}, nil
}

View File

@ -1,91 +0,0 @@
package habitat
import (
"testing"
"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("error: %s", err)
}
}
func TestResourceProvisioner_Validate_good(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"peers": []interface{}{"1.2.3.4"},
"version": "0.32.0",
"service_type": "systemd",
"accept_license": false,
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) > 0 {
t.Fatalf("Errors: %v", errs)
}
}
func TestResourceProvisioner_Validate_bad(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"service_type": "invalidtype",
"url": "badurl",
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
// 3 errors, bad service_type, bad url, missing accept_license
if len(errs) != 3 {
t.Fatalf("Should have three errors, got %d", len(errs))
}
}
func TestResourceProvisioner_Validate_bad_service_config(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"accept_license": true,
"service": []interface{}{
map[string]interface{}{
"name": "core/foo",
"strategy": "bar",
"topology": "baz",
"url": "badurl",
},
},
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) != 3 {
t.Fatalf("Should have three errors, got %d", len(errs))
}
}
func TestResourceProvisioner_Validate_bad_service_definition(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"service": "core/vault",
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) != 3 {
t.Fatalf("Should have three errors, got %d", len(errs))
}
}
func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig {
return terraform.NewResourceConfigRaw(c)
}

View File

@ -1,74 +0,0 @@
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
}

View File

@ -1,65 +0,0 @@
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
}

View File

@ -1,379 +0,0 @@
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)
}
}
}
}

View File

@ -1,359 +0,0 @@
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
}

View File

@ -1,123 +0,0 @@
package puppet
import (
"testing"
"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 {
return terraform.NewResourceConfigRaw(c)
}

View File

@ -1,71 +0,0 @@
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
}

View File

@ -1,393 +0,0 @@
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)
}
}
}
}

View File

@ -1,525 +0,0 @@
// This package implements a provisioner for Terraform that executes a
// saltstack state within the remote machine
//
// Adapted from gitub.com/hashicorp/packer/provisioner/salt-masterless
package saltmasterless
import (
"bytes"
"context"
"errors"
"fmt"
"io"
"os"
"path/filepath"
"github.com/hashicorp/terraform/communicator"
"github.com/hashicorp/terraform/communicator/remote"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
linereader "github.com/mitchellh/go-linereader"
)
type provisionFn func(terraform.UIOutput, communicator.Communicator) error
type provisioner struct {
SkipBootstrap bool
BootstrapArgs string
LocalStateTree string
DisableSudo bool
CustomState string
MinionConfig string
LocalPillarRoots string
RemoteStateTree string
RemotePillarRoots string
TempConfigDir string
NoExitOnFailure bool
LogLevel string
SaltCallArgs string
CmdArgs string
}
const DefaultStateTreeDir = "/srv/salt"
const DefaultPillarRootDir = "/srv/pillar"
// Provisioner returns a salt-masterless provisioner
func Provisioner() terraform.ResourceProvisioner {
return &schema.Provisioner{
Schema: map[string]*schema.Schema{
"local_state_tree": &schema.Schema{
Type: schema.TypeString,
Required: true,
},
"local_pillar_roots": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"remote_state_tree": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: DefaultStateTreeDir,
},
"remote_pillar_roots": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: DefaultPillarRootDir,
},
"temp_config_dir": &schema.Schema{
Type: schema.TypeString,
Optional: true,
Default: "/tmp/salt",
},
"skip_bootstrap": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"no_exit_on_failure": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"bootstrap_args": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"disable_sudo": &schema.Schema{
Type: schema.TypeBool,
Optional: true,
},
"custom_state": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"minion_config_file": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"cmd_args": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"salt_call_args": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
"log_level": &schema.Schema{
Type: schema.TypeString,
Optional: true,
},
},
ApplyFunc: applyFn,
ValidateFunc: validateFn,
}
}
// Apply executes the file provisioner
func applyFn(ctx context.Context) error {
// Decode the raw config for this provisioner
o := ctx.Value(schema.ProvOutputKey).(terraform.UIOutput)
d := ctx.Value(schema.ProvConfigDataKey).(*schema.ResourceData)
connState := ctx.Value(schema.ProvRawStateKey).(*terraform.InstanceState)
p, err := decodeConfig(d)
if err != nil {
return err
}
// Get a new communicator
comm, err := communicator.New(connState)
if err != nil {
return err
}
retryCtx, cancel := context.WithTimeout(ctx, comm.Timeout())
defer cancel()
// Wait and retry until we establish the connection
err = communicator.Retry(retryCtx, func() error {
return comm.Connect(o)
})
if err != nil {
return err
}
// Wait for the context to end and then disconnect
go func() {
<-ctx.Done()
comm.Disconnect()
}()
var src, dst string
o.Output("Provisioning with Salt...")
if !p.SkipBootstrap {
cmd := &remote.Cmd{
// Fallback on wget if curl failed for any reason (such as not being installed)
Command: fmt.Sprintf("curl -L https://bootstrap.saltstack.com -o /tmp/install_salt.sh || wget -O /tmp/install_salt.sh https://bootstrap.saltstack.com"),
}
o.Output(fmt.Sprintf("Downloading saltstack bootstrap to /tmp/install_salt.sh"))
if err = comm.Start(cmd); err != nil {
err = fmt.Errorf("Unable to download Salt: %s", err)
}
if err := cmd.Wait(); err != nil {
return err
}
outR, outW := io.Pipe()
errR, errW := io.Pipe()
go copyOutput(o, outR)
go copyOutput(o, errR)
defer outW.Close()
defer errW.Close()
cmd = &remote.Cmd{
Command: fmt.Sprintf("%s /tmp/install_salt.sh %s", p.sudo("sh"), p.BootstrapArgs),
Stdout: outW,
Stderr: errW,
}
o.Output(fmt.Sprintf("Installing Salt with command %s", cmd.Command))
if err := comm.Start(cmd); err != nil {
return fmt.Errorf("Unable to install Salt: %s", err)
}
if err := cmd.Wait(); err != nil {
return err
}
}
o.Output(fmt.Sprintf("Creating remote temporary directory: %s", p.TempConfigDir))
if err := p.createDir(o, comm, p.TempConfigDir); err != nil {
return fmt.Errorf("Error creating remote temporary directory: %s", err)
}
if p.MinionConfig != "" {
o.Output(fmt.Sprintf("Uploading minion config: %s", p.MinionConfig))
src = p.MinionConfig
dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion"))
if err = p.uploadFile(o, comm, dst, src); err != nil {
return fmt.Errorf("Error uploading local minion config file to remote: %s", err)
}
// move minion config into /etc/salt
o.Output(fmt.Sprintf("Make sure directory %s exists", "/etc/salt"))
if err := p.createDir(o, comm, "/etc/salt"); err != nil {
return fmt.Errorf("Error creating remote salt configuration directory: %s", err)
}
src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "minion"))
dst = "/etc/salt/minion"
if err = p.moveFile(o, comm, dst, src); err != nil {
return fmt.Errorf("Unable to move %s/minion to /etc/salt/minion: %s", p.TempConfigDir, err)
}
}
o.Output(fmt.Sprintf("Uploading local state tree: %s", p.LocalStateTree))
src = p.LocalStateTree
dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states"))
if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil {
return fmt.Errorf("Error uploading local state tree to remote: %s", err)
}
// move state tree from temporary directory
src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "states"))
dst = p.RemoteStateTree
if err = p.removeDir(o, comm, dst); err != nil {
return fmt.Errorf("Unable to clear salt tree: %s", err)
}
if err = p.moveFile(o, comm, dst, src); err != nil {
return fmt.Errorf("Unable to move %s/states to %s: %s", p.TempConfigDir, dst, err)
}
if p.LocalPillarRoots != "" {
o.Output(fmt.Sprintf("Uploading local pillar roots: %s", p.LocalPillarRoots))
src = p.LocalPillarRoots
dst = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar"))
if err = p.uploadDir(o, comm, dst, src, []string{".git"}); err != nil {
return fmt.Errorf("Error uploading local pillar roots to remote: %s", err)
}
// move pillar root from temporary directory
src = filepath.ToSlash(filepath.Join(p.TempConfigDir, "pillar"))
dst = p.RemotePillarRoots
if err = p.removeDir(o, comm, dst); err != nil {
return fmt.Errorf("Unable to clear pillar root: %s", err)
}
if err = p.moveFile(o, comm, dst, src); err != nil {
return fmt.Errorf("Unable to move %s/pillar to %s: %s", p.TempConfigDir, dst, err)
}
}
outR, outW := io.Pipe()
errR, errW := io.Pipe()
go copyOutput(o, outR)
go copyOutput(o, errR)
defer outW.Close()
defer errW.Close()
o.Output(fmt.Sprintf("Running: salt-call --local %s", p.CmdArgs))
cmd := &remote.Cmd{
Command: p.sudo(fmt.Sprintf("salt-call --local %s", p.CmdArgs)),
Stdout: outW,
Stderr: errW,
}
if err = comm.Start(cmd); err != nil {
err = fmt.Errorf("Error executing salt-call: %s", err)
}
if err := cmd.Wait(); err != nil {
return err
}
return nil
}
// Prepends sudo to supplied command if config says to
func (p *provisioner) sudo(cmd string) string {
if p.DisableSudo {
return cmd
}
return "sudo " + cmd
}
func validateDirConfig(path string, name string, required bool) error {
if required == true && path == "" {
return fmt.Errorf("%s cannot be empty", name)
} else if required == false && path == "" {
return nil
}
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err)
} else if !info.IsDir() {
return fmt.Errorf("%s: path '%s' must point to a directory", name, path)
}
return nil
}
func validateFileConfig(path string, name string, required bool) error {
if required == true && path == "" {
return fmt.Errorf("%s cannot be empty", name)
} else if required == false && path == "" {
return nil
}
info, err := os.Stat(path)
if err != nil {
return fmt.Errorf("%s: path '%s' is invalid: %s", name, path, err)
} else if info.IsDir() {
return fmt.Errorf("%s: path '%s' must point to a file", name, path)
}
return nil
}
func (p *provisioner) uploadFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error {
f, err := os.Open(src)
if err != nil {
return fmt.Errorf("Error opening: %s", err)
}
defer f.Close()
if err = comm.Upload(dst, f); err != nil {
return fmt.Errorf("Error uploading %s: %s", src, err)
}
return nil
}
func (p *provisioner) moveFile(o terraform.UIOutput, comm communicator.Communicator, dst, src string) error {
o.Output(fmt.Sprintf("Moving %s to %s", src, dst))
cmd := &remote.Cmd{Command: fmt.Sprintf(p.sudo("mv %s %s"), src, dst)}
if err := comm.Start(cmd); err != nil {
return fmt.Errorf("Unable to move %s to %s: %s", src, dst, err)
}
if err := cmd.Wait(); err != nil {
return err
}
return nil
}
func (p *provisioner) createDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error {
o.Output(fmt.Sprintf("Creating directory: %s", dir))
cmd := &remote.Cmd{
Command: fmt.Sprintf("mkdir -p '%s'", dir),
}
if err := comm.Start(cmd); err != nil {
return err
}
if err := cmd.Wait(); err != nil {
return err
}
return nil
}
func (p *provisioner) removeDir(o terraform.UIOutput, comm communicator.Communicator, dir string) error {
o.Output(fmt.Sprintf("Removing directory: %s", dir))
cmd := &remote.Cmd{
Command: fmt.Sprintf("rm -rf '%s'", dir),
}
if err := comm.Start(cmd); err != nil {
return err
}
if err := cmd.Wait(); err != nil {
return err
}
return nil
}
func (p *provisioner) uploadDir(o terraform.UIOutput, comm communicator.Communicator, dst, src string, ignore []string) error {
if err := p.createDir(o, comm, dst); err != nil {
return err
}
// Make sure there is a trailing "/" so that the directory isn't
// created on the other side.
if src[len(src)-1] != '/' {
src = src + "/"
}
return comm.UploadDir(dst, src)
}
// Validate checks if the required arguments are configured
func validateFn(c *terraform.ResourceConfig) (ws []string, es []error) {
// require a salt state tree
localStateTreeTmp, ok := c.Get("local_state_tree")
var localStateTree string
if !ok {
es = append(es,
errors.New("Required local_state_tree is not set"))
} else {
localStateTree = localStateTreeTmp.(string)
}
err := validateDirConfig(localStateTree, "local_state_tree", true)
if err != nil {
es = append(es, err)
}
var localPillarRoots string
localPillarRootsTmp, ok := c.Get("local_pillar_roots")
if !ok {
localPillarRoots = ""
} else {
localPillarRoots = localPillarRootsTmp.(string)
}
err = validateDirConfig(localPillarRoots, "local_pillar_roots", false)
if err != nil {
es = append(es, err)
}
var minionConfig string
minionConfigTmp, ok := c.Get("minion_config_file")
if !ok {
minionConfig = ""
} else {
minionConfig = minionConfigTmp.(string)
}
err = validateFileConfig(minionConfig, "minion_config_file", false)
if err != nil {
es = append(es, err)
}
var remoteStateTree string
remoteStateTreeTmp, ok := c.Get("remote_state_tree")
if !ok {
remoteStateTree = DefaultStateTreeDir
} else {
remoteStateTree = remoteStateTreeTmp.(string)
}
var remotePillarRoots string
remotePillarRootsTmp, ok := c.Get("remote_pillar_roots")
if !ok {
remotePillarRoots = DefaultPillarRootDir
} else {
remotePillarRoots = remotePillarRootsTmp.(string)
}
if minionConfig != "" && (remoteStateTree != DefaultStateTreeDir || remotePillarRoots != DefaultPillarRootDir) {
es = append(es,
errors.New("remote_state_tree and remote_pillar_roots only apply when minion_config_file is not used"))
}
if len(es) > 0 {
return ws, es
}
return ws, es
}
func decodeConfig(d *schema.ResourceData) (*provisioner, error) {
p := &provisioner{
LocalStateTree: d.Get("local_state_tree").(string),
LogLevel: d.Get("log_level").(string),
SaltCallArgs: d.Get("salt_call_args").(string),
CmdArgs: d.Get("cmd_args").(string),
MinionConfig: d.Get("minion_config_file").(string),
CustomState: d.Get("custom_state").(string),
DisableSudo: d.Get("disable_sudo").(bool),
BootstrapArgs: d.Get("bootstrap_args").(string),
NoExitOnFailure: d.Get("no_exit_on_failure").(bool),
SkipBootstrap: d.Get("skip_bootstrap").(bool),
TempConfigDir: d.Get("temp_config_dir").(string),
RemotePillarRoots: d.Get("remote_pillar_roots").(string),
RemoteStateTree: d.Get("remote_state_tree").(string),
LocalPillarRoots: d.Get("local_pillar_roots").(string),
}
// build the command line args to pass onto salt
var cmdArgs bytes.Buffer
if p.CustomState == "" {
cmdArgs.WriteString(" state.highstate")
} else {
cmdArgs.WriteString(" state.sls ")
cmdArgs.WriteString(p.CustomState)
}
if p.MinionConfig == "" {
// pass --file-root and --pillar-root if no minion_config_file is supplied
if p.RemoteStateTree != "" {
cmdArgs.WriteString(" --file-root=")
cmdArgs.WriteString(p.RemoteStateTree)
} else {
cmdArgs.WriteString(" --file-root=")
cmdArgs.WriteString(DefaultStateTreeDir)
}
if p.RemotePillarRoots != "" {
cmdArgs.WriteString(" --pillar-root=")
cmdArgs.WriteString(p.RemotePillarRoots)
} else {
cmdArgs.WriteString(" --pillar-root=")
cmdArgs.WriteString(DefaultPillarRootDir)
}
}
if !p.NoExitOnFailure {
cmdArgs.WriteString(" --retcode-passthrough")
}
if p.LogLevel == "" {
cmdArgs.WriteString(" -l info")
} else {
cmdArgs.WriteString(" -l ")
cmdArgs.WriteString(p.LogLevel)
}
if p.SaltCallArgs != "" {
cmdArgs.WriteString(" ")
cmdArgs.WriteString(p.SaltCallArgs)
}
p.CmdArgs = cmdArgs.String()
return p, nil
}
func copyOutput(
o terraform.UIOutput, r io.Reader) {
lr := linereader.New(r)
for line := range lr.Ch {
o.Output(line)
}
}

View File

@ -1,452 +0,0 @@
package saltmasterless
import (
"io/ioutil"
"os"
"strings"
"testing"
"github.com/hashicorp/terraform/helper/schema"
"github.com/hashicorp/terraform/terraform"
)
func testConfig(t *testing.T, c map[string]interface{}) *terraform.ResourceConfig {
return terraform.NewResourceConfigRaw(c)
}
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 TestResourceProvisioner_Validate_good(t *testing.T) {
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
if err != nil {
t.Fatalf("Error when creating temp dir: %v", err)
}
defer os.RemoveAll(dir) // clean up
c := testConfig(t, map[string]interface{}{
"local_state_tree": dir,
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) > 0 {
t.Fatalf("Errors: %v", errs)
}
}
func TestResourceProvider_Validate_missing_required(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"remote_state_tree": "_default",
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) == 0 {
t.Fatalf("Should have errors")
}
}
func TestResourceProvider_Validate_LocalStateTree_doesnt_exist(t *testing.T) {
c := testConfig(t, map[string]interface{}{
"local_state_tree": "/i/dont/exist",
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) == 0 {
t.Fatalf("Should have errors")
}
}
func TestResourceProvisioner_Validate_invalid(t *testing.T) {
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
if err != nil {
t.Fatalf("Error when creating temp dir: %v", err)
}
defer os.RemoveAll(dir) // clean up
c := testConfig(t, map[string]interface{}{
"local_state_tree": dir,
"i_am_not_valid": "_invalid",
})
warn, errs := Provisioner().Validate(c)
if len(warn) > 0 {
t.Fatalf("Warnings: %v", warn)
}
if len(errs) == 0 {
t.Fatalf("Should have errors")
}
}
func TestProvisionerPrepare_CustomState(t *testing.T) {
c := map[string]interface{}{
"local_state_tree": "/tmp/local_state_tree",
}
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
if !strings.Contains(p.CmdArgs, "state.highstate") {
t.Fatal("CmdArgs should contain state.highstate")
}
if err != nil {
t.Fatalf("err: %s", err)
}
c = map[string]interface{}{
"local_state_tree": "/tmp/local_state_tree",
"custom_state": "custom",
}
p, err = decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
)
if err != nil {
t.Fatalf("Error: %v", err)
}
if !strings.Contains(p.CmdArgs, "state.sls custom") {
t.Fatal("CmdArgs should contain state.sls custom")
}
if err != nil {
t.Fatalf("err: %s", err)
}
}
func TestProvisionerPrepare_MinionConfig(t *testing.T) {
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
if err != nil {
t.Fatalf("Error when creating temp dir: %v", err)
}
defer os.RemoveAll(dir) // clean up
c := testConfig(t, map[string]interface{}{
"local_state_tree": dir,
"minion_config_file": "i/dont/exist",
})
warns, errs := Provisioner().Validate(c)
if len(warns) > 0 {
t.Fatalf("Warnings: %v", warns)
}
if len(errs) == 0 {
t.Fatalf("Should have error")
}
tf, err := ioutil.TempFile("", "minion")
if err != nil {
t.Fatalf("error tempfile: %s", err)
}
defer os.Remove(tf.Name())
c = testConfig(t, map[string]interface{}{
"local_state_tree": dir,
"minion_config_file": tf.Name(),
})
warns, errs = Provisioner().Validate(c)
if len(warns) > 0 {
t.Fatalf("Warnings: %v", warns)
}
if len(errs) > 0 {
t.Fatalf("errs: %s", errs)
}
}
func TestProvisionerPrepare_MinionConfig_RemoteStateTree(t *testing.T) {
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
if err != nil {
t.Fatalf("Error when creating temp dir: %v", err)
}
c := testConfig(t, map[string]interface{}{
"local_state_tree": dir,
"minion_config_file": "i/dont/exist",
"remote_state_tree": "i/dont/exist/remote_state_tree",
})
warns, errs := Provisioner().Validate(c)
if len(warns) > 0 {
t.Fatalf("Warnings: %v", warns)
}
if len(errs) == 0 {
t.Fatalf("Should be error")
}
}
func TestProvisionerPrepare_MinionConfig_RemotePillarRoots(t *testing.T) {
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
if err != nil {
t.Fatalf("Error when creating temp dir: %v", err)
}
c := testConfig(t, map[string]interface{}{
"local_state_tree": dir,
"minion_config_file": "i/dont/exist",
"remote_pillar_roots": "i/dont/exist/remote_pillar_roots",
})
warns, errs := Provisioner().Validate(c)
if len(warns) > 0 {
t.Fatalf("Warnings: %v", warns)
}
if len(errs) == 0 {
t.Fatalf("Should be error")
}
}
func TestProvisionerPrepare_LocalPillarRoots(t *testing.T) {
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
if err != nil {
t.Fatalf("Error when creating temp dir: %v", err)
}
c := testConfig(t, map[string]interface{}{
"local_state_tree": dir,
"minion_config_file": "i/dont/exist",
"local_pillar_roots": "i/dont/exist/local_pillar_roots",
})
warns, errs := Provisioner().Validate(c)
if len(warns) > 0 {
t.Fatalf("Warnings: %v", warns)
}
if len(errs) == 0 {
t.Fatalf("Should be error")
}
}
func TestProvisionerSudo(t *testing.T) {
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
if err != nil {
t.Fatalf("Error when creating temp dir: %v", err)
}
c := map[string]interface{}{
"local_state_tree": dir,
}
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
)
withSudo := p.sudo("echo hello")
if withSudo != "sudo echo hello" {
t.Fatalf("sudo command not generated correctly")
}
c = map[string]interface{}{
"local_state_tree": dir,
"disable_sudo": "true",
}
p, err = decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
)
if err != nil {
t.Fatalf("err: %s", err)
}
withoutSudo := p.sudo("echo hello")
if withoutSudo != "echo hello" {
t.Fatalf("sudo-less command not generated correctly")
}
}
func TestProvisionerPrepare_RemoteStateTree(t *testing.T) {
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
if err != nil {
t.Fatalf("Error when creating temp dir: %v", err)
}
c := map[string]interface{}{
"local_state_tree": dir,
"remote_state_tree": "/remote_state_tree",
}
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
)
if err != nil {
t.Fatalf("err: %s", err)
}
if !strings.Contains(p.CmdArgs, "--file-root=/remote_state_tree") {
t.Fatal("--file-root should be set in CmdArgs")
}
}
func TestProvisionerPrepare_RemotePillarRoots(t *testing.T) {
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
if err != nil {
t.Fatalf("Error when creating temp dir: %v", err)
}
c := map[string]interface{}{
"local_state_tree": dir,
"remote_pillar_roots": "/remote_pillar_roots",
}
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
)
if err != nil {
t.Fatalf("err: %s", err)
}
if !strings.Contains(p.CmdArgs, "--pillar-root=/remote_pillar_roots") {
t.Fatal("--pillar-root should be set in CmdArgs")
}
}
func TestProvisionerPrepare_RemoteStateTree_Default(t *testing.T) {
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
if err != nil {
t.Fatalf("Error when creating temp dir: %v", err)
}
c := map[string]interface{}{
"local_state_tree": dir,
}
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
)
if err != nil {
t.Fatalf("err: %s", err)
}
if !strings.Contains(p.CmdArgs, "--file-root=/srv/salt") {
t.Fatal("--file-root should be set in CmdArgs")
}
}
func TestProvisionerPrepare_RemotePillarRoots_Default(t *testing.T) {
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
if err != nil {
t.Fatalf("Error when creating temp dir: %v", err)
}
c := map[string]interface{}{
"local_state_tree": dir,
}
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
)
if err != nil {
t.Fatalf("err: %s", err)
}
if !strings.Contains(p.CmdArgs, "--pillar-root=/srv/pillar") {
t.Fatal("--pillar-root should be set in CmdArgs")
}
}
func TestProvisionerPrepare_NoExitOnFailure(t *testing.T) {
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
if err != nil {
t.Fatalf("Error when creating temp dir: %v", err)
}
c := map[string]interface{}{
"local_state_tree": dir,
}
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
)
if err != nil {
t.Fatalf("err: %s", err)
}
if !strings.Contains(p.CmdArgs, "--retcode-passthrough") {
t.Fatal("--retcode-passthrough should be set in CmdArgs")
}
c = map[string]interface{}{
"no_exit_on_failure": true,
}
p, err = decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
)
if err != nil {
t.Fatalf("err: %s", err)
}
if strings.Contains(p.CmdArgs, "--retcode-passthrough") {
t.Fatal("--retcode-passthrough should not be set in CmdArgs")
}
}
func TestProvisionerPrepare_LogLevel(t *testing.T) {
dir, err := ioutil.TempDir("", "_terraform_saltmasterless_test")
if err != nil {
t.Fatalf("Error when creating temp dir: %v", err)
}
c := map[string]interface{}{
"local_state_tree": dir,
}
p, err := decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
)
if err != nil {
t.Fatalf("err: %s", err)
}
if !strings.Contains(p.CmdArgs, "-l info") {
t.Fatal("-l info should be set in CmdArgs")
}
c = map[string]interface{}{
"log_level": "debug",
}
p, err = decodeConfig(
schema.TestResourceDataRaw(t, Provisioner().(*schema.Provisioner).Schema, c),
)
if err != nil {
t.Fatalf("err: %s", err)
}
if !strings.Contains(p.CmdArgs, "-l debug") {
t.Fatal("-l debug should be set in CmdArgs")
}
}

View File

@ -4,13 +4,9 @@
package command
import (
chefprovisioner "github.com/hashicorp/terraform/builtin/provisioners/chef"
fileprovisioner "github.com/hashicorp/terraform/builtin/provisioners/file"
habitatprovisioner "github.com/hashicorp/terraform/builtin/provisioners/habitat"
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"
saltmasterlessprovisioner "github.com/hashicorp/terraform/builtin/provisioners/salt-masterless"
"github.com/hashicorp/terraform/plugin"
)
@ -18,11 +14,7 @@ import (
var InternalProviders = map[string]plugin.ProviderFunc{}
var InternalProvisioners = map[string]plugin.ProvisionerFunc{
"chef": chefprovisioner.Provisioner,
"file": fileprovisioner.Provisioner,
"habitat": habitatprovisioner.Provisioner,
"local-exec": localexecprovisioner.Provisioner,
"puppet": puppetprovisioner.Provisioner,
"remote-exec": remoteexecprovisioner.Provisioner,
"salt-masterless": saltmasterlessprovisioner.Provisioner,
"file": fileprovisioner.Provisioner,
"local-exec": localexecprovisioner.Provisioner,
"remote-exec": remoteexecprovisioner.Provisioner,
}

View File

@ -26,7 +26,7 @@ func TestInternalPlugin_InternalProviders(t *testing.T) {
}
func TestInternalPlugin_InternalProvisioners(t *testing.T) {
for _, name := range []string{"chef", "file", "local-exec", "remote-exec", "salt-masterless"} {
for _, name := range []string{"file", "local-exec", "remote-exec"} {
if _, ok := InternalProvisioners[name]; !ok {
t.Errorf("Expected to find %s in InternalProvisioners", name)
}

View File

@ -34,11 +34,12 @@ func decodeProvisionerBlock(block *hcl.Block) (*Provisioner, hcl.Diagnostics) {
switch pv.Type {
case "chef", "habitat", "puppet", "salt-masterless":
diags = append(diags, &hcl.Diagnostic{
Severity: hcl.DiagWarning,
Summary: fmt.Sprintf("The \"%s\" provisioner is deprecated", pv.Type),
Detail: fmt.Sprintf("The \"%s\" provisioner is deprecated and will be removed from future versions of Terraform. Visit https://learn.hashicorp.com/collections/terraform/provision for alternatives to using provisioners that are a better fit for the Terraform workflow.", pv.Type),
Severity: hcl.DiagError,
Summary: fmt.Sprintf("The \"%s\" provisioner has been removed", pv.Type),
Detail: fmt.Sprintf("The \"%s\" provisioner was deprecated in Terraform 0.13.4 has been removed from Terraform. Visit https://learn.hashicorp.com/collections/terraform/provision for alternatives to using provisioners that are a better fit for the Terraform workflow.", pv.Type),
Subject: &pv.TypeRange,
})
return nil, diags
}
if attr, exists := content.Attributes["when"]; exists {

View File

@ -0,0 +1,3 @@
resource "null_resource" "test" {
provisioner "habitat" {} # ERROR: The "habitat" provisioner has been removed
}

View File

@ -1,3 +0,0 @@
resource "null_resource" "test" {
provisioner "habitat" {} # WARNING: The "habitat" provisioner is deprecated
}

View File

@ -6,7 +6,7 @@ gofmt_files=$(gofmt -l `find . -name '*.go' | grep -v vendor`)
if [[ -n ${gofmt_files} ]]; then
echo 'gofmt needs running on the following files:'
echo "${gofmt_files}"
echo "You can use the command: \`make fmtcheck\` to reformat code."
echo "You can use the command: \`gofmt -w .\` to reformat code."
exit 1
fi

View File

@ -12,10 +12,7 @@ The `chef` provisioner installs, configures and runs the Chef Client on a remote
resource. The `chef` provisioner supports both `ssh` and `winrm` type
[connections](/docs/provisioners/connection.html).
!> **Note:** This provisioner has been deprecated as of Terraform 0.13.4 and will be
removed in a future version of Terraform. For most common situations there are better
alternatives to using provisioners. For more information, see
[the main Provisioners page](./).
!> **Note:** This provisioner was removed in the 0.14.0 version of Terraform after being deprecated as of Terraform 0.13.4. For most common situations there are better alternatives to using provisioners. For more information, see [the main Provisioners page](./).
## Requirements

View File

@ -10,10 +10,7 @@ description: |-
The `habitat` provisioner installs the [Habitat](https://habitat.sh) supervisor and loads configured services. This provisioner only supports Linux targets using the `ssh` connection type at this time.
!> **Note:** This provisioner has been deprecated as of Terraform 0.13.4 and will be
removed in a future version of Terraform. For most common situations there are better
alternatives to using provisioners. For more information, see
[the main Provisioners page](./).
!> **Note:** This provisioner was removed in the 0.14.0 version of Terraform after being deprecated as of Terraform 0.13.4. For most common situations there are better alternatives to using provisioners. For more information, see [the main Provisioners page](./).
## Requirements

View File

@ -12,10 +12,7 @@ The `puppet` provisioner installs, configures and runs the Puppet agent on a
remote resource. The `puppet` provisioner supports both `ssh` and `winrm` type
[connections](/docs/provisioners/connection.html).
!> **Note:** This provisioner has been deprecated as of Terraform 0.13.4 and will be
removed in a future version of Terraform. For most common situations there are better
alternatives to using provisioners. For more information, see
[the main Provisioners page](./).
!> **Note:** This provisioner was removed in the 0.14.0 version of Terraform after being deprecated as of Terraform 0.13.4. For most common situations there are better alternatives to using provisioners. For more information, see [the main Provisioners page](./).
## Requirements

View File

@ -13,10 +13,7 @@ Type: `salt-masterless`
The `salt-masterless` Terraform provisioner provisions machines built by Terraform
using [Salt](http://saltstack.com/) states, without connecting to a Salt master. The `salt-masterless` provisioner supports `ssh` [connections](/docs/provisioners/connection.html).
!> **Note:** This provisioner has been deprecated as of Terraform 0.13.4 and will be
removed in a future version of Terraform. For most common situations there are better
alternatives to using provisioners. For more information, see
[the main Provisioners page](./).
!> **Note:** This provisioner was removed in the 0.14.0 version of Terraform after being deprecated as of Terraform 0.13.4. For most common situations there are better alternatives to using provisioners. For more information, see [the main Provisioners page](./).
## Requirements