diff --git a/CHANGELOG.md b/CHANGELOG.md
index 28267c672..2cdf1b3f7 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,31 @@
## 1.3.5 (upcoming)
+### IMPROVEMENTS:
+* builder/amazon: AWS users can now use the Vault AWS engine to generate temporary credentials. [GH-7282]
+* builder/virtualbox: New "guest_additions_interface" option to enable attaching via a SATA interface. [GH-7298]
+* bulder/openstac: Deprecated compute/v2/images API [GH-7268]
+* post-processor/manifest: Add "custom_data" key to packer manifest post-processor [GH-7248]
+* builder/vmware: Add `cores` option for specifying the number of cores per socket. [GH-7191]
+* post-processor/googlecompute-export: Extend auth for the GCE-post-processors to act like the GCE builder. [GH-7222]
+* post-processor/googlecompute-import: Extend auth for the GCE-post-processors to act like the GCE builder. [GH-7222]
+
+### BUG FIXES:
+* provisioner/salt: Force powershell to overwrite duplicate files [GH-7281]
+* builder/vmware-esxi: Should properly strip whitespace from end of names of files stored on esxi. [GH-7310]
+* builder/hyper-v: Fix regression where we improperly handled spaces in switch names [GH-7266]
+* core: clean up Makefile [GH-7254][GH-7265]
+* builder/cloudstack: Updated sdk version; can now use ostype name in template_os option. [GH-7264]
+* builder/azure: Fixed Azure interactive authentication [GH-7276]
+* builder/hyper-v: Fix integer overflows in 32-bit builds [GH-7251]
+* builder/google: Change metadata url to use a FQDN fixing bug stemming from differing DNS/search domains. [GH-7260]
+* core: Fixes mismatches in checksums for dependencies for Go 1.11.4+ [GH-7261]
+* core: make sure 'only' option is completely ignored by post-processors [GH-7262]
+
+### Features:
+**new provisioner**`inspec` Added inspec.io provisioner #[GH-7180]
+**new post-processor** `digitalocean-import`Add digitalocean-import post-processor. [GH-7060]
+**new builder** `vagrant` allows users to call vagrant to provision starting from vagrant boxes and save them as new vagrant boxes. [GH-7221]
+
## 1.3.4 (January 30, 2019)
### IMPROVEMENTS:
* builder/alicloud: delete copied image and snapshots if corresponding options
diff --git a/CODEOWNERS b/CODEOWNERS
index 0c933bcd3..916948444 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -5,7 +5,7 @@
/builder/alicloud/ @chhaj5236
/builder/amazon/ebssurrogate/ @jen20
/builder/amazon/ebsvolume/ @jen20
-/builder/azure/ @boumenot
+/builder/azure/ @paulmey
/builder/hyperv/ @taliesins
/builder/lxc/ @ChrisLundquist
/builder/lxd/ @ChrisLundquist
diff --git a/Makefile b/Makefile
index b65b6f500..042c46f2f 100644
--- a/Makefile
+++ b/Makefile
@@ -11,7 +11,7 @@ GOPATH=$(shell go env GOPATH)
# gofmt
UNFORMATTED_FILES=$(shell find . -not -path "./vendor/*" -name "*.go" | xargs gofmt -s -l)
-EXECUTABLE_FILES=$(shell find . -type f -executable | egrep -v '^\./(website/[vendor|tmp]|vendor/|\.git|bin/|scripts/|pkg/)' | egrep -v '.*(\.sh|\.bats|\.git)' | egrep -v './provisioner/ansible/test-fixtures/exit1')
+EXECUTABLE_FILES=$(shell find . -type f -executable | egrep -v '^\./(website/[vendor|tmp]|vendor/|\.git|bin/|scripts/|pkg/)' | egrep -v '.*(\.sh|\.bats|\.git)' | egrep -v './provisioner/(ansible|inspec)/test-fixtures/exit1')
# Get the git commit
GIT_DIRTY=$(shell test -n "`git status --porcelain`" && echo "+CHANGES" || true)
diff --git a/builder/amazon/chroot/builder.go b/builder/amazon/chroot/builder.go
index 6ce5e2c16..3b1b4609e 100644
--- a/builder/amazon/chroot/builder.go
+++ b/builder/amazon/chroot/builder.go
@@ -205,6 +205,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Setup the state bag and initial state for the steps
state := new(multistep.BasicStateBag)
state.Put("config", &b.config)
+ state.Put("access_config", &b.config.AccessConfig)
+ state.Put("ami_config", &b.config.AMIConfig)
state.Put("ec2", ec2conn)
state.Put("awsSession", session)
state.Put("hook", hook)
diff --git a/builder/amazon/common/access_config.go b/builder/amazon/common/access_config.go
index d29845d3d..ed6910b34 100644
--- a/builder/amazon/common/access_config.go
+++ b/builder/amazon/common/access_config.go
@@ -16,8 +16,21 @@ import (
cleanhttp "github.com/hashicorp/go-cleanhttp"
commonhelper "github.com/hashicorp/packer/helper/common"
"github.com/hashicorp/packer/template/interpolate"
+ vaultapi "github.com/hashicorp/vault/api"
)
+type VaultAWSEngineOptions struct {
+ Name string `mapstructure:"name"`
+ RoleARN string `mapstructure:"role_arn"`
+ TTL string `mapstructure:"ttl"`
+ EngineName string `mapstructure:"engine_name"`
+}
+
+func (v *VaultAWSEngineOptions) Empty() bool {
+ return len(v.Name) == 0 && len(v.RoleARN) == 0 &&
+ len(v.EngineName) == 0 && len(v.TTL) == 0
+}
+
// AccessConfig is for common configuration related to AWS access
type AccessConfig struct {
AccessKey string `mapstructure:"access_key"`
@@ -32,6 +45,7 @@ type AccessConfig struct {
SkipMetadataApiCheck bool `mapstructure:"skip_metadata_api_check"`
Token string `mapstructure:"token"`
session *session.Session
+ VaultAWSEngine VaultAWSEngineOptions `mapstructure:"vault_aws_engine"`
getEC2Connection func() ec2iface.EC2API
}
@@ -44,6 +58,7 @@ func (c *AccessConfig) Session() (*session.Session, error) {
}
config := aws.NewConfig().WithCredentialsChainVerboseErrors(true)
+
staticCreds := credentials.NewStaticCredentials(c.AccessKey, c.SecretKey, c.Token)
if _, err := staticCreds.Get(); err != credentials.ErrStaticCredentialsEmpty {
config.WithCredentials(staticCreds)
@@ -122,14 +137,62 @@ func (c *AccessConfig) IsChinaCloud() bool {
return strings.HasPrefix(c.SessionRegion(), "cn-")
}
+func (c *AccessConfig) GetCredsFromVault() error {
+ // const EnvVaultAddress = "VAULT_ADDR"
+ // const EnvVaultToken = "VAULT_TOKEN"
+ vaultConfig := vaultapi.DefaultConfig()
+ cli, err := vaultapi.NewClient(vaultConfig)
+ if err != nil {
+ return fmt.Errorf("Error getting Vault client: %s", err)
+ }
+ if c.VaultAWSEngine.EngineName == "" {
+ c.VaultAWSEngine.EngineName = "aws"
+ }
+ path := fmt.Sprintf("/%s/creds/%s", c.VaultAWSEngine.EngineName,
+ c.VaultAWSEngine.Name)
+ secret, err := cli.Logical().Read(path)
+ if err != nil {
+ return fmt.Errorf("Error reading vault secret: %s", err)
+ }
+ if secret == nil {
+ return fmt.Errorf("Vault Secret does not exist at the given path.")
+ }
+
+ c.AccessKey = secret.Data["access_key"].(string)
+ c.SecretKey = secret.Data["secret_key"].(string)
+ token := secret.Data["security_token"]
+ if token != nil {
+ c.Token = token.(string)
+ } else {
+ c.Token = ""
+ }
+
+ return nil
+}
+
func (c *AccessConfig) Prepare(ctx *interpolate.Context) []error {
var errs []error
if c.SkipMetadataApiCheck {
log.Println("(WARN) skip_metadata_api_check ignored.")
}
- // Either both access and secret key must be set or neither of them should
- // be.
+
+ // Make sure it's obvious from the config how we're getting credentials:
+ // Vault, Packer config, or environemnt.
+ if !c.VaultAWSEngine.Empty() {
+ if len(c.AccessKey) > 0 {
+ errs = append(errs,
+ fmt.Errorf("If you have set vault_aws_engine, you must not set"+
+ " the access_key or secret_key."))
+ }
+ // Go ahead and grab those credentials from Vault now, so we can set
+ // the keys and token now.
+ err := c.GetCredsFromVault()
+ if err != nil {
+ errs = append(errs, err)
+ }
+ }
+
if (len(c.AccessKey) > 0) != (len(c.SecretKey) > 0) {
errs = append(errs,
fmt.Errorf("`access_key` and `secret_key` must both be either set or not set."))
diff --git a/builder/amazon/common/step_pre_validate.go b/builder/amazon/common/step_pre_validate.go
index 73b4022b7..f420be990 100644
--- a/builder/amazon/common/step_pre_validate.go
+++ b/builder/amazon/common/step_pre_validate.go
@@ -3,9 +3,12 @@ package common
import (
"context"
"fmt"
+ "log"
"github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/awserr"
"github.com/aws/aws-sdk-go/service/ec2"
+ retry "github.com/hashicorp/packer/common"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
@@ -20,6 +23,51 @@ type StepPreValidate struct {
func (s *StepPreValidate) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
+
+ if accessConfig, ok := state.GetOk("access_config"); ok {
+ accessconf := accessConfig.(*AccessConfig)
+ if !accessconf.VaultAWSEngine.Empty() {
+ // loop over the authentication a few times to give vault-created creds
+ // time to become eventually-consistent
+ ui.Say("You're using Vault-generated AWS credentials. It may take a " +
+ "few moments for them to become available on AWS. Waiting...")
+ err := retry.Retry(0.2, 30, 11, func(_ uint) (bool, error) {
+ ec2conn, err := accessconf.NewEC2Connection()
+ if err != nil {
+ return true, err
+ }
+ _, err = listEC2Regions(ec2conn)
+ if err != nil {
+ if awsErr, ok := err.(awserr.Error); ok && awsErr.Code() == "AuthFailure" {
+ log.Printf("Waiting for Vault-generated AWS credentials" +
+ " to pass authentication... trying again.")
+ return false, nil
+ }
+ return true, err
+ }
+ return true, nil
+ })
+
+ if err != nil {
+ state.Put("error", fmt.Errorf("Was unable to Authenticate to AWS using Vault-"+
+ "Generated Credentials within the retry timeout."))
+ return multistep.ActionHalt
+ }
+ }
+
+ if amiConfig, ok := state.GetOk("ami_config"); ok {
+ amiconf := amiConfig.(*AMIConfig)
+ if !amiconf.AMISkipRegionValidation {
+ regionsToValidate := append(amiconf.AMIRegions, accessconf.RawRegion)
+ err := accessconf.ValidateRegion(regionsToValidate...)
+ if err != nil {
+ state.Put("error", fmt.Errorf("error validating regions: %v", err))
+ return multistep.ActionHalt
+ }
+ }
+ }
+ }
+
if s.ForceDeregister {
ui.Say("Force Deregister flag found, skipping prevalidating AMI Name")
return multistep.ActionContinue
diff --git a/builder/amazon/ebs/builder.go b/builder/amazon/ebs/builder.go
index c1a36a71b..4734995b5 100644
--- a/builder/amazon/ebs/builder.go
+++ b/builder/amazon/ebs/builder.go
@@ -92,13 +92,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
if err != nil {
return nil, err
}
- if !b.config.AMISkipRegionValidation {
- regionsToValidate := append(b.config.AMIRegions, b.config.RawRegion)
- err := b.config.AccessConfig.ValidateRegion(regionsToValidate...)
- if err != nil {
- return nil, fmt.Errorf("error validating regions: %v", err)
- }
- }
+
ec2conn := ec2.New(session, &aws.Config{
HTTPClient: commonhelper.HttpClientWithEnvironmentProxy(),
})
@@ -106,6 +100,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Setup the state bag and initial state for the steps
state := new(multistep.BasicStateBag)
state.Put("config", &b.config)
+ state.Put("access_config", &b.config.AccessConfig)
+ state.Put("ami_config", &b.config.AMIConfig)
state.Put("ec2", ec2conn)
state.Put("awsSession", session)
state.Put("hook", hook)
diff --git a/builder/amazon/ebssurrogate/builder.go b/builder/amazon/ebssurrogate/builder.go
index 464ebfa31..82dd58125 100644
--- a/builder/amazon/ebssurrogate/builder.go
+++ b/builder/amazon/ebssurrogate/builder.go
@@ -114,6 +114,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Setup the state bag and initial state for the steps
state := new(multistep.BasicStateBag)
state.Put("config", &b.config)
+ state.Put("access_config", &b.config.AccessConfig)
+ state.Put("ami_config", &b.config.AMIConfig)
state.Put("ec2", ec2conn)
state.Put("awsSession", session)
state.Put("hook", hook)
diff --git a/builder/amazon/ebsvolume/builder.go b/builder/amazon/ebsvolume/builder.go
index 8a75c05ce..95801c80e 100644
--- a/builder/amazon/ebsvolume/builder.go
+++ b/builder/amazon/ebsvolume/builder.go
@@ -103,6 +103,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Setup the state bag and initial state for the steps
state := new(multistep.BasicStateBag)
state.Put("config", &b.config)
+ state.Put("access_config", &b.config.AccessConfig)
state.Put("ec2", ec2conn)
state.Put("hook", hook)
state.Put("ui", ui)
diff --git a/builder/amazon/instance/builder.go b/builder/amazon/instance/builder.go
index 1d9e29b20..5eef95538 100644
--- a/builder/amazon/instance/builder.go
+++ b/builder/amazon/instance/builder.go
@@ -184,6 +184,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
// Setup the state bag and initial state for the steps
state := new(multistep.BasicStateBag)
state.Put("config", &b.config)
+ state.Put("access_config", &b.config.AccessConfig)
+ state.Put("ami_config", &b.config.AMIConfig)
state.Put("ec2", ec2conn)
state.Put("awsSession", session)
state.Put("hook", hook)
diff --git a/builder/digitalocean/artifact.go b/builder/digitalocean/artifact.go
index 54aed4786..d41f1522d 100644
--- a/builder/digitalocean/artifact.go
+++ b/builder/digitalocean/artifact.go
@@ -12,16 +12,16 @@ import (
type Artifact struct {
// The name of the snapshot
- snapshotName string
+ SnapshotName string
// The ID of the image
- snapshotId int
+ SnapshotId int
// The name of the region
- regionNames []string
+ RegionNames []string
// The client for making API calls
- client *godo.Client
+ Client *godo.Client
}
func (*Artifact) BuilderId() string {
@@ -34,11 +34,11 @@ func (*Artifact) Files() []string {
}
func (a *Artifact) Id() string {
- return fmt.Sprintf("%s:%s", strings.Join(a.regionNames[:], ","), strconv.FormatUint(uint64(a.snapshotId), 10))
+ return fmt.Sprintf("%s:%s", strings.Join(a.RegionNames[:], ","), strconv.FormatUint(uint64(a.SnapshotId), 10))
}
func (a *Artifact) String() string {
- return fmt.Sprintf("A snapshot was created: '%v' (ID: %v) in regions '%v'", a.snapshotName, a.snapshotId, strings.Join(a.regionNames[:], ","))
+ return fmt.Sprintf("A snapshot was created: '%v' (ID: %v) in regions '%v'", a.SnapshotName, a.SnapshotId, strings.Join(a.RegionNames[:], ","))
}
func (a *Artifact) State(name string) interface{} {
@@ -46,7 +46,7 @@ func (a *Artifact) State(name string) interface{} {
}
func (a *Artifact) Destroy() error {
- log.Printf("Destroying image: %d (%s)", a.snapshotId, a.snapshotName)
- _, err := a.client.Images.Delete(context.TODO(), a.snapshotId)
+ log.Printf("Destroying image: %d (%s)", a.SnapshotId, a.SnapshotName)
+ _, err := a.Client.Images.Delete(context.TODO(), a.SnapshotId)
return err
}
diff --git a/builder/digitalocean/builder.go b/builder/digitalocean/builder.go
index 6af14df46..002d008b5 100644
--- a/builder/digitalocean/builder.go
+++ b/builder/digitalocean/builder.go
@@ -113,10 +113,10 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
}
artifact := &Artifact{
- snapshotName: state.Get("snapshot_name").(string),
- snapshotId: state.Get("snapshot_image_id").(int),
- regionNames: state.Get("regions").([]string),
- client: client,
+ SnapshotName: state.Get("snapshot_name").(string),
+ SnapshotId: state.Get("snapshot_image_id").(int),
+ RegionNames: state.Get("regions").([]string),
+ Client: client,
}
return artifact, nil
diff --git a/builder/digitalocean/step_snapshot.go b/builder/digitalocean/step_snapshot.go
index 773626fa1..ed1cd7faf 100644
--- a/builder/digitalocean/step_snapshot.go
+++ b/builder/digitalocean/step_snapshot.go
@@ -91,7 +91,7 @@ func (s *stepSnapshot) Run(_ context.Context, state multistep.StateBag) multiste
return multistep.ActionHalt
}
ui.Say(fmt.Sprintf("transferring Snapshot ID: %d", imageTransfer.ID))
- if err := waitForImageState(godo.ActionCompleted, imageTransfer.ID, action.ID,
+ if err := WaitForImageState(godo.ActionCompleted, imageTransfer.ID, action.ID,
client, 20*time.Minute); err != nil {
// If we get an error the first time, actually report it
err := fmt.Errorf("Error waiting for snapshot transfer: %s", err)
diff --git a/builder/digitalocean/wait.go b/builder/digitalocean/wait.go
index 684955853..dfbe8962a 100644
--- a/builder/digitalocean/wait.go
+++ b/builder/digitalocean/wait.go
@@ -158,9 +158,9 @@ func waitForActionState(
}
}
-// waitForImageState simply blocks until the image action is in
+// WaitForImageState simply blocks until the image action is in
// a state we expect, while eventually timing out.
-func waitForImageState(
+func WaitForImageState(
desiredState string, imageId, actionId int,
client *godo.Client, timeout time.Duration) error {
done := make(chan struct{})
diff --git a/builder/openstack/artifact.go b/builder/openstack/artifact.go
index 4be50ab35..5492a0df3 100644
--- a/builder/openstack/artifact.go
+++ b/builder/openstack/artifact.go
@@ -5,7 +5,7 @@ import (
"log"
"github.com/gophercloud/gophercloud"
- "github.com/gophercloud/gophercloud/openstack/compute/v2/images"
+ "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
)
// Artifact is an artifact implementation that contains built images.
diff --git a/builder/openstack/builder.go b/builder/openstack/builder.go
index bc87c3375..23f2448f1 100644
--- a/builder/openstack/builder.go
+++ b/builder/openstack/builder.go
@@ -71,6 +71,11 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
return nil, fmt.Errorf("Error initializing compute client: %s", err)
}
+ imageClient, err := b.config.imageV2Client()
+ if err != nil {
+ return nil, fmt.Errorf("Error initializing image client: %s", err)
+ }
+
// Setup the state bag and initial state for the steps
state := new(multistep.BasicStateBag)
state.Put("config", &b.config)
@@ -165,7 +170,7 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
artifact := &Artifact{
ImageId: state.Get("image").(string),
BuilderIdValue: BuilderId,
- Client: computeClient,
+ Client: imageClient,
}
return artifact, nil
diff --git a/builder/openstack/step_create_image.go b/builder/openstack/step_create_image.go
index a4bed481a..608a4a818 100644
--- a/builder/openstack/step_create_image.go
+++ b/builder/openstack/step_create_image.go
@@ -9,8 +9,8 @@ import (
"github.com/gophercloud/gophercloud/openstack/blockstorage/extensions/volumeactions"
"github.com/gophercloud/gophercloud"
- "github.com/gophercloud/gophercloud/openstack/compute/v2/images"
"github.com/gophercloud/gophercloud/openstack/compute/v2/servers"
+ "github.com/gophercloud/gophercloud/openstack/imageservice/v2/images"
"github.com/hashicorp/packer/helper/multistep"
"github.com/hashicorp/packer/packer"
)
@@ -25,13 +25,21 @@ func (s *stepCreateImage) Run(_ context.Context, state multistep.StateBag) multi
ui := state.Get("ui").(packer.Ui)
// We need the v2 compute client
- client, err := config.computeV2Client()
+ computeClient, err := config.computeV2Client()
if err != nil {
err = fmt.Errorf("Error initializing compute client: %s", err)
state.Put("error", err)
return multistep.ActionHalt
}
+ // We need the v2 image client
+ imageClient, err := config.imageV2Client()
+ if err != nil {
+ err = fmt.Errorf("Error initializing image service client: %s", err)
+ state.Put("error", err)
+ return multistep.ActionHalt
+ }
+
// Create the image.
// Image source depends on the type of the Compute instance. It can be
// Block Storage service volume or regular Compute service local volume.
@@ -58,7 +66,7 @@ func (s *stepCreateImage) Run(_ context.Context, state multistep.StateBag) multi
}
imageId = image.ImageID
} else {
- imageId, err = servers.CreateImage(client, server.ID, servers.CreateImageOpts{
+ imageId, err = servers.CreateImage(computeClient, server.ID, servers.CreateImageOpts{
Name: config.ImageName,
Metadata: config.ImageMetadata,
}).ExtractImageID()
@@ -76,7 +84,7 @@ func (s *stepCreateImage) Run(_ context.Context, state multistep.StateBag) multi
// Wait for the image to become ready
ui.Say(fmt.Sprintf("Waiting for image %s (image id: %s) to become ready...", config.ImageName, imageId))
- if err := WaitForImage(client, imageId); err != nil {
+ if err := WaitForImage(imageClient, imageId); err != nil {
err := fmt.Errorf("Error waiting for image: %s", err)
state.Put("error", err)
ui.Error(err.Error())
@@ -113,11 +121,11 @@ func WaitForImage(client *gophercloud.ServiceClient, imageId string) error {
return err
}
- if image.Status == "ACTIVE" {
+ if image.Status == "active" {
return nil
}
- log.Printf("Waiting for image creation status: %s (%d%%)", image.Status, image.Progress)
+ log.Printf("Waiting for image creation status: %s", image.Status)
time.Sleep(2 * time.Second)
}
}
diff --git a/builder/vagrant/artifact.go b/builder/vagrant/artifact.go
new file mode 100644
index 000000000..8a3c053b1
--- /dev/null
+++ b/builder/vagrant/artifact.go
@@ -0,0 +1,50 @@
+package vagrant
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "github.com/hashicorp/packer/packer"
+)
+
+// This is the common builder ID to all of these artifacts.
+const BuilderId = "vagrant"
+
+// Artifact is the result of running the vagrant builder, namely a set
+// of files associated with the resulting machine.
+type artifact struct {
+ OutputDir string
+ BoxName string
+}
+
+// NewArtifact returns a vagrant artifact containing the .box file
+func NewArtifact(dir string) (packer.Artifact, error) {
+ return &artifact{
+ OutputDir: dir,
+ BoxName: "package.box",
+ }, nil
+}
+
+func (*artifact) BuilderId() string {
+ return BuilderId
+}
+
+func (a *artifact) Files() []string {
+ return []string{a.BoxName}
+}
+
+func (a *artifact) Id() string {
+ return filepath.Join(a.OutputDir, a.BoxName)
+}
+
+func (a *artifact) String() string {
+ return fmt.Sprintf("Vagrant box is %s", a.Id())
+}
+
+func (a *artifact) State(name string) interface{} {
+ return nil
+}
+
+func (a *artifact) Destroy() error {
+ return nil
+}
diff --git a/builder/vagrant/artifact_test.go b/builder/vagrant/artifact_test.go
new file mode 100644
index 000000000..63e1db1ec
--- /dev/null
+++ b/builder/vagrant/artifact_test.go
@@ -0,0 +1,47 @@
+package vagrant
+
+import (
+ "runtime"
+ "strings"
+ "testing"
+
+ "github.com/hashicorp/packer/packer"
+)
+
+func TestArtifact_Impl(t *testing.T) {
+ var raw interface{} = &artifact{}
+
+ if _, ok := raw.(packer.Artifact); !ok {
+ t.Fatalf("Artifact does not implement packer.Artifact")
+ }
+}
+
+func TestArtifactId(t *testing.T) {
+ a := &artifact{
+ OutputDir: "/my/dir",
+ BoxName: "package.box",
+ }
+
+ expected := "/my/dir/package.box"
+ if runtime.GOOS == "windows" {
+ expected = strings.Replace(expected, "/", "\\", -1)
+ }
+ if strings.Compare(a.Id(), expected) != 0 {
+ t.Fatalf("artifact ID should match: expected: %s received: %s", expected, a.Id())
+ }
+}
+
+func TestArtifactString(t *testing.T) {
+ a := &artifact{
+ OutputDir: "/my/dir",
+ BoxName: "package.box",
+ }
+ expected := "Vagrant box is /my/dir/package.box"
+ if runtime.GOOS == "windows" {
+ expected = strings.Replace(expected, "/", "\\", -1)
+ }
+
+ if strings.Compare(a.String(), expected) != 0 {
+ t.Fatalf("artifact string should match: expected: %s received: %s", expected, a.String())
+ }
+}
diff --git a/builder/vagrant/builder.go b/builder/vagrant/builder.go
new file mode 100644
index 000000000..a0cd3202a
--- /dev/null
+++ b/builder/vagrant/builder.go
@@ -0,0 +1,276 @@
+package vagrant
+
+import (
+ "errors"
+ "fmt"
+ "log"
+ "path/filepath"
+ "strings"
+ "time"
+
+ "github.com/hashicorp/packer/common"
+ "github.com/hashicorp/packer/common/bootcommand"
+ "github.com/hashicorp/packer/helper/communicator"
+ "github.com/hashicorp/packer/helper/config"
+ "github.com/hashicorp/packer/helper/multistep"
+ "github.com/hashicorp/packer/packer"
+ "github.com/hashicorp/packer/template/interpolate"
+)
+
+// Builder implements packer.Builder and builds the actual VirtualBox
+// images.
+type Builder struct {
+ config *Config
+ runner multistep.Runner
+}
+
+type SSHConfig struct {
+ Comm communicator.Config `mapstructure:",squash"`
+}
+
+type Config struct {
+ common.PackerConfig `mapstructure:",squash"`
+ common.HTTPConfig `mapstructure:",squash"`
+ common.ISOConfig `mapstructure:",squash"`
+ common.FloppyConfig `mapstructure:",squash"`
+ bootcommand.BootConfig `mapstructure:",squash"`
+ SSHConfig `mapstructure:",squash"`
+
+ // This is the name of the new virtual machine.
+ // By default this is "packer-BUILDNAME", where "BUILDNAME" is the name of the build.
+ OutputDir string `mapstructure:"output_dir"`
+ SourceBox string `mapstructure:"source_path"`
+ GlobalID string `mapstructure:"global_id"`
+ Checksum string `mapstructure:"checksum"`
+ ChecksumType string `mapstructure:"checksum_type"`
+ BoxName string `mapstructure:"box_name"`
+
+ Provider string `mapstructure:"provider"`
+
+ Communicator string `mapstructure:"communicator"`
+
+ // What vagrantfile to use
+ VagrantfileTpl string `mapstructure:"vagrantfile_template"`
+
+ // Whether to Halt, Suspend, or Destroy the box
+ TeardownMethod string `mapstructure:"teardown_method"`
+
+ // Options for the "vagrant init" command
+ BoxVersion string `mapstructure:"box_version"`
+ Template string `mapstructure:"template"`
+ SyncedFolder string `mapstructure:"synced_folder"`
+
+ // Options for the "vagrant box add" command
+ SkipAdd bool `mapstructure:"skip_add"`
+ AddCACert string `mapstructure:"add_cacert"`
+ AddCAPath string `mapstructure:"add_capath"`
+ AddCert string `mapstructure:"add_cert"`
+ AddClean bool `mapstructure:"add_clean"`
+ AddForce bool `mapstructure:"add_force"`
+ AddInsecure bool `mapstructure:"add_insecure"`
+
+ // Don't package the Vagrant box after build.
+ SkipPackage bool `mapstructure:"skip_package"`
+ OutputVagrantfile string `mapstructure:"output_vagrantfile"`
+ PackageInclude []string `mapstructure:"package_include"`
+
+ ctx interpolate.Context
+}
+
+// Prepare processes the build configuration parameters.
+func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
+ b.config = new(Config)
+ err := config.Decode(&b.config, &config.DecodeOpts{
+ Interpolate: true,
+ InterpolateContext: &b.config.ctx,
+ InterpolateFilter: &interpolate.RenderFilter{
+ Exclude: []string{
+ "boot_command",
+ },
+ },
+ }, raws...)
+ if err != nil {
+ return nil, err
+ }
+
+ // Accumulate any errors and warnings
+ var errs *packer.MultiError
+ warnings := make([]string, 0)
+
+ if b.config.OutputDir == "" {
+ b.config.OutputDir = fmt.Sprintf("output-%s", b.config.PackerBuildName)
+ }
+
+ if b.config.Comm.SSHTimeout == 0 {
+ b.config.Comm.SSHTimeout = 10 * time.Minute
+ }
+
+ if b.config.Comm.Type != "ssh" {
+ errs = packer.MultiErrorAppend(errs,
+ fmt.Errorf(`The Vagrant builder currently only supports the ssh communicator"`))
+ }
+ // The box isn't a namespace like you'd pull from vagrant cloud
+ if b.config.BoxName == "" {
+ b.config.BoxName = fmt.Sprintf("packer_%s", b.config.PackerBuildName)
+ }
+
+ if b.config.SourceBox == "" {
+ if b.config.GlobalID == "" {
+ errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is required unless you have set global_id"))
+ }
+ } else {
+ if b.config.GlobalID != "" {
+ errs = packer.MultiErrorAppend(errs, fmt.Errorf("You may either set global_id or source_path but not both"))
+ }
+ if strings.HasSuffix(b.config.SourceBox, ".box") {
+ b.config.SourceBox, err = common.ValidatedURL(b.config.SourceBox)
+ if err != nil {
+ errs = packer.MultiErrorAppend(errs, fmt.Errorf("source_path is invalid: %s", err))
+ }
+ fileOK := common.FileExistsLocally(b.config.SourceBox)
+ if !fileOK {
+ errs = packer.MultiErrorAppend(errs,
+ fmt.Errorf("Source file '%s' needs to exist at time of config validation!", b.config.SourceBox))
+ }
+ }
+ }
+
+ if b.config.TeardownMethod == "" {
+ // If we're using a box that's already opened on the system, don't
+ // automatically destroy it. If we open the box ourselves, then go ahead
+ // and kill it by default.
+ if b.config.GlobalID != "" {
+ b.config.TeardownMethod = "halt"
+ } else {
+ b.config.TeardownMethod = "destroy"
+ }
+ } else {
+ matches := false
+ for _, name := range []string{"halt", "suspend", "destroy"} {
+ if strings.ToLower(b.config.TeardownMethod) == name {
+ matches = true
+ }
+ }
+ if !matches {
+ errs = packer.MultiErrorAppend(errs,
+ fmt.Errorf(`TeardownMethod must be "halt", "suspend", or "destroy"`))
+ }
+ }
+
+ if errs != nil && len(errs.Errors) > 0 {
+ return warnings, errs
+ }
+
+ return warnings, nil
+}
+
+// Run executes a Packer build and returns a packer.Artifact representing
+// a VirtualBox appliance.
+func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packer.Artifact, error) {
+ // Create the driver that we'll use to communicate with VirtualBox
+ VagrantCWD, err := filepath.Abs(b.config.OutputDir)
+ if err != nil {
+ return nil, err
+ }
+ driver, err := NewDriver(VagrantCWD)
+ if err != nil {
+ return nil, fmt.Errorf("Failed creating VirtualBox driver: %s", err)
+ }
+
+ // Set up the state.
+ state := new(multistep.BasicStateBag)
+ state.Put("config", b.config)
+ state.Put("debug", b.config.PackerDebug)
+ state.Put("driver", driver)
+ state.Put("cache", cache)
+ state.Put("hook", hook)
+ state.Put("ui", ui)
+
+ // Build the steps.
+ steps := []multistep.Step{}
+ // Download if source box isn't from vagrant cloud.
+ if strings.HasSuffix(b.config.SourceBox, ".box") {
+ steps = append(steps, &common.StepDownload{
+ Checksum: b.config.Checksum,
+ ChecksumType: b.config.ChecksumType,
+ Description: "Box",
+ Extension: "box",
+ ResultKey: "box_path",
+ Url: []string{b.config.SourceBox},
+ })
+ }
+ steps = append(steps,
+ &common.StepOutputDir{
+ Force: b.config.PackerForce,
+ Path: b.config.OutputDir,
+ },
+ &StepCreateVagrantfile{
+ Template: b.config.Template,
+ SyncedFolder: b.config.SyncedFolder,
+ SourceBox: b.config.SourceBox,
+ OutputDir: b.config.OutputDir,
+ GlobalID: b.config.GlobalID,
+ },
+ &StepAddBox{
+ BoxVersion: b.config.BoxVersion,
+ CACert: b.config.AddCACert,
+ CAPath: b.config.AddCAPath,
+ DownloadCert: b.config.AddCert,
+ Clean: b.config.AddClean,
+ Force: b.config.AddForce,
+ Insecure: b.config.AddInsecure,
+ Provider: b.config.Provider,
+ SourceBox: b.config.SourceBox,
+ BoxName: b.config.BoxName,
+ GlobalID: b.config.GlobalID,
+ SkipAdd: b.config.SkipAdd,
+ },
+ &StepUp{
+ TeardownMethod: b.config.TeardownMethod,
+ Provider: b.config.Provider,
+ GlobalID: b.config.GlobalID,
+ },
+ &StepSSHConfig{
+ b.config.GlobalID,
+ },
+ &communicator.StepConnect{
+ Config: &b.config.SSHConfig.Comm,
+ Host: CommHost(),
+ SSHConfig: b.config.SSHConfig.Comm.SSHConfigFunc(),
+ },
+ new(common.StepProvision),
+ &StepPackage{
+ SkipPackage: b.config.SkipPackage,
+ Include: b.config.PackageInclude,
+ Vagrantfile: b.config.OutputVagrantfile,
+ GlobalID: b.config.GlobalID,
+ })
+
+ // Run the steps.
+ b.runner = common.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
+ b.runner.Run(state)
+
+ // Report any errors.
+ if rawErr, ok := state.GetOk("error"); ok {
+ return nil, rawErr.(error)
+ }
+
+ // If we were interrupted or cancelled, then just exit.
+ if _, ok := state.GetOk(multistep.StateCancelled); ok {
+ return nil, errors.New("Build was cancelled.")
+ }
+
+ if _, ok := state.GetOk(multistep.StateHalted); ok {
+ return nil, errors.New("Build was halted.")
+ }
+
+ return NewArtifact(b.config.OutputDir)
+}
+
+// Cancel.
+func (b *Builder) Cancel() {
+ if b.runner != nil {
+ log.Println("Cancelling the step runner...")
+ b.runner.Cancel()
+ }
+}
diff --git a/builder/vagrant/builder_test.go b/builder/vagrant/builder_test.go
new file mode 100644
index 000000000..65c2e5e9f
--- /dev/null
+++ b/builder/vagrant/builder_test.go
@@ -0,0 +1,91 @@
+package vagrant
+
+import (
+ "testing"
+
+ "github.com/hashicorp/packer/packer"
+)
+
+func TestBuilder_ImplementsBuilder(t *testing.T) {
+ var raw interface{}
+ raw = &Builder{}
+ if _, ok := raw.(packer.Builder); !ok {
+ t.Fatalf("Builder should be a builder")
+ }
+}
+
+func TestBuilder_Prepare_ValidateSource(t *testing.T) {
+ b := &Builder{}
+ type testCase struct {
+ config map[string]interface{}
+ errExpected bool
+ reason string
+ }
+
+ cases := []testCase{
+ {
+ config: map[string]interface{}{
+ "global_id": "a3559ec",
+ },
+ errExpected: true,
+ reason: "Need to set SSH communicator.",
+ },
+ {
+ config: map[string]interface{}{
+ "global_id": "a3559ec",
+ "communicator": "ssh",
+ },
+ errExpected: false,
+ reason: "Shouldn't fail because we've set global_id",
+ },
+ {
+ config: map[string]interface{}{
+ "communicator": "ssh",
+ },
+ errExpected: true,
+ reason: "Should fail because we must set source_path or global_id",
+ },
+ {
+ config: map[string]interface{}{
+ "source_path": "./mybox",
+ "communicator": "ssh",
+ },
+ errExpected: false,
+ reason: "Source path is set; we should be fine",
+ },
+ {
+ config: map[string]interface{}{
+ "source_path": "./mybox",
+ "communicator": "ssh",
+ "global_id": "a3559ec",
+ },
+ errExpected: true,
+ reason: "Both source path and global are set: we should error.",
+ },
+ {
+ config: map[string]interface{}{
+ "communicator": "ssh",
+ "global_id": "a3559ec",
+ "teardown_method": "suspend",
+ },
+ errExpected: false,
+ reason: "Valid argument for teardown method",
+ },
+ {
+ config: map[string]interface{}{
+ "communicator": "ssh",
+ "global_id": "a3559ec",
+ "teardown_method": "surspernd",
+ },
+ errExpected: true,
+ reason: "Inalid argument for teardown method",
+ },
+ }
+
+ for _, tc := range cases {
+ _, err := b.Prepare(tc.config)
+ if (err != nil) != tc.errExpected {
+ t.Fatalf("Unexpected behavior from test case %#v; %s.", tc.config, tc.reason)
+ }
+ }
+}
diff --git a/builder/vagrant/driver.go b/builder/vagrant/driver.go
new file mode 100644
index 000000000..56f7e8f68
--- /dev/null
+++ b/builder/vagrant/driver.go
@@ -0,0 +1,67 @@
+package vagrant
+
+import (
+ "fmt"
+ "os/exec"
+ "runtime"
+)
+
+// A driver is able to talk to Vagrant and perform certain
+// operations with it.
+
+type VagrantDriver interface {
+ // Calls "vagrant init"
+ Init([]string) error
+
+ // Calls "vagrant add"
+ Add([]string) error
+
+ // Calls "vagrant up"
+ Up([]string) (string, string, error)
+
+ // Calls "vagrant halt"
+ Halt(string) error
+
+ // Calls "vagrant suspend"
+ Suspend(string) error
+
+ SSHConfig(string) (*VagrantSSHConfig, error)
+
+ // Calls "vagrant destroy"
+ Destroy(string) error
+
+ // Calls "vagrant package"[
+ Package([]string) error
+
+ // Verify checks to make sure that this driver should function
+ // properly. If there is any indication the driver can't function,
+ // this will return an error.
+ Verify() error
+
+ // Version reads the version of VirtualBox that is installed.
+ Version() (string, error)
+}
+
+func NewDriver(outputDir string) (VagrantDriver, error) {
+ // Hardcode path for now while I'm developing. Obviously this path needs
+ // to be discovered based on OS.
+ vagrantBinary := "vagrant"
+ if runtime.GOOS == "windows" {
+ vagrantBinary = "vagrant.exe"
+ }
+
+ if _, err := exec.LookPath(vagrantBinary); err != nil {
+ return nil, fmt.Errorf("Error: Packer cannot find Vagrant in the path: %s", err.Error())
+ }
+
+ driver := &Vagrant_2_2_Driver{
+ vagrantBinary: vagrantBinary,
+ VagrantCWD: outputDir,
+ }
+
+ if err := driver.Verify(); err != nil {
+ return nil, err
+ }
+
+ return driver, nil
+}
diff --git a/builder/vagrant/driver_2_2.go b/builder/vagrant/driver_2_2.go
new file mode 100644
index 000000000..01652f5c3
--- /dev/null
+++ b/builder/vagrant/driver_2_2.go
@@ -0,0 +1,199 @@
+package vagrant
+
+import (
+ "bytes"
+ "fmt"
+ "log"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "regexp"
+ "strings"
+
+ "github.com/hashicorp/go-version"
+)
+
+const VAGRANT_MIN_VERSION = ">= 2.0.2"
+
+type Vagrant_2_2_Driver struct {
+ vagrantBinary string
+ VagrantCWD string
+}
+
+// Calls "vagrant init"
+func (d *Vagrant_2_2_Driver) Init(args []string) error {
+ _, _, err := d.vagrantCmd(append([]string{"init"}, args...)...)
+ return err
+}
+
+// Calls "vagrant add"
+func (d *Vagrant_2_2_Driver) Add(args []string) error {
+ // vagrant box add partyvm ubuntu-14.04.vmware.box
+ _, _, err := d.vagrantCmd(append([]string{"box", "add"}, args...)...)
+ return err
+}
+
+// Calls "vagrant up"
+func (d *Vagrant_2_2_Driver) Up(args []string) (string, string, error) {
+ stdout, stderr, err := d.vagrantCmd(append([]string{"up"}, args...)...)
+ return stdout, stderr, err
+}
+
+// Calls "vagrant halt"
+func (d *Vagrant_2_2_Driver) Halt(id string) error {
+ args := []string{"halt"}
+ if id != "" {
+ args = append(args, id)
+ }
+ _, _, err := d.vagrantCmd(args...)
+ return err
+}
+
+// Calls "vagrant suspend"
+func (d *Vagrant_2_2_Driver) Suspend(id string) error {
+ args := []string{"suspend"}
+ if id != "" {
+ args = append(args, id)
+ }
+ _, _, err := d.vagrantCmd(args...)
+ return err
+}
+
+// Calls "vagrant destroy"
+func (d *Vagrant_2_2_Driver) Destroy(id string) error {
+ args := []string{"destroy", "-f"}
+ if id != "" {
+ args = append(args, id)
+ }
+ _, _, err := d.vagrantCmd(args...)
+ return err
+}
+
+// Calls "vagrant package"
+func (d *Vagrant_2_2_Driver) Package(args []string) error {
+ args = append(args, "--output", filepath.Join(d.VagrantCWD, "package.box"))
+ _, _, err := d.vagrantCmd(append([]string{"package"}, args...)...)
+ return err
+}
+
+// Verify makes sure that Vagrant exists at the given path
+func (d *Vagrant_2_2_Driver) Verify() error {
+ vagrantPath, err := exec.LookPath(d.vagrantBinary)
+ if err != nil {
+ return fmt.Errorf("Can't find Vagrant binary!")
+ }
+ _, err = os.Stat(vagrantPath)
+ if err != nil {
+ return fmt.Errorf("Can't find Vagrant binary.")
+ }
+
+ constraints, err := version.NewConstraint(VAGRANT_MIN_VERSION)
+ vers, err := d.Version()
+ v, err := version.NewVersion(vers)
+ if err != nil {
+ return fmt.Errorf("Error figuring out Vagrant version.")
+ }
+
+ if !constraints.Check(v) {
+ return fmt.Errorf("installed Vagrant version must be >=2.0.2")
+ }
+
+ return nil
+}
+
+type VagrantSSHConfig struct {
+ Hostname string
+ User string
+ Port string
+ UserKnownHostsFile string
+ StrictHostKeyChecking bool
+ PasswordAuthentication bool
+ IdentityFile string
+ IdentitiesOnly bool
+ LogLevel string
+}
+
+func parseSSHConfig(lines []string, value string) string {
+ out := ""
+ for _, line := range lines {
+ if index := strings.Index(line, value); index != -1 {
+ out = line[index+len(value):]
+ }
+ }
+ return out
+}
+
+func yesno(yn string) bool {
+ if yn == "no" {
+ return false
+ }
+ return true
+}
+
+func (d *Vagrant_2_2_Driver) SSHConfig(id string) (*VagrantSSHConfig, error) {
+ // vagrant ssh-config --host 8df7860
+ args := []string{"ssh-config"}
+ if id != "" {
+ args = append(args, id)
+ }
+ stdout, _, err := d.vagrantCmd(args...)
+ sshConf := &VagrantSSHConfig{}
+
+ lines := strings.Split(stdout, "\n")
+ sshConf.Hostname = parseSSHConfig(lines, "HostName ")
+ sshConf.User = parseSSHConfig(lines, "User ")
+ sshConf.Port = parseSSHConfig(lines, "Port ")
+ sshConf.UserKnownHostsFile = parseSSHConfig(lines, "UserKnownHostsFile ")
+ sshConf.IdentityFile = parseSSHConfig(lines, "IdentityFile ")
+ sshConf.LogLevel = parseSSHConfig(lines, "LogLevel ")
+
+ // handle the booleans
+ sshConf.StrictHostKeyChecking = yesno(parseSSHConfig(lines, "StrictHostKeyChecking "))
+ sshConf.PasswordAuthentication = yesno(parseSSHConfig(lines, "PasswordAuthentication "))
+ sshConf.IdentitiesOnly = yesno((parseSSHConfig(lines, "IdentitiesOnly ")))
+
+ return sshConf, err
+}
+
+// Version reads the version of VirtualBox that is installed.
+func (d *Vagrant_2_2_Driver) Version() (string, error) {
+ stdoutString, _, err := d.vagrantCmd([]string{"--version"}...)
+ // Example stdout:
+
+ // Installed Version: 2.2.3
+ //
+ // Vagrant was unable to check for the latest version of Vagrant.
+ // Please check manually at https://www.vagrantup.com
+
+ // Use regex to find version
+ reg := regexp.MustCompile(`(\d+\.)?(\d+\.)?(\*|\d+)`)
+ version := reg.FindString(stdoutString)
+ if version == "" {
+ return "", err
+ }
+
+ return version, nil
+}
+
+func (d *Vagrant_2_2_Driver) vagrantCmd(args ...string) (string, string, error) {
+ var stdout, stderr bytes.Buffer
+
+ log.Printf("Calling Vagrant CLI: %#v", args)
+ cmd := exec.Command(d.vagrantBinary, args...)
+ cmd.Env = append(os.Environ(), fmt.Sprintf("VAGRANT_CWD=%s", d.VagrantCWD))
+ cmd.Stdout = &stdout
+ cmd.Stderr = &stderr
+ err := cmd.Run()
+
+ stdoutString := strings.TrimSpace(stdout.String())
+ stderrString := strings.TrimSpace(stderr.String())
+
+ if _, ok := err.(*exec.ExitError); ok {
+ err = fmt.Errorf("Vagrant error: %s", stderrString)
+ }
+
+ log.Printf("[vagrant driver] stdout: %s", stdoutString)
+ log.Printf("[vagrant driver] stderr: %s", stderrString)
+
+ return stdoutString, stderrString, err
+}
diff --git a/builder/vagrant/ssh.go b/builder/vagrant/ssh.go
new file mode 100644
index 000000000..d4a56cb17
--- /dev/null
+++ b/builder/vagrant/ssh.go
@@ -0,0 +1,19 @@
+package vagrant
+
+import (
+ "github.com/hashicorp/packer/helper/multistep"
+)
+
+func CommHost() func(multistep.StateBag) (string, error) {
+ return func(state multistep.StateBag) (string, error) {
+ config := state.Get("config").(*Config)
+ return config.Comm.SSHHost, nil
+ }
+}
+
+func SSHPort() func(multistep.StateBag) (int, error) {
+ return func(state multistep.StateBag) (int, error) {
+ config := state.Get("config").(*Config)
+ return config.Comm.SSHPort, nil
+ }
+}
diff --git a/builder/vagrant/step_add_box.go b/builder/vagrant/step_add_box.go
new file mode 100644
index 000000000..9c1e23eb6
--- /dev/null
+++ b/builder/vagrant/step_add_box.go
@@ -0,0 +1,100 @@
+package vagrant
+
+import (
+ "context"
+ "log"
+ "strings"
+
+ "github.com/hashicorp/packer/helper/multistep"
+ "github.com/hashicorp/packer/packer"
+)
+
+type StepAddBox struct {
+ BoxVersion string
+ CACert string
+ CAPath string
+ DownloadCert string
+ Clean bool
+ Force bool
+ Insecure bool
+ Provider string
+ SourceBox string
+ BoxName string
+ GlobalID string
+ SkipAdd bool
+}
+
+func (s *StepAddBox) generateAddArgs() []string {
+ addArgs := []string{}
+
+ if strings.HasSuffix(s.SourceBox, ".box") {
+ addArgs = append(addArgs, s.BoxName)
+ }
+
+ addArgs = append(addArgs, s.SourceBox)
+
+ if s.BoxVersion != "" {
+ addArgs = append(addArgs, "--box-version", s.BoxVersion)
+ }
+
+ if s.CACert != "" {
+ addArgs = append(addArgs, "--cacert", s.CACert)
+ }
+
+ if s.CAPath != "" {
+ addArgs = append(addArgs, "--capath", s.CAPath)
+ }
+
+ if s.DownloadCert != "" {
+ addArgs = append(addArgs, "--cert", s.DownloadCert)
+ }
+
+ if s.Clean {
+ addArgs = append(addArgs, "--clean")
+ }
+
+ if s.Force {
+ addArgs = append(addArgs, "--force")
+ }
+
+ if s.Insecure {
+ addArgs = append(addArgs, "--insecure")
+ }
+
+ if s.Provider != "" {
+ addArgs = append(addArgs, "--provider", s.Provider)
+ }
+
+ return addArgs
+}
+
+func (s *StepAddBox) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
+ driver := state.Get("driver").(VagrantDriver)
+ ui := state.Get("ui").(packer.Ui)
+
+ if s.SkipAdd {
+ ui.Say("skip_add was set so we assume the box is already in Vagrant...")
+ return multistep.ActionContinue
+ }
+
+ if s.GlobalID != "" {
+ ui.Say("Using a global-id; skipping Vagrant add command...")
+ return multistep.ActionContinue
+ }
+
+ ui.Say("Adding box using vagrant box add..")
+ addArgs := s.generateAddArgs()
+
+ log.Printf("[vagrant] Calling box add with following args %s", strings.Join(addArgs, " "))
+ // Call vagrant using prepared arguments
+ err := driver.Add(addArgs)
+ if err != nil {
+ state.Put("error", err)
+ return multistep.ActionHalt
+ }
+
+ return multistep.ActionContinue
+}
+
+func (s *StepAddBox) Cleanup(state multistep.StateBag) {
+}
diff --git a/builder/vagrant/step_add_box_test.go b/builder/vagrant/step_add_box_test.go
new file mode 100644
index 000000000..29feca5d1
--- /dev/null
+++ b/builder/vagrant/step_add_box_test.go
@@ -0,0 +1,62 @@
+package vagrant
+
+import (
+ "strings"
+ "testing"
+
+ "github.com/hashicorp/packer/helper/multistep"
+)
+
+func TestStepAdd_Impl(t *testing.T) {
+ var raw interface{}
+ raw = new(StepAddBox)
+ if _, ok := raw.(multistep.Step); !ok {
+ t.Fatalf("initialize should be a step")
+ }
+}
+
+func TestPrepAddArgs(t *testing.T) {
+ type testArgs struct {
+ Step StepAddBox
+ Expected []string
+ }
+ addTests := []testArgs{
+ {
+ Step: StepAddBox{
+ SourceBox: "my_source_box.box",
+ BoxName: "AWESOME BOX",
+ },
+ Expected: []string{"AWESOME BOX", "my_source_box.box"},
+ },
+ {
+ Step: StepAddBox{
+ SourceBox: "my_source_box",
+ BoxName: "AWESOME BOX",
+ },
+ Expected: []string{"my_source_box"},
+ },
+ {
+ Step: StepAddBox{
+ BoxVersion: "eleventyone",
+ CACert: "adfasdf",
+ CAPath: "adfasdf",
+ DownloadCert: "adfasdf",
+ Clean: true,
+ Force: true,
+ Insecure: true,
+ Provider: "virtualbox",
+ SourceBox: "bananabox.box",
+ BoxName: "bananas",
+ },
+ Expected: []string{"bananas", "bananabox.box", "--box-version", "eleventyone", "--cacert", "adfasdf", "--capath", "adfasdf", "--cert", "adfasdf", "--clean", "--force", "--insecure", "--provider", "virtualbox"},
+ },
+ }
+ for _, addTest := range addTests {
+ addArgs := addTest.Step.generateAddArgs()
+ for i, val := range addTest.Expected {
+ if strings.Compare(addArgs[i], val) != 0 {
+ t.Fatalf("expected %#v but received %#v", addTest.Expected, addArgs)
+ }
+ }
+ }
+}
diff --git a/builder/vagrant/step_create_vagrantfile.go b/builder/vagrant/step_create_vagrantfile.go
new file mode 100644
index 000000000..40ead21dd
--- /dev/null
+++ b/builder/vagrant/step_create_vagrantfile.go
@@ -0,0 +1,96 @@
+package vagrant
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "os"
+ "path/filepath"
+ "text/template"
+
+ "github.com/hashicorp/packer/helper/multistep"
+ "github.com/hashicorp/packer/packer"
+)
+
+type StepCreateVagrantfile struct {
+ Template string
+ SourceBox string
+ OutputDir string
+ SyncedFolder string
+ GlobalID string
+}
+
+var DEFAULT_TEMPLATE = `Vagrant.configure("2") do |config|
+ config.vm.box = "{{.BoxName}}"
+ {{ if ne .SyncedFolder "" -}}
+ config.vm.synced_folder "{{.SyncedFolder}}", "/vagrant"
+ {{- else -}}
+ config.vm.synced_folder ".", "/vagrant", disabled: true
+ {{- end}}
+end`
+
+type VagrantfileOptions struct {
+ SyncedFolder string
+ BoxName string
+}
+
+func (s *StepCreateVagrantfile) createVagrantfile() (string, error) {
+ tplPath := filepath.Join(s.OutputDir, "Vagrantfile")
+ templateFile, err := os.Create(tplPath)
+ if err != nil {
+ retErr := fmt.Errorf("Error creating vagrantfile %s", err.Error())
+ return "", retErr
+ }
+
+ var tpl *template.Template
+ if s.Template == "" {
+ // Generate vagrantfile template based on our default
+ tpl = template.Must(template.New("VagrantTpl").Parse(DEFAULT_TEMPLATE))
+ } else {
+ // Read in the template from provided file.
+ tpl, err = template.ParseFiles(s.Template)
+ if err != nil {
+ return "", err
+ }
+ }
+
+ opts := &VagrantfileOptions{
+ SyncedFolder: s.SyncedFolder,
+ BoxName: s.SourceBox,
+ }
+
+ err = tpl.Execute(templateFile, opts)
+ if err != nil {
+ return "", err
+ }
+
+ abspath, err := filepath.Abs(tplPath)
+ if err != nil {
+ return "", err
+ }
+
+ return abspath, nil
+}
+
+func (s *StepCreateVagrantfile) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
+ ui := state.Get("ui").(packer.Ui)
+
+ // Skip the initialize step if we're trying to launch from a global ID.
+ if s.GlobalID != "" {
+ ui.Say("Using a global-id; skipping Vagrant init in this directory...")
+ return multistep.ActionContinue
+ }
+
+ ui.Say("Creating a Vagrantfile in the build directory...")
+ vagrantfilePath, err := s.createVagrantfile()
+ if err != nil {
+ state.Put("error", err)
+ return multistep.ActionHalt
+ }
+ log.Printf("Created vagrantfile at %s", vagrantfilePath)
+
+ return multistep.ActionContinue
+}
+
+func (s *StepCreateVagrantfile) Cleanup(state multistep.StateBag) {
+}
diff --git a/builder/vagrant/step_create_vagrantfile_test.go b/builder/vagrant/step_create_vagrantfile_test.go
new file mode 100644
index 000000000..d3bcf21b9
--- /dev/null
+++ b/builder/vagrant/step_create_vagrantfile_test.go
@@ -0,0 +1,60 @@
+package vagrant
+
+import (
+ "io/ioutil"
+ "os"
+ "strings"
+ "testing"
+
+ "github.com/hashicorp/packer/helper/multistep"
+)
+
+func TestStepCreateVagrantfile_Impl(t *testing.T) {
+ var raw interface{}
+ raw = new(StepCreateVagrantfile)
+ if _, ok := raw.(multistep.Step); !ok {
+ t.Fatalf("initialize should be a step")
+ }
+}
+
+func TestCreateFile(t *testing.T) {
+ testy := StepCreateVagrantfile{
+ OutputDir: "./",
+ SourceBox: "bananas",
+ }
+ templatePath, err := testy.createVagrantfile()
+ if err != nil {
+ t.Fatalf(err.Error())
+ }
+ defer os.Remove(templatePath)
+ contents, err := ioutil.ReadFile(templatePath)
+ actual := string(contents)
+ expected := `Vagrant.configure("2") do |config|
+ config.vm.box = "bananas"
+ config.vm.synced_folder ".", "/vagrant", disabled: true
+end`
+ if ok := strings.Compare(actual, expected); ok != 0 {
+ t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, actual)
+ }
+}
+
+func TestCreateFile_customSync(t *testing.T) {
+ testy := StepCreateVagrantfile{
+ OutputDir: "./",
+ SyncedFolder: "myfolder/foldertimes",
+ }
+ templatePath, err := testy.createVagrantfile()
+ if err != nil {
+ t.Fatalf(err.Error())
+ }
+ defer os.Remove(templatePath)
+ contents, err := ioutil.ReadFile(templatePath)
+ actual := string(contents)
+ expected := `Vagrant.configure("2") do |config|
+ config.vm.box = ""
+ config.vm.synced_folder "myfolder/foldertimes", "/vagrant"
+end`
+ if ok := strings.Compare(actual, expected); ok != 0 {
+ t.Fatalf("EXPECTED: \n%s\n\n RECEIVED: \n%s\n\n", expected, actual)
+ }
+}
diff --git a/builder/vagrant/step_package.go b/builder/vagrant/step_package.go
new file mode 100644
index 000000000..8200296b4
--- /dev/null
+++ b/builder/vagrant/step_package.go
@@ -0,0 +1,49 @@
+package vagrant
+
+import (
+ "context"
+ "strings"
+
+ "github.com/hashicorp/packer/helper/multistep"
+ "github.com/hashicorp/packer/packer"
+)
+
+type StepPackage struct {
+ SkipPackage bool
+ Include []string
+ Vagrantfile string
+ GlobalID string
+}
+
+func (s *StepPackage) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
+ driver := state.Get("driver").(VagrantDriver)
+ ui := state.Get("ui").(packer.Ui)
+
+ if s.SkipPackage {
+ ui.Say("skip_package flag set; not going to call Vagrant package on this box.")
+ return multistep.ActionContinue
+ }
+ ui.Say("Packaging box...")
+ packageArgs := []string{}
+ if s.GlobalID != "" {
+ packageArgs = append(packageArgs, s.GlobalID)
+ }
+
+ if len(s.Include) > 0 {
+ packageArgs = append(packageArgs, "--include", strings.Join(s.Include, ","))
+ }
+ if s.Vagrantfile != "" {
+ packageArgs = append(packageArgs, "--vagrantfile", s.Vagrantfile)
+ }
+
+ err := driver.Package(packageArgs)
+ if err != nil {
+ state.Put("error", err)
+ return multistep.ActionHalt
+ }
+
+ return multistep.ActionContinue
+}
+
+func (s *StepPackage) Cleanup(state multistep.StateBag) {
+}
diff --git a/builder/vagrant/step_ssh_config.go b/builder/vagrant/step_ssh_config.go
new file mode 100644
index 000000000..68fc13aa3
--- /dev/null
+++ b/builder/vagrant/step_ssh_config.go
@@ -0,0 +1,53 @@
+package vagrant
+
+import (
+ "context"
+ "strconv"
+
+ "github.com/hashicorp/packer/helper/multistep"
+)
+
+// Vagrant already sets up ssh on the guests; our job is to find out what
+// it did. We can do that with the ssh-config command. Example output:
+
+// $ vagrant ssh-config
+// Host default
+// HostName 172.16.41.194
+// User vagrant
+// Port 22
+// UserKnownHostsFile /dev/null
+// StrictHostKeyChecking no
+// PasswordAuthentication no
+// IdentityFile /Users/mmarsh/Projects/vagrant-boxes/ubuntu/.vagrant/machines/default/vmware_fusion/private_key
+// IdentitiesOnly yes
+// LogLevel FATAL
+
+type StepSSHConfig struct {
+ GlobalID string
+}
+
+func (s *StepSSHConfig) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
+ driver := state.Get("driver").(VagrantDriver)
+ config := state.Get("config").(*Config)
+
+ sshConfig, err := driver.SSHConfig(s.GlobalID)
+ if err != nil {
+ state.Put("error", err)
+ return multistep.ActionHalt
+ }
+
+ config.Comm.SSHPrivateKeyFile = sshConfig.IdentityFile
+ config.Comm.SSHUsername = sshConfig.User
+ config.Comm.SSHHost = sshConfig.Hostname
+ port, err := strconv.Atoi(sshConfig.Port)
+ if err != nil {
+ state.Put("error", err)
+ return multistep.ActionHalt
+ }
+ config.Comm.SSHPort = port
+
+ return multistep.ActionContinue
+}
+
+func (s *StepSSHConfig) Cleanup(state multistep.StateBag) {
+}
diff --git a/builder/vagrant/step_up.go b/builder/vagrant/step_up.go
new file mode 100644
index 000000000..6f2584c5f
--- /dev/null
+++ b/builder/vagrant/step_up.go
@@ -0,0 +1,62 @@
+package vagrant
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/hashicorp/packer/helper/multistep"
+ "github.com/hashicorp/packer/packer"
+)
+
+type StepUp struct {
+ TeardownMethod string
+ Provider string
+ GlobalID string
+}
+
+func (s *StepUp) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
+ driver := state.Get("driver").(VagrantDriver)
+ ui := state.Get("ui").(packer.Ui)
+
+ ui.Say("Calling Vagrant Up...")
+
+ var args []string
+ if s.GlobalID != "" {
+ args = append(args, s.GlobalID)
+ }
+
+ if s.Provider != "" {
+ args = append(args, fmt.Sprintf("--provider=%s", s.Provider))
+ }
+
+ _, _, err := driver.Up(args)
+
+ if err != nil {
+ state.Put("error", err)
+ return multistep.ActionHalt
+ }
+
+ return multistep.ActionContinue
+}
+
+func (s *StepUp) Cleanup(state multistep.StateBag) {
+ driver := state.Get("driver").(VagrantDriver)
+ ui := state.Get("ui").(packer.Ui)
+
+ ui.Say(fmt.Sprintf("%sing Vagrant box...", s.TeardownMethod))
+
+ var err error
+ if s.TeardownMethod == "halt" {
+ err = driver.Halt(s.GlobalID)
+ } else if s.TeardownMethod == "suspend" {
+ err = driver.Suspend(s.GlobalID)
+ } else if s.TeardownMethod == "destroy" {
+ err = driver.Destroy(s.GlobalID)
+ } else {
+ // Should never get here because of template validation
+ state.Put("error", fmt.Errorf("Invalid teardown method selected; must be either halt, suspend, or destory."))
+ }
+ if err != nil {
+ state.Put("error", fmt.Errorf("Error halting Vagrant machine; please try to do this manually"))
+ }
+}
diff --git a/builder/virtualbox/common/step_attach_guest_additions.go b/builder/virtualbox/common/step_attach_guest_additions.go
index d5698c372..dc3e2a0eb 100644
--- a/builder/virtualbox/common/step_attach_guest_additions.go
+++ b/builder/virtualbox/common/step_attach_guest_additions.go
@@ -21,8 +21,9 @@ import (
//
// Produces:
type StepAttachGuestAdditions struct {
- attachedPath string
- GuestAdditionsMode string
+ attachedPath string
+ GuestAdditionsMode string
+ GuestAdditionsInterface string
}
func (s *StepAttachGuestAdditions) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
@@ -40,12 +41,22 @@ func (s *StepAttachGuestAdditions) Run(_ context.Context, state multistep.StateB
guestAdditionsPath := state.Get("guest_additions_path").(string)
// Attach the guest additions to the computer
+
+ controllerName := "IDE Controller"
+ port := "1"
+ device := "0"
+ if s.GuestAdditionsInterface == "sata" {
+ controllerName = "SATA Controller"
+ port = "2"
+ device = "0"
+ }
+
log.Println("Attaching guest additions ISO onto IDE controller...")
command := []string{
"storageattach", vmName,
- "--storagectl", "IDE Controller",
- "--port", "1",
- "--device", "0",
+ "--storagectl", controllerName,
+ "--port", port,
+ "--device", device,
"--type", "dvddrive",
"--medium", guestAdditionsPath,
}
@@ -71,11 +82,20 @@ func (s *StepAttachGuestAdditions) Cleanup(state multistep.StateBag) {
driver := state.Get("driver").(Driver)
vmName := state.Get("vmName").(string)
+ controllerName := "IDE Controller"
+ port := "1"
+ device := "0"
+ if s.GuestAdditionsInterface == "sata" {
+ controllerName = "SATA Controller"
+ port = "2"
+ device = "0"
+ }
+
command := []string{
"storageattach", vmName,
- "--storagectl", "IDE Controller",
- "--port", "1",
- "--device", "0",
+ "--storagectl", controllerName,
+ "--port", port,
+ "--device", device,
"--medium", "none",
}
diff --git a/builder/virtualbox/common/step_remove_devices.go b/builder/virtualbox/common/step_remove_devices.go
index 461705763..d50afb398 100644
--- a/builder/virtualbox/common/step_remove_devices.go
+++ b/builder/virtualbox/common/step_remove_devices.go
@@ -20,7 +20,8 @@ import (
//
// Produces:
type StepRemoveDevices struct {
- Bundling VBoxBundleConfig
+ Bundling VBoxBundleConfig
+ GuestAdditionsInterface string
}
func (s *StepRemoveDevices) Run(_ context.Context, state multistep.StateBag) multistep.StepAction {
@@ -100,11 +101,19 @@ func (s *StepRemoveDevices) Run(_ context.Context, state multistep.StateBag) mul
if _, ok := state.GetOk("guest_additions_attached"); ok {
ui.Message("Removing guest additions drive...")
+ controllerName := "IDE Controller"
+ port := "1"
+ device := "0"
+ if s.GuestAdditionsInterface == "sata" {
+ controllerName = "SATA Controller"
+ port = "2"
+ device = "0"
+ }
command := []string{
"storageattach", vmName,
- "--storagectl", "IDE Controller",
- "--port", "1",
- "--device", "0",
+ "--storagectl", controllerName,
+ "--port", port,
+ "--device", device,
"--medium", "none",
}
if err := driver.VBoxManage(command...); err != nil {
diff --git a/builder/virtualbox/iso/builder.go b/builder/virtualbox/iso/builder.go
index 55aa602a9..dce65c4df 100644
--- a/builder/virtualbox/iso/builder.go
+++ b/builder/virtualbox/iso/builder.go
@@ -41,20 +41,21 @@ type Config struct {
vboxcommon.VBoxVersionConfig `mapstructure:",squash"`
vboxcommon.VBoxBundleConfig `mapstructure:",squash"`
- DiskSize uint `mapstructure:"disk_size"`
- GuestAdditionsMode string `mapstructure:"guest_additions_mode"`
- GuestAdditionsPath string `mapstructure:"guest_additions_path"`
- GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
- GuestAdditionsURL string `mapstructure:"guest_additions_url"`
- GuestOSType string `mapstructure:"guest_os_type"`
- HardDriveDiscard bool `mapstructure:"hard_drive_discard"`
- HardDriveInterface string `mapstructure:"hard_drive_interface"`
- SATAPortCount int `mapstructure:"sata_port_count"`
- HardDriveNonrotational bool `mapstructure:"hard_drive_nonrotational"`
- ISOInterface string `mapstructure:"iso_interface"`
- KeepRegistered bool `mapstructure:"keep_registered"`
- SkipExport bool `mapstructure:"skip_export"`
- VMName string `mapstructure:"vm_name"`
+ DiskSize uint `mapstructure:"disk_size"`
+ GuestAdditionsMode string `mapstructure:"guest_additions_mode"`
+ GuestAdditionsPath string `mapstructure:"guest_additions_path"`
+ GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
+ GuestAdditionsURL string `mapstructure:"guest_additions_url"`
+ GuestAdditionsInterface string `mapstructure:"guest_additions_interface"`
+ GuestOSType string `mapstructure:"guest_os_type"`
+ HardDriveDiscard bool `mapstructure:"hard_drive_discard"`
+ HardDriveInterface string `mapstructure:"hard_drive_interface"`
+ SATAPortCount int `mapstructure:"sata_port_count"`
+ HardDriveNonrotational bool `mapstructure:"hard_drive_nonrotational"`
+ ISOInterface string `mapstructure:"iso_interface"`
+ KeepRegistered bool `mapstructure:"keep_registered"`
+ SkipExport bool `mapstructure:"skip_export"`
+ VMName string `mapstructure:"vm_name"`
ctx interpolate.Context
}
@@ -125,6 +126,10 @@ func (b *Builder) Prepare(raws ...interface{}) ([]string, error) {
b.config.ISOInterface = "ide"
}
+ if b.config.GuestAdditionsInterface == "" {
+ b.config.GuestAdditionsInterface = b.config.ISOInterface
+ }
+
if b.config.VMName == "" {
b.config.VMName = fmt.Sprintf(
"packer-%s-%d", b.config.PackerBuildName, interpolate.InitTime.Unix())
@@ -227,7 +232,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
new(stepCreateDisk),
new(stepAttachISO),
&vboxcommon.StepAttachGuestAdditions{
- GuestAdditionsMode: b.config.GuestAdditionsMode,
+ GuestAdditionsMode: b.config.GuestAdditionsMode,
+ GuestAdditionsInterface: b.config.GuestAdditionsInterface,
},
&vboxcommon.StepConfigureVRDP{
VRDPBindAddress: b.config.VRDPBindAddress,
@@ -280,7 +286,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Delay: b.config.PostShutdownDelay,
},
&vboxcommon.StepRemoveDevices{
- Bundling: b.config.VBoxBundleConfig,
+ Bundling: b.config.VBoxBundleConfig,
+ GuestAdditionsInterface: b.config.GuestAdditionsInterface,
},
&vboxcommon.StepVBoxManage{
Commands: b.config.VBoxManagePost,
diff --git a/builder/virtualbox/ovf/builder.go b/builder/virtualbox/ovf/builder.go
index 60142728f..3a8b3f901 100644
--- a/builder/virtualbox/ovf/builder.go
+++ b/builder/virtualbox/ovf/builder.go
@@ -84,7 +84,8 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
ImportFlags: b.config.ImportFlags,
},
&vboxcommon.StepAttachGuestAdditions{
- GuestAdditionsMode: b.config.GuestAdditionsMode,
+ GuestAdditionsMode: b.config.GuestAdditionsMode,
+ GuestAdditionsInterface: b.config.GuestAdditionsInterface,
},
&vboxcommon.StepConfigureVRDP{
VRDPBindAddress: b.config.VRDPBindAddress,
@@ -136,7 +137,9 @@ func (b *Builder) Run(ui packer.Ui, hook packer.Hook, cache packer.Cache) (packe
Timeout: b.config.ShutdownTimeout,
Delay: b.config.PostShutdownDelay,
},
- new(vboxcommon.StepRemoveDevices),
+ &vboxcommon.StepRemoveDevices{
+ GuestAdditionsInterface: b.config.GuestAdditionsInterface,
+ },
&vboxcommon.StepVBoxManage{
Commands: b.config.VBoxManagePost,
Ctx: b.config.ctx,
diff --git a/builder/virtualbox/ovf/config.go b/builder/virtualbox/ovf/config.go
index bd60a4c4c..b49b5c948 100644
--- a/builder/virtualbox/ovf/config.go
+++ b/builder/virtualbox/ovf/config.go
@@ -28,19 +28,20 @@ type Config struct {
vboxcommon.VBoxManagePostConfig `mapstructure:",squash"`
vboxcommon.VBoxVersionConfig `mapstructure:",squash"`
- Checksum string `mapstructure:"checksum"`
- ChecksumType string `mapstructure:"checksum_type"`
- GuestAdditionsMode string `mapstructure:"guest_additions_mode"`
- GuestAdditionsPath string `mapstructure:"guest_additions_path"`
- GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
- GuestAdditionsURL string `mapstructure:"guest_additions_url"`
- ImportFlags []string `mapstructure:"import_flags"`
- ImportOpts string `mapstructure:"import_opts"`
- SourcePath string `mapstructure:"source_path"`
- TargetPath string `mapstructure:"target_path"`
- VMName string `mapstructure:"vm_name"`
- KeepRegistered bool `mapstructure:"keep_registered"`
- SkipExport bool `mapstructure:"skip_export"`
+ Checksum string `mapstructure:"checksum"`
+ ChecksumType string `mapstructure:"checksum_type"`
+ GuestAdditionsMode string `mapstructure:"guest_additions_mode"`
+ GuestAdditionsPath string `mapstructure:"guest_additions_path"`
+ GuestAdditionsInterface string `mapstructure:"guest_additions_interface"`
+ GuestAdditionsSHA256 string `mapstructure:"guest_additions_sha256"`
+ GuestAdditionsURL string `mapstructure:"guest_additions_url"`
+ ImportFlags []string `mapstructure:"import_flags"`
+ ImportOpts string `mapstructure:"import_opts"`
+ SourcePath string `mapstructure:"source_path"`
+ TargetPath string `mapstructure:"target_path"`
+ VMName string `mapstructure:"vm_name"`
+ KeepRegistered bool `mapstructure:"keep_registered"`
+ SkipExport bool `mapstructure:"skip_export"`
ctx interpolate.Context
}
@@ -72,6 +73,9 @@ func NewConfig(raws ...interface{}) (*Config, []string, error) {
if c.GuestAdditionsPath == "" {
c.GuestAdditionsPath = "VBoxGuestAdditions.iso"
}
+ if c.GuestAdditionsInterface == "" {
+ c.GuestAdditionsInterface = "ide"
+ }
if c.VMName == "" {
c.VMName = fmt.Sprintf(
diff --git a/builder/vmware/common/driver_config.go b/builder/vmware/common/driver_config.go
index b4014768f..b600ae237 100644
--- a/builder/vmware/common/driver_config.go
+++ b/builder/vmware/common/driver_config.go
@@ -69,7 +69,7 @@ func (c *DriverConfig) Validate(SkipExport bool) error {
// now, so that we don't fail for a simple mistake after a long
// build
ovftool := GetOVFTool()
- ovfToolArgs := []string{"--verifyOnly", fmt.Sprintf("vi://%s:%s@%s",
+ ovfToolArgs := []string{"--noSSLVerify", "--verifyOnly", fmt.Sprintf("vi://%s:%s@%s",
url.QueryEscape(c.RemoteUser),
url.QueryEscape(c.RemotePassword),
c.RemoteHost)}
diff --git a/builder/vmware/common/driver_esx5.go b/builder/vmware/common/driver_esx5.go
index 602680ddc..4b083e5ff 100644
--- a/builder/vmware/common/driver_esx5.go
+++ b/builder/vmware/common/driver_esx5.go
@@ -47,7 +47,7 @@ type ESX5Driver struct {
func (d *ESX5Driver) Clone(dst, src string, linked bool) error {
- linesToArray := func(lines string) []string { return strings.Split(strings.Trim(lines, "\n"), "\n") }
+ linesToArray := func(lines string) []string { return strings.Split(strings.Trim(lines, "\r\n"), "\n") }
d.SetOutputDir(path.Dir(filepath.ToSlash(dst)))
srcVmx := d.datastorePath(src)
diff --git a/command/plugin.go b/command/plugin.go
index a387ef682..b0b7149c5 100644
--- a/command/plugin.go
+++ b/command/plugin.go
@@ -44,6 +44,7 @@ import (
scalewaybuilder "github.com/hashicorp/packer/builder/scaleway"
tencentcloudcvmbuilder "github.com/hashicorp/packer/builder/tencentcloud/cvm"
tritonbuilder "github.com/hashicorp/packer/builder/triton"
+ vagrantbuilder "github.com/hashicorp/packer/builder/vagrant"
virtualboxisobuilder "github.com/hashicorp/packer/builder/virtualbox/iso"
virtualboxovfbuilder "github.com/hashicorp/packer/builder/virtualbox/ovf"
vmwareisobuilder "github.com/hashicorp/packer/builder/vmware/iso"
@@ -53,6 +54,7 @@ import (
artificepostprocessor "github.com/hashicorp/packer/post-processor/artifice"
checksumpostprocessor "github.com/hashicorp/packer/post-processor/checksum"
compresspostprocessor "github.com/hashicorp/packer/post-processor/compress"
+ digitaloceanimportpostprocessor "github.com/hashicorp/packer/post-processor/digitalocean-import"
dockerimportpostprocessor "github.com/hashicorp/packer/post-processor/docker-import"
dockerpushpostprocessor "github.com/hashicorp/packer/post-processor/docker-push"
dockersavepostprocessor "github.com/hashicorp/packer/post-processor/docker-save"
@@ -72,6 +74,7 @@ import (
chefsoloprovisioner "github.com/hashicorp/packer/provisioner/chef-solo"
convergeprovisioner "github.com/hashicorp/packer/provisioner/converge"
fileprovisioner "github.com/hashicorp/packer/provisioner/file"
+ inspecprovisioner "github.com/hashicorp/packer/provisioner/inspec"
powershellprovisioner "github.com/hashicorp/packer/provisioner/powershell"
puppetmasterlessprovisioner "github.com/hashicorp/packer/provisioner/puppet-masterless"
puppetserverprovisioner "github.com/hashicorp/packer/provisioner/puppet-server"
@@ -118,6 +121,7 @@ var Builders = map[string]packer.Builder{
"scaleway": new(scalewaybuilder.Builder),
"tencentcloud-cvm": new(tencentcloudcvmbuilder.Builder),
"triton": new(tritonbuilder.Builder),
+ "vagrant": new(vagrantbuilder.Builder),
"virtualbox-iso": new(virtualboxisobuilder.Builder),
"virtualbox-ovf": new(virtualboxovfbuilder.Builder),
"vmware-iso": new(vmwareisobuilder.Builder),
@@ -132,6 +136,7 @@ var Provisioners = map[string]packer.Provisioner{
"chef-solo": new(chefsoloprovisioner.Provisioner),
"converge": new(convergeprovisioner.Provisioner),
"file": new(fileprovisioner.Provisioner),
+ "inspec": new(inspecprovisioner.Provisioner),
"powershell": new(powershellprovisioner.Provisioner),
"puppet-masterless": new(puppetmasterlessprovisioner.Provisioner),
"puppet-server": new(puppetserverprovisioner.Provisioner),
@@ -148,6 +153,7 @@ var PostProcessors = map[string]packer.PostProcessor{
"artifice": new(artificepostprocessor.PostProcessor),
"checksum": new(checksumpostprocessor.PostProcessor),
"compress": new(compresspostprocessor.PostProcessor),
+ "digitalocean-import": new(digitaloceanimportpostprocessor.PostProcessor),
"docker-import": new(dockerimportpostprocessor.PostProcessor),
"docker-push": new(dockerpushpostprocessor.PostProcessor),
"docker-save": new(dockersavepostprocessor.PostProcessor),
diff --git a/provisioner/ansible/adapter.go b/common/adapter/adapter.go
similarity index 93%
rename from provisioner/ansible/adapter.go
rename to common/adapter/adapter.go
index c3dfd3495..510ae40bb 100644
--- a/provisioner/ansible/adapter.go
+++ b/common/adapter/adapter.go
@@ -1,4 +1,4 @@
-package ansible
+package adapter
import (
"bytes"
@@ -17,7 +17,7 @@ import (
// An adapter satisfies SSH requests (from an Ansible client) by delegating SSH
// exec and subsystem commands to a packer.Communicator.
-type adapter struct {
+type Adapter struct {
done <-chan struct{}
l net.Listener
config *ssh.ServerConfig
@@ -26,8 +26,8 @@ type adapter struct {
comm packer.Communicator
}
-func newAdapter(done <-chan struct{}, l net.Listener, config *ssh.ServerConfig, sftpCmd string, ui packer.Ui, comm packer.Communicator) *adapter {
- return &adapter{
+func NewAdapter(done <-chan struct{}, l net.Listener, config *ssh.ServerConfig, sftpCmd string, ui packer.Ui, comm packer.Communicator) *Adapter {
+ return &Adapter{
done: done,
l: l,
config: config,
@@ -37,7 +37,7 @@ func newAdapter(done <-chan struct{}, l net.Listener, config *ssh.ServerConfig,
}
}
-func (c *adapter) Serve() {
+func (c *Adapter) Serve() {
log.Printf("SSH proxy: serving on %s", c.l.Addr())
for {
@@ -62,7 +62,7 @@ func (c *adapter) Serve() {
}
}
-func (c *adapter) Handle(conn net.Conn, ui packer.Ui) error {
+func (c *Adapter) Handle(conn net.Conn, ui packer.Ui) error {
log.Print("SSH proxy: accepted connection")
_, chans, reqs, err := ssh.NewServerConn(conn, c.config)
if err != nil {
@@ -89,7 +89,7 @@ func (c *adapter) Handle(conn net.Conn, ui packer.Ui) error {
return nil
}
-func (c *adapter) handleSession(newChannel ssh.NewChannel) error {
+func (c *Adapter) handleSession(newChannel ssh.NewChannel) error {
channel, requests, err := newChannel.Accept()
if err != nil {
return err
@@ -182,11 +182,11 @@ func (c *adapter) handleSession(newChannel ssh.NewChannel) error {
return nil
}
-func (c *adapter) Shutdown() {
+func (c *Adapter) Shutdown() {
c.l.Close()
}
-func (c *adapter) exec(command string, in io.Reader, out io.Writer, err io.Writer) int {
+func (c *Adapter) exec(command string, in io.Reader, out io.Writer, err io.Writer) int {
var exitStatus int
switch {
case strings.HasPrefix(command, "scp ") && serveSCP(command[4:]):
@@ -206,7 +206,7 @@ func serveSCP(args string) bool {
return bytes.IndexAny(opts, "tf") >= 0
}
-func (c *adapter) scpExec(args string, in io.Reader, out io.Writer) error {
+func (c *Adapter) scpExec(args string, in io.Reader, out io.Writer) error {
opts, rest := scpOptions(args)
// remove the quoting that ansible added to rest for shell safety.
@@ -226,7 +226,7 @@ func (c *adapter) scpExec(args string, in io.Reader, out io.Writer) error {
return errors.New("no scp mode specified")
}
-func (c *adapter) remoteExec(command string, in io.Reader, out io.Writer, err io.Writer) int {
+func (c *Adapter) remoteExec(command string, in io.Reader, out io.Writer, err io.Writer) int {
cmd := &packer.RemoteCmd{
Stdin: in,
Stdout: out,
diff --git a/provisioner/ansible/adapter_test.go b/common/adapter/adapter_test.go
similarity index 95%
rename from provisioner/ansible/adapter_test.go
rename to common/adapter/adapter_test.go
index 29667cbe2..a43b3bedc 100644
--- a/provisioner/ansible/adapter_test.go
+++ b/common/adapter/adapter_test.go
@@ -1,4 +1,4 @@
-package ansible
+package adapter
import (
"errors"
@@ -26,7 +26,7 @@ func TestAdapter_Serve(t *testing.T) {
ui := new(packer.NoopUi)
- sut := newAdapter(done, &l, config, "", newUi(ui), communicator{})
+ sut := NewAdapter(done, &l, config, "", ui, communicator{})
go func() {
i := 0
for range acceptC {
diff --git a/provisioner/ansible/scp.go b/common/adapter/scp.go
similarity index 99%
rename from provisioner/ansible/scp.go
rename to common/adapter/scp.go
index ca029605d..06043ff20 100644
--- a/provisioner/ansible/scp.go
+++ b/common/adapter/scp.go
@@ -1,4 +1,4 @@
-package ansible
+package adapter
import (
"bufio"
diff --git a/common/iso_config.go b/common/iso_config.go
index 4f30580bc..75d285e11 100644
--- a/common/iso_config.go
+++ b/common/iso_config.go
@@ -58,6 +58,11 @@ func (c *ISOConfig) Prepare(ctx *interpolate.Context) (warnings []string, errs [
// If iso_checksum has no value use iso_checksum_url instead.
if c.ISOChecksum == "" {
+ if strings.HasSuffix(strings.ToLower(c.ISOChecksumURL), ".iso") {
+ errs = append(errs, fmt.Errorf("Error parsing checksum:"+
+ " .iso is not a valid checksum extension"))
+ return warnings, errs
+ }
u, err := url.Parse(c.ISOChecksumURL)
if err != nil {
errs = append(errs,
diff --git a/common/iso_config_test.go b/common/iso_config_test.go
index 0a054f700..c38154f42 100644
--- a/common/iso_config_test.go
+++ b/common/iso_config_test.go
@@ -239,6 +239,16 @@ func TestISOConfigPrepare_ISOChecksumURL(t *testing.T) {
if i.ISOChecksum != "bar0" {
t.Fatalf("should've found \"bar0\" got: %s", i.ISOChecksum)
}
+
+ // Test that we won't try to read an iso into memory because of a user
+ // error
+ i = testISOConfig()
+ i.ISOChecksumURL = "file:///not_read.iso"
+ i.ISOChecksum = ""
+ warns, err = i.Prepare(nil)
+ if err == nil {
+ t.Fatalf("should have error because iso is bad filetype: %s", err)
+ }
}
func TestISOConfigPrepare_ISOChecksumType(t *testing.T) {
diff --git a/helper/communicator/config.go b/helper/communicator/config.go
index 7d66254b5..fa02810d7 100644
--- a/helper/communicator/config.go
+++ b/helper/communicator/config.go
@@ -66,6 +66,9 @@ type Config struct {
WinRMInsecure bool `mapstructure:"winrm_insecure"`
WinRMUseNTLM bool `mapstructure:"winrm_use_ntlm"`
WinRMTransportDecorator func() winrm.Transporter
+
+ // Delay
+ PauseBeforeConnect time.Duration `mapstructure:"pause_before_connecting"`
}
// ReadSSHPrivateKeyFile returns the SSH private key bytes
diff --git a/helper/communicator/step_connect.go b/helper/communicator/step_connect.go
index 10adc5d2b..6cab6e004 100644
--- a/helper/communicator/step_connect.go
+++ b/helper/communicator/step_connect.go
@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"log"
+ "time"
"github.com/hashicorp/packer/communicator/none"
"github.com/hashicorp/packer/helper/multistep"
@@ -44,9 +45,28 @@ type StepConnect struct {
substep multistep.Step
}
+func (s *StepConnect) pause(pauseLen time.Duration, ctx context.Context) bool {
+ // Use a select to determine if we get cancelled during the wait
+ log.Printf("Pausing before connecting...")
+ select {
+ case <-ctx.Done():
+ return true
+ case <-time.After(pauseLen):
+ }
+ log.Printf("Pause over; connecting...")
+ return false
+}
+
func (s *StepConnect) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
ui := state.Get("ui").(packer.Ui)
+ if s.Config.PauseBeforeConnect > 0 {
+ cancelled := s.pause(s.Config.PauseBeforeConnect, ctx)
+ if cancelled {
+ return multistep.ActionHalt
+ }
+ }
+
typeMap := map[string]multistep.Step{
"none": nil,
"ssh": &StepConnectSSH{
diff --git a/packer/ui.go b/packer/ui.go
index 2a0f5e275..e42e08139 100644
--- a/packer/ui.go
+++ b/packer/ui.go
@@ -343,7 +343,7 @@ func (u *MachineReadableUi) ProgressBar() ProgressBar {
return new(NoopProgressBar)
}
-// TimestampedUi is a UI that wraps another UI implementation and prefixes
+// TimestampedUi is a UI that wraps another UI implementation and
// prefixes each message with an RFC3339 timestamp
type TimestampedUi struct {
Ui Ui
@@ -376,3 +376,48 @@ func (u *TimestampedUi) ProgressBar() ProgressBar { return u.Ui.ProgressBar() }
func (u *TimestampedUi) timestampLine(string string) string {
return fmt.Sprintf("%v: %v", time.Now().Format(time.RFC3339), string)
}
+
+// Safe is a UI that wraps another UI implementation and
+// provides concurrency-safe access
+type SafeUi struct {
+ Sem chan int
+ Ui Ui
+}
+
+var _ Ui = new(SafeUi)
+
+func (u *SafeUi) Ask(s string) (string, error) {
+ u.Sem <- 1
+ ret, err := u.Ui.Ask(s)
+ <-u.Sem
+
+ return ret, err
+}
+
+func (u *SafeUi) Say(s string) {
+ u.Sem <- 1
+ u.Ui.Say(s)
+ <-u.Sem
+}
+
+func (u *SafeUi) Message(s string) {
+ u.Sem <- 1
+ u.Ui.Message(s)
+ <-u.Sem
+}
+
+func (u *SafeUi) Error(s string) {
+ u.Sem <- 1
+ u.Ui.Error(s)
+ <-u.Sem
+}
+
+func (u *SafeUi) Machine(t string, args ...string) {
+ u.Sem <- 1
+ u.Ui.Machine(t, args...)
+ <-u.Sem
+}
+
+func (u *SafeUi) ProgressBar() ProgressBar {
+ return new(NoopProgressBar)
+}
diff --git a/post-processor/digitalocean-import/post-processor.go b/post-processor/digitalocean-import/post-processor.go
new file mode 100644
index 000000000..d7dcd867b
--- /dev/null
+++ b/post-processor/digitalocean-import/post-processor.go
@@ -0,0 +1,365 @@
+package digitaloceanimport
+
+import (
+ "context"
+ "fmt"
+ "golang.org/x/oauth2"
+ "log"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/aws/aws-sdk-go/aws"
+ "github.com/aws/aws-sdk-go/aws/credentials"
+ "github.com/aws/aws-sdk-go/aws/session"
+ "github.com/aws/aws-sdk-go/service/s3"
+ "github.com/aws/aws-sdk-go/service/s3/s3manager"
+ "github.com/digitalocean/godo"
+
+ "github.com/hashicorp/packer/builder/digitalocean"
+ "github.com/hashicorp/packer/common"
+ "github.com/hashicorp/packer/helper/config"
+ "github.com/hashicorp/packer/packer"
+ "github.com/hashicorp/packer/template/interpolate"
+)
+
+const BuilderId = "packer.post-processor.digitalocean-import"
+
+type Config struct {
+ common.PackerConfig `mapstructure:",squash"`
+
+ APIToken string `mapstructure:"api_token"`
+ SpacesKey string `mapstructure:"spaces_key"`
+ SpacesSecret string `mapstructure:"spaces_secret"`
+
+ SpacesRegion string `mapstructure:"spaces_region"`
+ SpaceName string `mapstructure:"space_name"`
+ ObjectName string `mapstructure:"space_object_name"`
+ SkipClean bool `mapstructure:"skip_clean"`
+ Tags []string `mapstructure:"image_tags"`
+ Name string `mapstructure:"image_name"`
+ Description string `mapstructure:"image_description"`
+ Distribution string `mapstructure:"image_distribution"`
+ ImageRegions []string `mapstructure:"image_regions"`
+ Timeout time.Duration `mapstructure:"timeout"`
+
+ ctx interpolate.Context
+}
+
+type PostProcessor struct {
+ config Config
+}
+
+type apiTokenSource struct {
+ AccessToken string
+}
+
+type logger struct {
+ logger *log.Logger
+}
+
+func (t *apiTokenSource) Token() (*oauth2.Token, error) {
+ return &oauth2.Token{
+ AccessToken: t.AccessToken,
+ }, nil
+}
+
+func (l logger) Log(args ...interface{}) {
+ l.logger.Println(args...)
+}
+
+func (p *PostProcessor) Configure(raws ...interface{}) error {
+ err := config.Decode(&p.config, &config.DecodeOpts{
+ Interpolate: true,
+ InterpolateContext: &p.config.ctx,
+ InterpolateFilter: &interpolate.RenderFilter{
+ Exclude: []string{"space_object_name"},
+ },
+ }, raws...)
+ if err != nil {
+ return err
+ }
+
+ if p.config.SpacesKey == "" {
+ p.config.SpacesKey = os.Getenv("DIGITALOCEAN_SPACES_ACCESS_KEY")
+ }
+
+ if p.config.SpacesSecret == "" {
+ p.config.SpacesSecret = os.Getenv("DIGITALOCEAN_SPACES_SECRET_KEY")
+ }
+
+ if p.config.APIToken == "" {
+ p.config.APIToken = os.Getenv("DIGITALOCEAN_API_TOKEN")
+ }
+
+ if p.config.ObjectName == "" {
+ p.config.ObjectName = "packer-import-{{timestamp}}"
+ }
+
+ if p.config.Distribution == "" {
+ p.config.Distribution = "Unkown"
+ }
+
+ if p.config.Timeout == 0 {
+ p.config.Timeout = 20 * time.Minute
+ }
+
+ errs := new(packer.MultiError)
+
+ if err = interpolate.Validate(p.config.ObjectName, &p.config.ctx); err != nil {
+ errs = packer.MultiErrorAppend(
+ errs, fmt.Errorf("Error parsing space_object_name template: %s", err))
+ }
+
+ requiredArgs := map[string]*string{
+ "api_token": &p.config.APIToken,
+ "spaces_key": &p.config.SpacesKey,
+ "spaces_secret": &p.config.SpacesSecret,
+ "spaces_region": &p.config.SpacesRegion,
+ "space_name": &p.config.SpaceName,
+ "image_name": &p.config.Name,
+ "image_regions": &p.config.ImageRegions[0],
+ }
+ for key, ptr := range requiredArgs {
+ if *ptr == "" {
+ errs = packer.MultiErrorAppend(
+ errs, fmt.Errorf("%s must be set", key))
+ }
+ }
+
+ if len(errs.Errors) > 0 {
+ return errs
+ }
+
+ packer.LogSecretFilter.Set(p.config.SpacesKey, p.config.SpacesSecret, p.config.APIToken)
+ log.Println(p.config)
+ return nil
+}
+
+func (p *PostProcessor) PostProcess(ui packer.Ui, artifact packer.Artifact) (packer.Artifact, bool, error) {
+ var err error
+
+ p.config.ObjectName, err = interpolate.Render(p.config.ObjectName, &p.config.ctx)
+ if err != nil {
+ return nil, false, fmt.Errorf("Error rendering space_object_name template: %s", err)
+ }
+ log.Printf("Rendered space_object_name as %s", p.config.ObjectName)
+
+ source := ""
+ artifacts := artifact.Files()
+ log.Println("Looking for image in artifact")
+ if len(artifacts) > 1 {
+ validSuffix := []string{"raw", "img", "qcow2", "vhdx", "vdi", "vmdk", "tar.bz2", "tar.xz", "tar.gz"}
+ for _, path := range artifact.Files() {
+ for _, suffix := range validSuffix {
+ if strings.HasSuffix(path, suffix) {
+ source = path
+ break
+ }
+ }
+ if source != "" {
+ break
+ }
+ }
+ } else {
+ source = artifact.Files()[0]
+ }
+
+ if source == "" {
+ return nil, false, fmt.Errorf("Image file not found")
+ }
+
+ spacesCreds := credentials.NewStaticCredentials(p.config.SpacesKey, p.config.SpacesSecret, "")
+ spacesEndpoint := fmt.Sprintf("https://%s.digitaloceanspaces.com", p.config.SpacesRegion)
+ spacesConfig := &aws.Config{
+ Credentials: spacesCreds,
+ Endpoint: aws.String(spacesEndpoint),
+ Region: aws.String(p.config.SpacesRegion),
+ LogLevel: aws.LogLevel(aws.LogDebugWithSigning),
+ Logger: &logger{
+ logger: log.New(os.Stderr, "", log.LstdFlags),
+ },
+ }
+ sess := session.New(spacesConfig)
+
+ ui.Message(fmt.Sprintf("Uploading %s to spaces://%s/%s", source, p.config.SpaceName, p.config.ObjectName))
+ err = uploadImageToSpaces(source, p, sess)
+ if err != nil {
+ return nil, false, err
+ }
+ ui.Message(fmt.Sprintf("Completed upload of %s to spaces://%s/%s", source, p.config.SpaceName, p.config.ObjectName))
+
+ client := godo.NewClient(oauth2.NewClient(oauth2.NoContext, &apiTokenSource{
+ AccessToken: p.config.APIToken,
+ }))
+
+ ui.Message(fmt.Sprintf("Started import of spaces://%s/%s", p.config.SpaceName, p.config.ObjectName))
+ image, err := importImageFromSpaces(p, client)
+ if err != nil {
+ return nil, false, err
+ }
+
+ ui.Message(fmt.Sprintf("Waiting for import of image %s to complete (may take a while)", p.config.Name))
+ err = waitUntilImageAvailable(client, image.ID, p.config.Timeout)
+ if err != nil {
+ return nil, false, fmt.Errorf("Import of image %s failed with error: %s", p.config.Name, err)
+ }
+ ui.Message(fmt.Sprintf("Import of image %s complete", p.config.Name))
+
+ if len(p.config.ImageRegions) > 1 {
+ // Remove the first region from the slice as the image is already there.
+ regions := p.config.ImageRegions
+ regions[0] = regions[len(regions)-1]
+ regions[len(regions)-1] = ""
+ regions = regions[:len(regions)-1]
+
+ ui.Message(fmt.Sprintf("Distributing image %s to additional regions: %v", p.config.Name, regions))
+ err = distributeImageToRegions(client, image.ID, regions, p.config.Timeout)
+ if err != nil {
+ return nil, false, err
+ }
+ }
+
+ log.Printf("Adding created image ID %v to output artifacts", image.ID)
+ artifact = &digitalocean.Artifact{
+ SnapshotName: image.Name,
+ SnapshotId: image.ID,
+ RegionNames: p.config.ImageRegions,
+ Client: client,
+ }
+
+ if !p.config.SkipClean {
+ ui.Message(fmt.Sprintf("Deleting import source spaces://%s/%s", p.config.SpaceName, p.config.ObjectName))
+ err = deleteImageFromSpaces(p, sess)
+ if err != nil {
+ return nil, false, err
+ }
+ }
+
+ return artifact, false, nil
+}
+
+func uploadImageToSpaces(source string, p *PostProcessor, s *session.Session) (err error) {
+ file, err := os.Open(source)
+ if err != nil {
+ return fmt.Errorf("Failed to open %s: %s", source, err)
+ }
+
+ uploader := s3manager.NewUploader(s)
+ _, err = uploader.Upload(&s3manager.UploadInput{
+ Body: file,
+ Bucket: &p.config.SpaceName,
+ Key: &p.config.ObjectName,
+ ACL: aws.String("public-read"),
+ })
+ if err != nil {
+ return fmt.Errorf("Failed to upload %s: %s", source, err)
+ }
+
+ file.Close()
+
+ return nil
+}
+
+func importImageFromSpaces(p *PostProcessor, client *godo.Client) (image *godo.Image, err error) {
+ log.Printf("Importing custom image from spaces://%s/%s", p.config.SpaceName, p.config.ObjectName)
+
+ url := fmt.Sprintf("https://%s.%s.digitaloceanspaces.com/%s", p.config.SpaceName, p.config.SpacesRegion, p.config.ObjectName)
+ createRequest := &godo.CustomImageCreateRequest{
+ Name: p.config.Name,
+ Url: url,
+ Region: p.config.ImageRegions[0],
+ Distribution: p.config.Distribution,
+ Description: p.config.Description,
+ Tags: p.config.Tags,
+ }
+
+ image, _, err = client.Images.Create(context.TODO(), createRequest)
+ if err != nil {
+ return image, fmt.Errorf("Failed to import from spaces://%s/%s: %s", p.config.SpaceName, p.config.ObjectName, err)
+ }
+
+ return image, nil
+}
+
+func waitUntilImageAvailable(client *godo.Client, imageId int, timeout time.Duration) (err error) {
+ done := make(chan struct{})
+ defer close(done)
+
+ result := make(chan error, 1)
+ go func() {
+ attempts := 0
+ for {
+ attempts += 1
+
+ log.Printf("Waiting for image to become available... (attempt: %d)", attempts)
+ image, _, err := client.Images.GetByID(context.TODO(), imageId)
+ if err != nil {
+ result <- err
+ return
+ }
+
+ if image.Status == "available" {
+ result <- nil
+ return
+ }
+
+ if image.ErrorMessage != "" {
+ result <- fmt.Errorf("%v", image.ErrorMessage)
+ return
+ }
+
+ time.Sleep(3 * time.Second)
+
+ select {
+ case <-done:
+ return
+ default:
+ }
+ }
+ }()
+
+ log.Printf("Waiting for up to %d seconds for image to become available", timeout/time.Second)
+ select {
+ case err := <-result:
+ return err
+ case <-time.After(timeout):
+ err := fmt.Errorf("Timeout while waiting to for action to become available")
+ return err
+ }
+}
+
+func distributeImageToRegions(client *godo.Client, imageId int, regions []string, timeout time.Duration) (err error) {
+ for _, region := range regions {
+ transferRequest := &godo.ActionRequest{
+ "type": "transfer",
+ "region": region,
+ }
+ log.Printf("Transferring image to %s", region)
+ action, _, err := client.ImageActions.Transfer(context.TODO(), imageId, transferRequest)
+ if err != nil {
+ return fmt.Errorf("Error transferring image: %s", err)
+ }
+
+ if err := digitalocean.WaitForImageState(godo.ActionCompleted, imageId, action.ID, client, timeout); err != nil {
+ if err != nil {
+ return fmt.Errorf("Error transferring image: %s", err)
+ }
+ }
+ }
+
+ return nil
+}
+
+func deleteImageFromSpaces(p *PostProcessor, s *session.Session) (err error) {
+ s3conn := s3.New(s)
+ _, err = s3conn.DeleteObject(&s3.DeleteObjectInput{
+ Bucket: &p.config.SpaceName,
+ Key: &p.config.ObjectName,
+ })
+ if err != nil {
+ return fmt.Errorf("Failed to delete spaces://%s/%s: %s", p.config.SpaceName, p.config.ObjectName, err)
+ }
+
+ return nil
+}
diff --git a/post-processor/digitalocean-import/post-processor_test.go b/post-processor/digitalocean-import/post-processor_test.go
new file mode 100644
index 000000000..45e7a5b32
--- /dev/null
+++ b/post-processor/digitalocean-import/post-processor_test.go
@@ -0,0 +1,32 @@
+package digitaloceanimport
+
+import (
+ "bytes"
+ "testing"
+
+ "github.com/hashicorp/packer/packer"
+)
+
+func testConfig() map[string]interface{} {
+ return map[string]interface{}{}
+}
+
+func testPP(t *testing.T) *PostProcessor {
+ var p PostProcessor
+ if err := p.Configure(testConfig()); err != nil {
+ t.Fatalf("err: %s", err)
+ }
+
+ return &p
+}
+
+func testUi() *packer.BasicUi {
+ return &packer.BasicUi{
+ Reader: new(bytes.Buffer),
+ Writer: new(bytes.Buffer),
+ }
+}
+
+func TestPostProcessor_ImplementsPostProcessor(t *testing.T) {
+ var _ packer.PostProcessor = new(PostProcessor)
+}
diff --git a/provisioner/ansible/provisioner.go b/provisioner/ansible/provisioner.go
index 9979fbd2a..e836eb894 100644
--- a/provisioner/ansible/provisioner.go
+++ b/provisioner/ansible/provisioner.go
@@ -26,6 +26,7 @@ import (
"golang.org/x/crypto/ssh"
"github.com/hashicorp/packer/common"
+ "github.com/hashicorp/packer/common/adapter"
commonhelper "github.com/hashicorp/packer/helper/common"
"github.com/hashicorp/packer/helper/config"
"github.com/hashicorp/packer/packer"
@@ -63,7 +64,7 @@ type Config struct {
type Provisioner struct {
config Config
- adapter *adapter
+ adapter *adapter.Adapter
done chan struct{}
ansibleVersion string
ansibleMajVersion uint
@@ -285,8 +286,11 @@ func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
return err
}
- ui = newUi(ui)
- p.adapter = newAdapter(p.done, localListener, config, p.config.SFTPCmd, ui, comm)
+ ui = &packer.SafeUi{
+ Sem: make(chan int, 1),
+ Ui: ui,
+ }
+ p.adapter = adapter.NewAdapter(p.done, localListener, config, p.config.SFTPCmd, ui, comm)
defer func() {
log.Print("shutting down the SSH proxy")
@@ -556,49 +560,3 @@ func getWinRMPassword(buildName string) string {
packer.LogSecretFilter.Set(winRMPass)
return winRMPass
}
-
-// Ui provides concurrency-safe access to packer.Ui.
-type Ui struct {
- sem chan int
- ui packer.Ui
-}
-
-func newUi(ui packer.Ui) packer.Ui {
- return &Ui{sem: make(chan int, 1), ui: ui}
-}
-
-func (ui *Ui) Ask(s string) (string, error) {
- ui.sem <- 1
- ret, err := ui.ui.Ask(s)
- <-ui.sem
-
- return ret, err
-}
-
-func (ui *Ui) Say(s string) {
- ui.sem <- 1
- ui.ui.Say(s)
- <-ui.sem
-}
-
-func (ui *Ui) Message(s string) {
- ui.sem <- 1
- ui.ui.Message(s)
- <-ui.sem
-}
-
-func (ui *Ui) Error(s string) {
- ui.sem <- 1
- ui.ui.Error(s)
- <-ui.sem
-}
-
-func (ui *Ui) Machine(t string, args ...string) {
- ui.sem <- 1
- ui.ui.Machine(t, args...)
- <-ui.sem
-}
-
-func (ui *Ui) ProgressBar() packer.ProgressBar {
- return new(packer.NoopProgressBar)
-}
diff --git a/provisioner/guest_commands.go b/provisioner/guest_commands.go
index 3a361a208..ca2b238a4 100644
--- a/provisioner/guest_commands.go
+++ b/provisioner/guest_commands.go
@@ -30,7 +30,7 @@ var guestOSTypeCommands = map[string]guestOSTypeCommand{
mkdir: "powershell.exe -Command \"New-Item -ItemType directory -Force -ErrorAction SilentlyContinue -Path %s\"",
removeDir: "powershell.exe -Command \"rm %s -recurse -force\"",
statPath: "powershell.exe -Command { if (test-path %s) { exit 0 } else { exit 1 } }",
- mv: "powershell.exe -Command \"mv %s %s\"",
+ mv: "powershell.exe -Command \"mv %s %s -force\"",
},
}
diff --git a/provisioner/guest_commands_test.go b/provisioner/guest_commands_test.go
index 08baca058..ffbad4bbd 100644
--- a/provisioner/guest_commands_test.go
+++ b/provisioner/guest_commands_test.go
@@ -183,13 +183,13 @@ func TestMovePath(t *testing.T) {
t.Fatalf("Failed to create new GuestCommands for OS: %s", WindowsOSType)
}
cmd = guestCmd.MovePath("C:\\Temp\\SomeDir", "C:\\Temp\\NewDir")
- if cmd != "powershell.exe -Command \"mv C:\\Temp\\SomeDir C:\\Temp\\NewDir\"" {
+ if cmd != "powershell.exe -Command \"mv C:\\Temp\\SomeDir C:\\Temp\\NewDir -force\"" {
t.Fatalf("Unexpected Windows remove dir cmd: %s", cmd)
}
// Windows OS w/ space in path
cmd = guestCmd.MovePath("C:\\Temp\\Some Dir", "C:\\Temp\\New Dir")
- if cmd != "powershell.exe -Command \"mv C:\\Temp\\Some` Dir C:\\Temp\\New` Dir\"" {
+ if cmd != "powershell.exe -Command \"mv C:\\Temp\\Some` Dir C:\\Temp\\New` Dir -force\"" {
t.Fatalf("Unexpected Windows remove dir cmd: %s", cmd)
}
}
diff --git a/provisioner/inspec/provisioner.go b/provisioner/inspec/provisioner.go
new file mode 100644
index 000000000..fab92ee49
--- /dev/null
+++ b/provisioner/inspec/provisioner.go
@@ -0,0 +1,524 @@
+package inspec
+
+import (
+ "bufio"
+ "bytes"
+ "crypto/rand"
+ "crypto/rsa"
+ "crypto/x509"
+ "encoding/pem"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "log"
+ "net"
+ "os"
+ "os/exec"
+ "os/user"
+ "regexp"
+ "strconv"
+ "strings"
+ "sync"
+ "unicode"
+
+ "golang.org/x/crypto/ssh"
+
+ "github.com/hashicorp/packer/common"
+ "github.com/hashicorp/packer/common/adapter"
+ "github.com/hashicorp/packer/helper/config"
+ "github.com/hashicorp/packer/packer"
+ "github.com/hashicorp/packer/template/interpolate"
+)
+
+var SupportedBackends = map[string]bool{"docker": true, "local": true, "ssh": true, "winrm": true}
+
+type Config struct {
+ common.PackerConfig `mapstructure:",squash"`
+ ctx interpolate.Context
+
+ // The command to run inspec
+ Command string
+ SubCommand string
+
+ // Extra options to pass to the inspec command
+ ExtraArguments []string `mapstructure:"extra_arguments"`
+ InspecEnvVars []string `mapstructure:"inspec_env_vars"`
+
+ // The profile to execute.
+ Profile string `mapstructure:"profile"`
+ AttributesDirectory string `mapstructure:"attributes_directory"`
+ AttributesFiles []string `mapstructure:"attributes"`
+ Backend string `mapstructure:"backend"`
+ User string `mapstructure:"user"`
+ Host string `mapstructure:"host"`
+ LocalPort uint `mapstructure:"local_port"`
+ SSHHostKeyFile string `mapstructure:"ssh_host_key_file"`
+ SSHAuthorizedKeyFile string `mapstructure:"ssh_authorized_key_file"`
+}
+
+type Provisioner struct {
+ config Config
+ adapter *adapter.Adapter
+ done chan struct{}
+ inspecVersion string
+ inspecMajVersion uint
+}
+
+func (p *Provisioner) Prepare(raws ...interface{}) error {
+ p.done = make(chan struct{})
+
+ err := config.Decode(&p.config, &config.DecodeOpts{
+ Interpolate: true,
+ InterpolateContext: &p.config.ctx,
+ InterpolateFilter: &interpolate.RenderFilter{
+ Exclude: []string{},
+ },
+ }, raws...)
+ if err != nil {
+ return err
+ }
+
+ // Defaults
+ if p.config.Command == "" {
+ p.config.Command = "inspec"
+ }
+
+ if p.config.SubCommand == "" {
+ p.config.SubCommand = "exec"
+ }
+
+ var errs *packer.MultiError
+ err = validateProfileConfig(p.config.Profile)
+ if err != nil {
+ errs = packer.MultiErrorAppend(errs, err)
+ }
+
+ // Check that the authorized key file exists
+ if len(p.config.SSHAuthorizedKeyFile) > 0 {
+ err = validateFileConfig(p.config.SSHAuthorizedKeyFile, "ssh_authorized_key_file", true)
+ if err != nil {
+ log.Println(p.config.SSHAuthorizedKeyFile, "does not exist")
+ errs = packer.MultiErrorAppend(errs, err)
+ }
+ }
+ if len(p.config.SSHHostKeyFile) > 0 {
+ err = validateFileConfig(p.config.SSHHostKeyFile, "ssh_host_key_file", true)
+ if err != nil {
+ log.Println(p.config.SSHHostKeyFile, "does not exist")
+ errs = packer.MultiErrorAppend(errs, err)
+ }
+ }
+
+ if p.config.Backend == "" {
+ p.config.Backend = "ssh"
+ }
+
+ if _, ok := SupportedBackends[p.config.Backend]; !ok {
+ errs = packer.MultiErrorAppend(errs, fmt.Errorf("backend: %s must be a valid backend", p.config.Backend))
+ }
+
+ if p.config.Backend == "docker" && p.config.Host == "" {
+ errs = packer.MultiErrorAppend(errs, fmt.Errorf("backend: host must be specified for docker backend"))
+ }
+
+ if p.config.Host == "" {
+ p.config.Host = "127.0.0.1"
+ }
+
+ if p.config.LocalPort > 65535 {
+ errs = packer.MultiErrorAppend(errs, fmt.Errorf("local_port: %d must be a valid port", p.config.LocalPort))
+ }
+
+ if len(p.config.AttributesDirectory) > 0 {
+ err = validateDirectoryConfig(p.config.AttributesDirectory, "attrs")
+ if err != nil {
+ log.Println(p.config.AttributesDirectory, "does not exist")
+ errs = packer.MultiErrorAppend(errs, err)
+ }
+ }
+
+ if p.config.User == "" {
+ usr, err := user.Current()
+ if err != nil {
+ errs = packer.MultiErrorAppend(errs, err)
+ } else {
+ p.config.User = usr.Username
+ }
+ }
+ if p.config.User == "" {
+ errs = packer.MultiErrorAppend(errs, fmt.Errorf("user: could not determine current user from environment."))
+ }
+
+ if errs != nil && len(errs.Errors) > 0 {
+ return errs
+ }
+ return nil
+}
+
+func (p *Provisioner) getVersion() error {
+ out, err := exec.Command(p.config.Command, "version").Output()
+ if err != nil {
+ return fmt.Errorf(
+ "Error running \"%s version\": %s", p.config.Command, err.Error())
+ }
+
+ versionRe := regexp.MustCompile(`\w (\d+\.\d+[.\d+]*)`)
+ matches := versionRe.FindStringSubmatch(string(out))
+ if matches == nil {
+ return fmt.Errorf(
+ "Could not find %s version in output:\n%s", p.config.Command, string(out))
+ }
+
+ version := matches[1]
+ log.Printf("%s version: %s", p.config.Command, version)
+ p.inspecVersion = version
+
+ majVer, err := strconv.ParseUint(strings.Split(version, ".")[0], 10, 0)
+ if err != nil {
+ return fmt.Errorf("Could not parse major version from \"%s\".", version)
+ }
+ p.inspecMajVersion = uint(majVer)
+
+ return nil
+}
+
+func (p *Provisioner) Provision(ui packer.Ui, comm packer.Communicator) error {
+ ui.Say("Provisioning with Inspec...")
+
+ for i, envVar := range p.config.InspecEnvVars {
+ envVar, err := interpolate.Render(envVar, &p.config.ctx)
+ if err != nil {
+ return fmt.Errorf("Could not interpolate inspec env vars: %s", err)
+ }
+ p.config.InspecEnvVars[i] = envVar
+ }
+
+ for i, arg := range p.config.ExtraArguments {
+ arg, err := interpolate.Render(arg, &p.config.ctx)
+ if err != nil {
+ return fmt.Errorf("Could not interpolate inspec extra arguments: %s", err)
+ }
+ p.config.ExtraArguments[i] = arg
+ }
+
+ for i, arg := range p.config.AttributesFiles {
+ arg, err := interpolate.Render(arg, &p.config.ctx)
+ if err != nil {
+ return fmt.Errorf("Could not interpolate inspec attributes: %s", err)
+ }
+ p.config.AttributesFiles[i] = arg
+ }
+
+ k, err := newUserKey(p.config.SSHAuthorizedKeyFile)
+ if err != nil {
+ return err
+ }
+
+ hostSigner, err := newSigner(p.config.SSHHostKeyFile)
+ // Remove the private key file
+ if len(k.privKeyFile) > 0 {
+ defer os.Remove(k.privKeyFile)
+ }
+
+ keyChecker := ssh.CertChecker{
+ UserKeyFallback: func(conn ssh.ConnMetadata, pubKey ssh.PublicKey) (*ssh.Permissions, error) {
+ if user := conn.User(); user != p.config.User {
+ return nil, errors.New(fmt.Sprintf("authentication failed: %s is not a valid user", user))
+ }
+
+ if !bytes.Equal(k.Marshal(), pubKey.Marshal()) {
+ return nil, errors.New("authentication failed: unauthorized key")
+ }
+
+ return nil, nil
+ },
+ }
+
+ config := &ssh.ServerConfig{
+ AuthLogCallback: func(conn ssh.ConnMetadata, method string, err error) {
+ log.Printf("authentication attempt from %s to %s as %s using %s", conn.RemoteAddr(), conn.LocalAddr(), conn.User(), method)
+ },
+ PublicKeyCallback: keyChecker.Authenticate,
+ //NoClientAuth: true,
+ }
+
+ config.AddHostKey(hostSigner)
+
+ localListener, err := func() (net.Listener, error) {
+
+ port := p.config.LocalPort
+ tries := 1
+ if port != 0 {
+ tries = 10
+ }
+ for i := 0; i < tries; i++ {
+ l, err := net.Listen("tcp", fmt.Sprintf("127.0.0.1:%d", port))
+ port++
+ if err != nil {
+ ui.Say(err.Error())
+ continue
+ }
+ _, portStr, err := net.SplitHostPort(l.Addr().String())
+ if err != nil {
+ ui.Say(err.Error())
+ continue
+ }
+ portUint64, err := strconv.ParseUint(portStr, 10, 0)
+ if err != nil {
+ ui.Say(err.Error())
+ continue
+ }
+ p.config.LocalPort = uint(portUint64)
+ return l, nil
+ }
+ return nil, errors.New("Error setting up SSH proxy connection")
+ }()
+
+ if err != nil {
+ return err
+ }
+
+ ui = &packer.SafeUi{
+ Sem: make(chan int, 1),
+ Ui: ui,
+ }
+ p.adapter = adapter.NewAdapter(p.done, localListener, config, "", ui, comm)
+
+ defer func() {
+ log.Print("shutting down the SSH proxy")
+ close(p.done)
+ p.adapter.Shutdown()
+ }()
+
+ go p.adapter.Serve()
+
+ tf, err := ioutil.TempFile(p.config.AttributesDirectory, "packer-provisioner-inspec.*.yml")
+ if err != nil {
+ return fmt.Errorf("Error preparing packer attributes file: %s", err)
+ }
+ defer os.Remove(tf.Name())
+
+ w := bufio.NewWriter(tf)
+ w.WriteString(fmt.Sprintf("packer_build_name: %s\n", p.config.PackerBuildName))
+ w.WriteString(fmt.Sprintf("packer_builder_type: %s\n", p.config.PackerBuilderType))
+
+ if err := w.Flush(); err != nil {
+ tf.Close()
+ return fmt.Errorf("Error preparing packer attributes file: %s", err)
+ }
+ tf.Close()
+ p.config.AttributesFiles = append(p.config.AttributesFiles, tf.Name())
+
+ if err := p.executeInspec(ui, comm, k.privKeyFile); err != nil {
+ return fmt.Errorf("Error executing Inspec: %s", err)
+ }
+
+ return nil
+}
+func (p *Provisioner) Cancel() {
+ if p.done != nil {
+ close(p.done)
+ }
+ if p.adapter != nil {
+ p.adapter.Shutdown()
+ }
+ os.Exit(0)
+}
+
+func (p *Provisioner) executeInspec(ui packer.Ui, comm packer.Communicator, privKeyFile string) error {
+ var envvars []string
+
+ args := []string{p.config.SubCommand, p.config.Profile}
+ args = append(args, "--backend", p.config.Backend)
+ args = append(args, "--host", p.config.Host)
+
+ if p.config.Backend == "ssh" {
+ if len(privKeyFile) > 0 {
+ args = append(args, "--key-files", privKeyFile)
+ }
+ args = append(args, "--user", p.config.User)
+ args = append(args, "--port", strconv.FormatUint(uint64(p.config.LocalPort), 10))
+ }
+
+ args = append(args, "--attrs")
+ args = append(args, p.config.AttributesFiles...)
+ args = append(args, p.config.ExtraArguments...)
+
+ if len(p.config.InspecEnvVars) > 0 {
+ envvars = append(envvars, p.config.InspecEnvVars...)
+ }
+
+ cmd := exec.Command(p.config.Command, args...)
+
+ cmd.Env = os.Environ()
+ if len(envvars) > 0 {
+ cmd.Env = append(cmd.Env, envvars...)
+ }
+
+ stdout, err := cmd.StdoutPipe()
+ if err != nil {
+ return err
+ }
+ stderr, err := cmd.StderrPipe()
+ if err != nil {
+ return err
+ }
+
+ wg := sync.WaitGroup{}
+ repeat := func(r io.ReadCloser) {
+ reader := bufio.NewReader(r)
+ for {
+ line, err := reader.ReadString('\n')
+ if line != "" {
+ line = strings.TrimRightFunc(line, unicode.IsSpace)
+ ui.Message(line)
+ }
+ if err != nil {
+ if err == io.EOF {
+ break
+ } else {
+ ui.Error(err.Error())
+ break
+ }
+ }
+ }
+ wg.Done()
+ }
+ wg.Add(2)
+ go repeat(stdout)
+ go repeat(stderr)
+
+ ui.Say(fmt.Sprintf("Executing Inspec: %s", strings.Join(cmd.Args, " ")))
+ if err := cmd.Start(); err != nil {
+ return err
+ }
+ wg.Wait()
+ err = cmd.Wait()
+ if err != nil {
+ return fmt.Errorf("Non-zero exit status: %s", err)
+ }
+
+ return nil
+}
+
+func validateFileConfig(name string, config string, req bool) error {
+ if req {
+ if name == "" {
+ return fmt.Errorf("%s must be specified.", config)
+ }
+ }
+ info, err := os.Stat(name)
+ if err != nil {
+ return fmt.Errorf("%s: %s is invalid: %s", config, name, err)
+ } else if info.IsDir() {
+ return fmt.Errorf("%s: %s must point to a file", config, name)
+ }
+ return nil
+}
+
+func validateProfileConfig(name string) error {
+ if name == "" {
+ return fmt.Errorf("profile must be specified.")
+ }
+ return nil
+}
+
+func validateDirectoryConfig(name string, config string) error {
+ info, err := os.Stat(name)
+ if err != nil {
+ return fmt.Errorf("%s: %s is invalid: %s", config, name, err)
+ } else if !info.IsDir() {
+ return fmt.Errorf("%s: %s must point to a directory", config, name)
+ }
+ return nil
+}
+
+type userKey struct {
+ ssh.PublicKey
+ privKeyFile string
+}
+
+func newUserKey(pubKeyFile string) (*userKey, error) {
+ userKey := new(userKey)
+ if len(pubKeyFile) > 0 {
+ pubKeyBytes, err := ioutil.ReadFile(pubKeyFile)
+ if err != nil {
+ return nil, errors.New("Failed to read public key")
+ }
+ userKey.PublicKey, _, _, _, err = ssh.ParseAuthorizedKey(pubKeyBytes)
+ if err != nil {
+ return nil, errors.New("Failed to parse authorized key")
+ }
+
+ return userKey, nil
+ }
+
+ key, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ return nil, errors.New("Failed to generate key pair")
+ }
+ userKey.PublicKey, err = ssh.NewPublicKey(key.Public())
+ if err != nil {
+ return nil, errors.New("Failed to extract public key from generated key pair")
+ }
+
+ // To support Inspec calling back to us we need to write
+ // this file down
+ privateKeyDer := x509.MarshalPKCS1PrivateKey(key)
+ privateKeyBlock := pem.Block{
+ Type: "RSA PRIVATE KEY",
+ Headers: nil,
+ Bytes: privateKeyDer,
+ }
+ tf, err := ioutil.TempFile("", "packer-provisioner-inspec.*.key")
+ if err != nil {
+ return nil, errors.New("failed to create temp file for generated key")
+ }
+ _, err = tf.Write(pem.EncodeToMemory(&privateKeyBlock))
+ if err != nil {
+ return nil, errors.New("failed to write private key to temp file")
+ }
+
+ err = tf.Close()
+ if err != nil {
+ return nil, errors.New("failed to close private key temp file")
+ }
+ userKey.privKeyFile = tf.Name()
+
+ return userKey, nil
+}
+
+type signer struct {
+ ssh.Signer
+}
+
+func newSigner(privKeyFile string) (*signer, error) {
+ signer := new(signer)
+
+ if len(privKeyFile) > 0 {
+ privateBytes, err := ioutil.ReadFile(privKeyFile)
+ if err != nil {
+ return nil, errors.New("Failed to load private host key")
+ }
+
+ signer.Signer, err = ssh.ParsePrivateKey(privateBytes)
+ if err != nil {
+ return nil, errors.New("Failed to parse private host key")
+ }
+
+ return signer, nil
+ }
+
+ key, err := rsa.GenerateKey(rand.Reader, 2048)
+ if err != nil {
+ return nil, errors.New("Failed to generate server key pair")
+ }
+
+ signer.Signer, err = ssh.NewSignerFromKey(key)
+ if err != nil {
+ return nil, errors.New("Failed to extract private key from generated key pair")
+ }
+
+ return signer, nil
+}
diff --git a/provisioner/inspec/provisioner_test.go b/provisioner/inspec/provisioner_test.go
new file mode 100644
index 000000000..6857f7004
--- /dev/null
+++ b/provisioner/inspec/provisioner_test.go
@@ -0,0 +1,293 @@
+package inspec
+
+import (
+ "crypto/rand"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path"
+ "strings"
+ "testing"
+
+ "github.com/hashicorp/packer/packer"
+)
+
+// Be sure to remove the InSpec stub file in each test with:
+// defer os.Remove(config["command"].(string))
+func testConfig(t *testing.T) map[string]interface{} {
+ m := make(map[string]interface{})
+ wd, err := os.Getwd()
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ inspec_stub := path.Join(wd, "packer-inspec-stub.sh")
+
+ err = ioutil.WriteFile(inspec_stub, []byte("#!/usr/bin/env bash\necho 2.2.16"), 0777)
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ m["command"] = inspec_stub
+
+ return m
+}
+
+func TestProvisioner_Impl(t *testing.T) {
+ var raw interface{}
+ raw = &Provisioner{}
+ if _, ok := raw.(packer.Provisioner); !ok {
+ t.Fatalf("must be a Provisioner")
+ }
+}
+
+func TestProvisionerPrepare_Defaults(t *testing.T) {
+ var p Provisioner
+ config := testConfig(t)
+ defer os.Remove(config["command"].(string))
+
+ err := p.Prepare(config)
+ if err == nil {
+ t.Fatalf("should have error")
+ }
+
+ hostkey_file, err := ioutil.TempFile("", "hostkey")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ defer os.Remove(hostkey_file.Name())
+
+ publickey_file, err := ioutil.TempFile("", "publickey")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ defer os.Remove(publickey_file.Name())
+
+ profile_file, err := ioutil.TempFile("", "test")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ defer os.Remove(profile_file.Name())
+
+ config["ssh_host_key_file"] = hostkey_file.Name()
+ config["ssh_authorized_key_file"] = publickey_file.Name()
+ config["profile"] = profile_file.Name()
+ err = p.Prepare(config)
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ defer os.Remove(profile_file.Name())
+
+ err = os.Unsetenv("USER")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ err = p.Prepare(config)
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+}
+
+func TestProvisionerPrepare_ProfileFile(t *testing.T) {
+ var p Provisioner
+ config := testConfig(t)
+ defer os.Remove(config["command"].(string))
+
+ hostkey_file, err := ioutil.TempFile("", "hostkey")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ defer os.Remove(hostkey_file.Name())
+
+ publickey_file, err := ioutil.TempFile("", "publickey")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ defer os.Remove(publickey_file.Name())
+
+ config["ssh_host_key_file"] = hostkey_file.Name()
+ config["ssh_authorized_key_file"] = publickey_file.Name()
+
+ err = p.Prepare(config)
+ if err == nil {
+ t.Fatal("should have error")
+ }
+
+ profile_file, err := ioutil.TempFile("", "test")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ defer os.Remove(profile_file.Name())
+
+ config["profile"] = profile_file.Name()
+ err = p.Prepare(config)
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+
+ test_dir, err := ioutil.TempDir("", "test")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ defer os.Remove(test_dir)
+
+ config["profile"] = test_dir
+ err = p.Prepare(config)
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+}
+
+func TestProvisionerPrepare_HostKeyFile(t *testing.T) {
+ var p Provisioner
+ config := testConfig(t)
+ defer os.Remove(config["command"].(string))
+
+ publickey_file, err := ioutil.TempFile("", "publickey")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ defer os.Remove(publickey_file.Name())
+
+ profile_file, err := ioutil.TempFile("", "test")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ defer os.Remove(profile_file.Name())
+
+ filename := make([]byte, 10)
+ n, err := io.ReadFull(rand.Reader, filename)
+ if n != len(filename) || err != nil {
+ t.Fatal("could not create random file name")
+ }
+
+ config["ssh_host_key_file"] = fmt.Sprintf("%x", filename)
+ config["ssh_authorized_key_file"] = publickey_file.Name()
+ config["profile"] = profile_file.Name()
+
+ err = p.Prepare(config)
+ if err == nil {
+ t.Fatal("should error if ssh_host_key_file does not exist")
+ }
+
+ hostkey_file, err := ioutil.TempFile("", "hostkey")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ defer os.Remove(hostkey_file.Name())
+
+ config["ssh_host_key_file"] = hostkey_file.Name()
+ err = p.Prepare(config)
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+}
+
+func TestProvisionerPrepare_AuthorizedKeyFiles(t *testing.T) {
+ var p Provisioner
+ config := testConfig(t)
+ defer os.Remove(config["command"].(string))
+
+ hostkey_file, err := ioutil.TempFile("", "hostkey")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ defer os.Remove(hostkey_file.Name())
+
+ profile_file, err := ioutil.TempFile("", "test")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ defer os.Remove(profile_file.Name())
+
+ filename := make([]byte, 10)
+ n, err := io.ReadFull(rand.Reader, filename)
+ if n != len(filename) || err != nil {
+ t.Fatal("could not create random file name")
+ }
+
+ config["ssh_host_key_file"] = hostkey_file.Name()
+ config["profile"] = profile_file.Name()
+ config["ssh_authorized_key_file"] = fmt.Sprintf("%x", filename)
+
+ err = p.Prepare(config)
+ if err == nil {
+ t.Errorf("should error if ssh_authorized_key_file does not exist")
+ }
+
+ publickey_file, err := ioutil.TempFile("", "publickey")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ defer os.Remove(publickey_file.Name())
+
+ config["ssh_authorized_key_file"] = publickey_file.Name()
+ err = p.Prepare(config)
+ if err != nil {
+ t.Errorf("err: %s", err)
+ }
+}
+
+func TestProvisionerPrepare_LocalPort(t *testing.T) {
+ var p Provisioner
+ config := testConfig(t)
+ defer os.Remove(config["command"].(string))
+
+ hostkey_file, err := ioutil.TempFile("", "hostkey")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ defer os.Remove(hostkey_file.Name())
+
+ publickey_file, err := ioutil.TempFile("", "publickey")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ defer os.Remove(publickey_file.Name())
+
+ profile_file, err := ioutil.TempFile("", "test")
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+ defer os.Remove(profile_file.Name())
+
+ config["ssh_host_key_file"] = hostkey_file.Name()
+ config["ssh_authorized_key_file"] = publickey_file.Name()
+ config["profile"] = profile_file.Name()
+
+ config["local_port"] = uint(65537)
+ err = p.Prepare(config)
+ if err == nil {
+ t.Fatal("should have error")
+ }
+
+ config["local_port"] = uint(22222)
+ err = p.Prepare(config)
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+}
+
+func TestInspecGetVersion(t *testing.T) {
+ if os.Getenv("PACKER_ACC") == "" {
+ t.Skip("This test is only run with PACKER_ACC=1 and it requires InSpec to be installed")
+ }
+
+ var p Provisioner
+ p.config.Command = "inspec exec"
+ err := p.getVersion()
+ if err != nil {
+ t.Fatalf("err: %s", err)
+ }
+}
+
+func TestInspecGetVersionError(t *testing.T) {
+ var p Provisioner
+ p.config.Command = "./test-fixtures/exit1"
+ err := p.getVersion()
+ if err == nil {
+ t.Fatal("Should return error")
+ }
+ if !strings.Contains(err.Error(), "./test-fixtures/exit1 version") {
+ t.Fatal("Error message should include command name")
+ }
+}
diff --git a/provisioner/inspec/test-fixtures/exit1 b/provisioner/inspec/test-fixtures/exit1
new file mode 100755
index 000000000..2bb8d868b
--- /dev/null
+++ b/provisioner/inspec/test-fixtures/exit1
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+exit 1
diff --git a/scripts/build.sh b/scripts/build.sh
index cf44e0713..4ebda8185 100755
--- a/scripts/build.sh
+++ b/scripts/build.sh
@@ -2,8 +2,8 @@
# This script builds the application from source for multiple platforms.
# Determine the arch/os combos we're building for
-ALL_XC_ARCH="386 amd64 arm arm64 ppc64le"
-ALL_XC_OS="linux darwin windows freebsd openbsd solaris"
+ALL_XC_ARCH=${ALL_XC_ARCH:-"386 amd64 arm arm64 ppc64le"}
+ALL_XC_OS=${ALL_XC_OS:-"linux darwin windows freebsd openbsd solaris"}
# Exit immediately if a command fails
set -e
diff --git a/vendor/github.com/digitalocean/godo/CHANGELOG.md b/vendor/github.com/digitalocean/godo/CHANGELOG.md
index 71886a354..e01582aff 100644
--- a/vendor/github.com/digitalocean/godo/CHANGELOG.md
+++ b/vendor/github.com/digitalocean/godo/CHANGELOG.md
@@ -1,5 +1,61 @@
# Change Log
+## [v1.6.0] - 2018-10-16
+
+- #185 Projects support [beta] - @mchitten
+
+## [v1.5.0] - 2018-10-01
+
+- #181 Adding tagging images support - @hugocorbucci
+
+## [v1.4.2] - 2018-08-30
+
+- #178 Allowing creating domain records with weight of 0 - @TFaga
+- #177 Adding `VolumeLimit` to account - @lxfontes
+
+## [v1.4.1] - 2018-08-23
+
+- #176 Fix cdn flush cache API endpoint - @sunny-b
+
+## [v1.4.0] - 2018-08-22
+
+- #175 Add support for Spaces CDN - @sunny-b
+
+## [v1.3.0] - 2018-05-24
+
+- #170 Add support for volume formatting - @adamwg
+
+## [v1.2.0] - 2018-05-08
+
+- #166 Remove support for Go 1.6 - @iheanyi
+- #165 Add support for Let's Encrypt Certificates - @viola
+
+## [v1.1.3] - 2018-03-07
+
+- #156 Handle non-json errors from the API - @aknuds1
+- #158 Update droplet example to use latest instance type - @dan-v
+
+## [v1.1.2] - 2018-03-06
+
+- #157 storage: list volumes should handle only name or only region params - @andrewsykim
+- #154 docs: replace first example with fully-runnable example - @xmudrii
+- #152 Handle flags & tag properties of domain record - @jaymecd
+
+## [v1.1.1] - 2017-09-29
+
+- #151 Following user agent field recommendations - @joonas
+- #148 AsRequest method to create load balancers requests - @lukegb
+
+## [v1.1.0] - 2017-06-06
+
+### Added
+- #145 Add FirewallsService for managing Firewalls with the DigitalOcean API. - @viola
+- #139 Add TTL field to the Domains. - @xmudrii
+
+### Fixed
+- #143 Fix oauth2.NoContext depreciation. - @jbowens
+- #141 Fix DropletActions on tagged resources. - @xmudrii
+
## [v1.0.0] - 2017-03-10
### Added
diff --git a/vendor/github.com/digitalocean/godo/CONTRIBUTING.md b/vendor/github.com/digitalocean/godo/CONTRIBUTING.md
index f27200a7a..7e16f89db 100644
--- a/vendor/github.com/digitalocean/godo/CONTRIBUTING.md
+++ b/vendor/github.com/digitalocean/godo/CONTRIBUTING.md
@@ -12,6 +12,7 @@ Assuming your `$GOPATH` is set up according to your desires, run:
```sh
go get github.com/digitalocean/godo
+go get -u github.com/stretchr/testify/assert
```
## Running tests
diff --git a/vendor/github.com/digitalocean/godo/README.md b/vendor/github.com/digitalocean/godo/README.md
index 4d5cdf83e..ffd3988b7 100644
--- a/vendor/github.com/digitalocean/godo/README.md
+++ b/vendor/github.com/digitalocean/godo/README.md
@@ -27,25 +27,37 @@ at the DigitalOcean Control Panel [Applications Page](https://cloud.digitalocean
You can then use your token to create a new client:
```go
-import "golang.org/x/oauth2"
+package main
+
+import (
+ "context"
+ "github.com/digitalocean/godo"
+ "golang.org/x/oauth2"
+)
+
+const (
+ pat = "mytoken"
+)
-pat := "mytoken"
type TokenSource struct {
- AccessToken string
+ AccessToken string
}
func (t *TokenSource) Token() (*oauth2.Token, error) {
- token := &oauth2.Token{
- AccessToken: t.AccessToken,
- }
- return token, nil
+ token := &oauth2.Token{
+ AccessToken: t.AccessToken,
+ }
+ return token, nil
}
-tokenSource := &TokenSource{
- AccessToken: pat,
+func main() {
+ tokenSource := &TokenSource{
+ AccessToken: pat,
+ }
+
+ oauthClient := oauth2.NewClient(context.Background(), tokenSource)
+ client := godo.NewClient(oauthClient)
}
-oauthClient := oauth2.NewClient(oauth2.NoContext, tokenSource)
-client := godo.NewClient(oauthClient)
```
## Examples
@@ -59,7 +71,7 @@ dropletName := "super-cool-droplet"
createRequest := &godo.DropletCreateRequest{
Name: dropletName,
Region: "nyc3",
- Size: "512mb",
+ Size: "s-1vcpu-1gb",
Image: godo.DropletCreateImage{
Slug: "ubuntu-14-04-x64",
},
diff --git a/vendor/github.com/digitalocean/godo/account.go b/vendor/github.com/digitalocean/godo/account.go
index 18eed9712..7d3e105d3 100644
--- a/vendor/github.com/digitalocean/godo/account.go
+++ b/vendor/github.com/digitalocean/godo/account.go
@@ -1,6 +1,9 @@
package godo
-import "context"
+import (
+ "context"
+ "net/http"
+)
// AccountService is an interface for interfacing with the Account
// endpoints of the DigitalOcean API
@@ -21,6 +24,7 @@ var _ AccountService = &AccountServiceOp{}
type Account struct {
DropletLimit int `json:"droplet_limit,omitempty"`
FloatingIPLimit int `json:"floating_ip_limit,omitempty"`
+ VolumeLimit int `json:"volume_limit,omitempty"`
Email string `json:"email,omitempty"`
UUID string `json:"uuid,omitempty"`
EmailVerified bool `json:"email_verified,omitempty"`
@@ -41,13 +45,13 @@ func (s *AccountServiceOp) Get(ctx context.Context) (*Account, *Response, error)
path := "v2/account"
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(accountRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
diff --git a/vendor/github.com/digitalocean/godo/action.go b/vendor/github.com/digitalocean/godo/action.go
index 9baef2147..67ef3ab8d 100644
--- a/vendor/github.com/digitalocean/godo/action.go
+++ b/vendor/github.com/digitalocean/godo/action.go
@@ -3,6 +3,7 @@ package godo
import (
"context"
"fmt"
+ "net/http"
)
const (
@@ -60,13 +61,13 @@ func (s *ActionsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Action
return nil, nil, err
}
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(actionsRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -84,13 +85,13 @@ func (s *ActionsServiceOp) Get(ctx context.Context, id int) (*Action, *Response,
}
path := fmt.Sprintf("%s/%d", actionsBasePath, id)
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
diff --git a/vendor/github.com/digitalocean/godo/cdn.go b/vendor/github.com/digitalocean/godo/cdn.go
new file mode 100644
index 000000000..217d90e52
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/cdn.go
@@ -0,0 +1,195 @@
+package godo
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+)
+
+const cdnBasePath = "v2/cdn/endpoints"
+
+// CDNService is an interface for managing Spaces CDN with the DigitalOcean API.
+type CDNService interface {
+ List(context.Context, *ListOptions) ([]CDN, *Response, error)
+ Get(context.Context, string) (*CDN, *Response, error)
+ Create(context.Context, *CDNCreateRequest) (*CDN, *Response, error)
+ UpdateTTL(context.Context, string, *CDNUpdateRequest) (*CDN, *Response, error)
+ FlushCache(context.Context, string, *CDNFlushCacheRequest) (*Response, error)
+ Delete(context.Context, string) (*Response, error)
+}
+
+// CDNServiceOp handles communication with the CDN related methods of the
+// DigitalOcean API.
+type CDNServiceOp struct {
+ client *Client
+}
+
+var _ CDNService = &CDNServiceOp{}
+
+// CDN represents a DigitalOcean CDN
+type CDN struct {
+ ID string `json:"id"`
+ Origin string `json:"origin"`
+ Endpoint string `json:"endpoint"`
+ CreatedAt time.Time `json:"created_at"`
+ TTL uint32 `json:"ttl"`
+}
+
+// CDNRoot represents a response from the DigitalOcean API
+type cdnRoot struct {
+ Endpoint *CDN `json:"endpoint"`
+}
+
+type cdnsRoot struct {
+ Endpoints []CDN `json:"endpoints"`
+ Links *Links `json:"links"`
+}
+
+// CDNCreateRequest represents a request to create a CDN.
+type CDNCreateRequest struct {
+ Origin string `json:"origin"`
+ TTL uint32 `json:"ttl"`
+}
+
+// CDNUpdateRequest represents a request to update the ttl of a CDN.
+type CDNUpdateRequest struct {
+ TTL uint32 `json:"ttl"`
+}
+
+// CDNFlushCacheRequest represents a request to flush cache of a CDN.
+type CDNFlushCacheRequest struct {
+ Files []string `json:"files"`
+}
+
+// List all CDN endpoints
+func (c CDNServiceOp) List(ctx context.Context, opt *ListOptions) ([]CDN, *Response, error) {
+ path, err := addOptions(cdnBasePath, opt)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ req, err := c.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(cdnsRoot)
+ resp, err := c.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.Endpoints, resp, err
+}
+
+// Get individual CDN. It requires a non-empty cdn id.
+func (c CDNServiceOp) Get(ctx context.Context, id string) (*CDN, *Response, error) {
+ if len(id) == 0 {
+ return nil, nil, NewArgError("id", "cannot be an empty string")
+ }
+
+ path := fmt.Sprintf("%s/%s", cdnBasePath, id)
+
+ req, err := c.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(cdnRoot)
+ resp, err := c.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Endpoint, resp, err
+}
+
+// Create a new CDN
+func (c CDNServiceOp) Create(ctx context.Context, createRequest *CDNCreateRequest) (*CDN, *Response, error) {
+ if createRequest == nil {
+ return nil, nil, NewArgError("createRequest", "cannot be nil")
+ }
+
+ req, err := c.client.NewRequest(ctx, http.MethodPost, cdnBasePath, createRequest)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(cdnRoot)
+ resp, err := c.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Endpoint, resp, err
+}
+
+// UpdateTTL updates the ttl of individual CDN
+func (c CDNServiceOp) UpdateTTL(ctx context.Context, id string, updateRequest *CDNUpdateRequest) (*CDN, *Response, error) {
+ if updateRequest == nil {
+ return nil, nil, NewArgError("updateRequest", "cannot be nil")
+ }
+
+ if len(id) == 0 {
+ return nil, nil, NewArgError("id", "cannot be an empty string")
+ }
+
+ path := fmt.Sprintf("%s/%s", cdnBasePath, id)
+
+ req, err := c.client.NewRequest(ctx, http.MethodPut, path, updateRequest)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(cdnRoot)
+ resp, err := c.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Endpoint, resp, err
+}
+
+// FlushCache flushes the cache of an individual CDN. Requires a non-empty slice of file paths and/or wildcards
+func (c CDNServiceOp) FlushCache(ctx context.Context, id string, flushCacheRequest *CDNFlushCacheRequest) (*Response, error) {
+ if flushCacheRequest == nil {
+ return nil, NewArgError("flushCacheRequest", "cannot be nil")
+ }
+
+ if len(id) == 0 {
+ return nil, NewArgError("id", "cannot be an empty string")
+ }
+
+ path := fmt.Sprintf("%s/%s/cache", cdnBasePath, id)
+
+ req, err := c.client.NewRequest(ctx, http.MethodDelete, path, flushCacheRequest)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := c.client.Do(ctx, req, nil)
+
+ return resp, err
+}
+
+// Delete an individual CDN
+func (c CDNServiceOp) Delete(ctx context.Context, id string) (*Response, error) {
+ if len(id) == 0 {
+ return nil, NewArgError("id", "cannot be an empty string")
+ }
+
+ path := fmt.Sprintf("%s/%s", cdnBasePath, id)
+
+ req, err := c.client.NewRequest(ctx, http.MethodDelete, path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ resp, err := c.client.Do(ctx, req, nil)
+
+ return resp, err
+}
diff --git a/vendor/github.com/digitalocean/godo/certificates.go b/vendor/github.com/digitalocean/godo/certificates.go
index b48e80ec3..cbc3e16ae 100644
--- a/vendor/github.com/digitalocean/godo/certificates.go
+++ b/vendor/github.com/digitalocean/godo/certificates.go
@@ -2,6 +2,7 @@ package godo
import (
"context"
+ "net/http"
"path"
)
@@ -18,19 +19,24 @@ type CertificatesService interface {
// Certificate represents a DigitalOcean certificate configuration.
type Certificate struct {
- ID string `json:"id,omitempty"`
- Name string `json:"name,omitempty"`
- NotAfter string `json:"not_after,omitempty"`
- SHA1Fingerprint string `json:"sha1_fingerprint,omitempty"`
- Created string `json:"created_at,omitempty"`
+ ID string `json:"id,omitempty"`
+ Name string `json:"name,omitempty"`
+ DNSNames []string `json:"dns_names,omitempty"`
+ NotAfter string `json:"not_after,omitempty"`
+ SHA1Fingerprint string `json:"sha1_fingerprint,omitempty"`
+ Created string `json:"created_at,omitempty"`
+ State string `json:"state,omitempty"`
+ Type string `json:"type,omitempty"`
}
// CertificateRequest represents configuration for a new certificate.
type CertificateRequest struct {
- Name string `json:"name,omitempty"`
- PrivateKey string `json:"private_key,omitempty"`
- LeafCertificate string `json:"leaf_certificate,omitempty"`
- CertificateChain string `json:"certificate_chain,omitempty"`
+ Name string `json:"name,omitempty"`
+ DNSNames []string `json:"dns_names,omitempty"`
+ PrivateKey string `json:"private_key,omitempty"`
+ LeafCertificate string `json:"leaf_certificate,omitempty"`
+ CertificateChain string `json:"certificate_chain,omitempty"`
+ Type string `json:"type,omitempty"`
}
type certificateRoot struct {
@@ -53,13 +59,13 @@ var _ CertificatesService = &CertificatesServiceOp{}
func (c *CertificatesServiceOp) Get(ctx context.Context, cID string) (*Certificate, *Response, error) {
urlStr := path.Join(certificatesBasePath, cID)
- req, err := c.client.NewRequest(ctx, "GET", urlStr, nil)
+ req, err := c.client.NewRequest(ctx, http.MethodGet, urlStr, nil)
if err != nil {
return nil, nil, err
}
root := new(certificateRoot)
- resp, err := c.client.Do(req, root)
+ resp, err := c.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -74,13 +80,13 @@ func (c *CertificatesServiceOp) List(ctx context.Context, opt *ListOptions) ([]C
return nil, nil, err
}
- req, err := c.client.NewRequest(ctx, "GET", urlStr, nil)
+ req, err := c.client.NewRequest(ctx, http.MethodGet, urlStr, nil)
if err != nil {
return nil, nil, err
}
root := new(certificatesRoot)
- resp, err := c.client.Do(req, root)
+ resp, err := c.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -93,13 +99,13 @@ func (c *CertificatesServiceOp) List(ctx context.Context, opt *ListOptions) ([]C
// Create a new certificate with provided configuration.
func (c *CertificatesServiceOp) Create(ctx context.Context, cr *CertificateRequest) (*Certificate, *Response, error) {
- req, err := c.client.NewRequest(ctx, "POST", certificatesBasePath, cr)
+ req, err := c.client.NewRequest(ctx, http.MethodPost, certificatesBasePath, cr)
if err != nil {
return nil, nil, err
}
root := new(certificateRoot)
- resp, err := c.client.Do(req, root)
+ resp, err := c.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -111,10 +117,10 @@ func (c *CertificatesServiceOp) Create(ctx context.Context, cr *CertificateReque
func (c *CertificatesServiceOp) Delete(ctx context.Context, cID string) (*Response, error) {
urlStr := path.Join(certificatesBasePath, cID)
- req, err := c.client.NewRequest(ctx, "DELETE", urlStr, nil)
+ req, err := c.client.NewRequest(ctx, http.MethodDelete, urlStr, nil)
if err != nil {
return nil, err
}
- return c.client.Do(req, nil)
+ return c.client.Do(ctx, req, nil)
}
diff --git a/vendor/github.com/digitalocean/godo/domains.go b/vendor/github.com/digitalocean/godo/domains.go
index fdc2a84c6..de266512e 100644
--- a/vendor/github.com/digitalocean/godo/domains.go
+++ b/vendor/github.com/digitalocean/godo/domains.go
@@ -3,6 +3,7 @@ package godo
import (
"context"
"fmt"
+ "net/http"
)
const domainsBasePath = "v2/domains"
@@ -51,7 +52,7 @@ type domainsRoot struct {
// DomainCreateRequest respresents a request to create a domain.
type DomainCreateRequest struct {
Name string `json:"name"`
- IPAddress string `json:"ip_address"`
+ IPAddress string `json:"ip_address,omitempty"`
}
// DomainRecordRoot is the root of an individual Domain Record response
@@ -71,9 +72,12 @@ type DomainRecord struct {
Type string `json:"type,omitempty"`
Name string `json:"name,omitempty"`
Data string `json:"data,omitempty"`
- Priority int `json:"priority,omitempty"`
+ Priority int `json:"priority"`
Port int `json:"port,omitempty"`
- Weight int `json:"weight,omitempty"`
+ TTL int `json:"ttl,omitempty"`
+ Weight int `json:"weight"`
+ Flags int `json:"flags"`
+ Tag string `json:"tag,omitempty"`
}
// DomainRecordEditRequest represents a request to update a domain record.
@@ -81,15 +85,22 @@ type DomainRecordEditRequest struct {
Type string `json:"type,omitempty"`
Name string `json:"name,omitempty"`
Data string `json:"data,omitempty"`
- Priority int `json:"priority,omitempty"`
+ Priority int `json:"priority"`
Port int `json:"port,omitempty"`
- Weight int `json:"weight,omitempty"`
+ TTL int `json:"ttl,omitempty"`
+ Weight int `json:"weight"`
+ Flags int `json:"flags"`
+ Tag string `json:"tag,omitempty"`
}
func (d Domain) String() string {
return Stringify(d)
}
+func (d Domain) URN() string {
+ return ToURN("Domain", d.Name)
+}
+
// List all domains.
func (s DomainsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Domain, *Response, error) {
path := domainsBasePath
@@ -98,13 +109,13 @@ func (s DomainsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Domain,
return nil, nil, err
}
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(domainsRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -123,13 +134,13 @@ func (s *DomainsServiceOp) Get(ctx context.Context, name string) (*Domain, *Resp
path := fmt.Sprintf("%s/%s", domainsBasePath, name)
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(domainRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -145,13 +156,13 @@ func (s *DomainsServiceOp) Create(ctx context.Context, createRequest *DomainCrea
path := domainsBasePath
- req, err := s.client.NewRequest(ctx, "POST", path, createRequest)
+ req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)
if err != nil {
return nil, nil, err
}
root := new(domainRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -166,12 +177,12 @@ func (s *DomainsServiceOp) Delete(ctx context.Context, name string) (*Response,
path := fmt.Sprintf("%s/%s", domainsBasePath, name)
- req, err := s.client.NewRequest(ctx, "DELETE", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
if err != nil {
return nil, err
}
- resp, err := s.client.Do(req, nil)
+ resp, err := s.client.Do(ctx, req, nil)
return resp, err
}
@@ -198,13 +209,13 @@ func (s *DomainsServiceOp) Records(ctx context.Context, domain string, opt *List
return nil, nil, err
}
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(domainRecordsRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -227,13 +238,13 @@ func (s *DomainsServiceOp) Record(ctx context.Context, domain string, id int) (*
path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
record := new(domainRecordRoot)
- resp, err := s.client.Do(req, record)
+ resp, err := s.client.Do(ctx, req, record)
if err != nil {
return nil, resp, err
}
@@ -253,12 +264,12 @@ func (s *DomainsServiceOp) DeleteRecord(ctx context.Context, domain string, id i
path := fmt.Sprintf("%s/%s/records/%d", domainsBasePath, domain, id)
- req, err := s.client.NewRequest(ctx, "DELETE", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
if err != nil {
return nil, err
}
- resp, err := s.client.Do(req, nil)
+ resp, err := s.client.Do(ctx, req, nil)
return resp, err
}
@@ -289,7 +300,7 @@ func (s *DomainsServiceOp) EditRecord(ctx context.Context,
}
d := new(DomainRecord)
- resp, err := s.client.Do(req, d)
+ resp, err := s.client.Do(ctx, req, d)
if err != nil {
return nil, resp, err
}
@@ -310,14 +321,14 @@ func (s *DomainsServiceOp) CreateRecord(ctx context.Context,
}
path := fmt.Sprintf("%s/%s/records", domainsBasePath, domain)
- req, err := s.client.NewRequest(ctx, "POST", path, createRequest)
+ req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)
if err != nil {
return nil, nil, err
}
d := new(domainRecordRoot)
- resp, err := s.client.Do(req, d)
+ resp, err := s.client.Do(ctx, req, d)
if err != nil {
return nil, resp, err
}
diff --git a/vendor/github.com/digitalocean/godo/droplet_actions.go b/vendor/github.com/digitalocean/godo/droplet_actions.go
index cd8b94e2f..ddeacfc86 100644
--- a/vendor/github.com/digitalocean/godo/droplet_actions.go
+++ b/vendor/github.com/digitalocean/godo/droplet_actions.go
@@ -3,6 +3,7 @@ package godo
import (
"context"
"fmt"
+ "net/http"
"net/url"
)
@@ -14,32 +15,31 @@ type ActionRequest map[string]interface{}
// See: https://developers.digitalocean.com/documentation/v2#droplet-actions
type DropletActionsService interface {
Shutdown(context.Context, int) (*Action, *Response, error)
- ShutdownByTag(context.Context, string) (*Action, *Response, error)
+ ShutdownByTag(context.Context, string) ([]Action, *Response, error)
PowerOff(context.Context, int) (*Action, *Response, error)
- PowerOffByTag(context.Context, string) (*Action, *Response, error)
+ PowerOffByTag(context.Context, string) ([]Action, *Response, error)
PowerOn(context.Context, int) (*Action, *Response, error)
- PowerOnByTag(context.Context, string) (*Action, *Response, error)
+ PowerOnByTag(context.Context, string) ([]Action, *Response, error)
PowerCycle(context.Context, int) (*Action, *Response, error)
- PowerCycleByTag(context.Context, string) (*Action, *Response, error)
+ PowerCycleByTag(context.Context, string) ([]Action, *Response, error)
Reboot(context.Context, int) (*Action, *Response, error)
Restore(context.Context, int, int) (*Action, *Response, error)
Resize(context.Context, int, string, bool) (*Action, *Response, error)
Rename(context.Context, int, string) (*Action, *Response, error)
Snapshot(context.Context, int, string) (*Action, *Response, error)
- SnapshotByTag(context.Context, string, string) (*Action, *Response, error)
+ SnapshotByTag(context.Context, string, string) ([]Action, *Response, error)
EnableBackups(context.Context, int) (*Action, *Response, error)
- EnableBackupsByTag(context.Context, string) (*Action, *Response, error)
+ EnableBackupsByTag(context.Context, string) ([]Action, *Response, error)
DisableBackups(context.Context, int) (*Action, *Response, error)
- DisableBackupsByTag(context.Context, string) (*Action, *Response, error)
+ DisableBackupsByTag(context.Context, string) ([]Action, *Response, error)
PasswordReset(context.Context, int) (*Action, *Response, error)
RebuildByImageID(context.Context, int, int) (*Action, *Response, error)
RebuildByImageSlug(context.Context, int, string) (*Action, *Response, error)
ChangeKernel(context.Context, int, int) (*Action, *Response, error)
EnableIPv6(context.Context, int) (*Action, *Response, error)
- EnableIPv6ByTag(context.Context, string) (*Action, *Response, error)
+ EnableIPv6ByTag(context.Context, string) ([]Action, *Response, error)
EnablePrivateNetworking(context.Context, int) (*Action, *Response, error)
- EnablePrivateNetworkingByTag(context.Context, string) (*Action, *Response, error)
- Upgrade(context.Context, int) (*Action, *Response, error)
+ EnablePrivateNetworkingByTag(context.Context, string) ([]Action, *Response, error)
Get(context.Context, int, int) (*Action, *Response, error)
GetByURI(context.Context, string) (*Action, *Response, error)
}
@@ -59,7 +59,7 @@ func (s *DropletActionsServiceOp) Shutdown(ctx context.Context, id int) (*Action
}
// ShutdownByTag shuts down Droplets matched by a Tag.
-func (s *DropletActionsServiceOp) ShutdownByTag(ctx context.Context, tag string) (*Action, *Response, error) {
+func (s *DropletActionsServiceOp) ShutdownByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
request := &ActionRequest{"type": "shutdown"}
return s.doActionByTag(ctx, tag, request)
}
@@ -71,7 +71,7 @@ func (s *DropletActionsServiceOp) PowerOff(ctx context.Context, id int) (*Action
}
// PowerOffByTag powers off Droplets matched by a Tag.
-func (s *DropletActionsServiceOp) PowerOffByTag(ctx context.Context, tag string) (*Action, *Response, error) {
+func (s *DropletActionsServiceOp) PowerOffByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
request := &ActionRequest{"type": "power_off"}
return s.doActionByTag(ctx, tag, request)
}
@@ -83,7 +83,7 @@ func (s *DropletActionsServiceOp) PowerOn(ctx context.Context, id int) (*Action,
}
// PowerOnByTag powers on Droplets matched by a Tag.
-func (s *DropletActionsServiceOp) PowerOnByTag(ctx context.Context, tag string) (*Action, *Response, error) {
+func (s *DropletActionsServiceOp) PowerOnByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
request := &ActionRequest{"type": "power_on"}
return s.doActionByTag(ctx, tag, request)
}
@@ -95,7 +95,7 @@ func (s *DropletActionsServiceOp) PowerCycle(ctx context.Context, id int) (*Acti
}
// PowerCycleByTag power cycles Droplets matched by a Tag.
-func (s *DropletActionsServiceOp) PowerCycleByTag(ctx context.Context, tag string) (*Action, *Response, error) {
+func (s *DropletActionsServiceOp) PowerCycleByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
request := &ActionRequest{"type": "power_cycle"}
return s.doActionByTag(ctx, tag, request)
}
@@ -148,7 +148,7 @@ func (s *DropletActionsServiceOp) Snapshot(ctx context.Context, id int, name str
}
// SnapshotByTag snapshots Droplets matched by a Tag.
-func (s *DropletActionsServiceOp) SnapshotByTag(ctx context.Context, tag string, name string) (*Action, *Response, error) {
+func (s *DropletActionsServiceOp) SnapshotByTag(ctx context.Context, tag string, name string) ([]Action, *Response, error) {
requestType := "snapshot"
request := &ActionRequest{
"type": requestType,
@@ -164,7 +164,7 @@ func (s *DropletActionsServiceOp) EnableBackups(ctx context.Context, id int) (*A
}
// EnableBackupsByTag enables backups for Droplets matched by a Tag.
-func (s *DropletActionsServiceOp) EnableBackupsByTag(ctx context.Context, tag string) (*Action, *Response, error) {
+func (s *DropletActionsServiceOp) EnableBackupsByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
request := &ActionRequest{"type": "enable_backups"}
return s.doActionByTag(ctx, tag, request)
}
@@ -176,7 +176,7 @@ func (s *DropletActionsServiceOp) DisableBackups(ctx context.Context, id int) (*
}
// DisableBackupsByTag disables backups for Droplet matched by a Tag.
-func (s *DropletActionsServiceOp) DisableBackupsByTag(ctx context.Context, tag string) (*Action, *Response, error) {
+func (s *DropletActionsServiceOp) DisableBackupsByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
request := &ActionRequest{"type": "disable_backups"}
return s.doActionByTag(ctx, tag, request)
}
@@ -212,7 +212,7 @@ func (s *DropletActionsServiceOp) EnableIPv6(ctx context.Context, id int) (*Acti
}
// EnableIPv6ByTag enables IPv6 for Droplets matched by a Tag.
-func (s *DropletActionsServiceOp) EnableIPv6ByTag(ctx context.Context, tag string) (*Action, *Response, error) {
+func (s *DropletActionsServiceOp) EnableIPv6ByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
request := &ActionRequest{"type": "enable_ipv6"}
return s.doActionByTag(ctx, tag, request)
}
@@ -224,17 +224,11 @@ func (s *DropletActionsServiceOp) EnablePrivateNetworking(ctx context.Context, i
}
// EnablePrivateNetworkingByTag enables private networking for Droplets matched by a Tag.
-func (s *DropletActionsServiceOp) EnablePrivateNetworkingByTag(ctx context.Context, tag string) (*Action, *Response, error) {
+func (s *DropletActionsServiceOp) EnablePrivateNetworkingByTag(ctx context.Context, tag string) ([]Action, *Response, error) {
request := &ActionRequest{"type": "enable_private_networking"}
return s.doActionByTag(ctx, tag, request)
}
-// Upgrade a Droplet.
-func (s *DropletActionsServiceOp) Upgrade(ctx context.Context, id int) (*Action, *Response, error) {
- request := &ActionRequest{"type": "upgrade"}
- return s.doAction(ctx, id, request)
-}
-
func (s *DropletActionsServiceOp) doAction(ctx context.Context, id int, request *ActionRequest) (*Action, *Response, error) {
if id < 1 {
return nil, nil, NewArgError("id", "cannot be less than 1")
@@ -246,13 +240,13 @@ func (s *DropletActionsServiceOp) doAction(ctx context.Context, id int, request
path := dropletActionPath(id)
- req, err := s.client.NewRequest(ctx, "POST", path, request)
+ req, err := s.client.NewRequest(ctx, http.MethodPost, path, request)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -260,7 +254,7 @@ func (s *DropletActionsServiceOp) doAction(ctx context.Context, id int, request
return root.Event, resp, err
}
-func (s *DropletActionsServiceOp) doActionByTag(ctx context.Context, tag string, request *ActionRequest) (*Action, *Response, error) {
+func (s *DropletActionsServiceOp) doActionByTag(ctx context.Context, tag string, request *ActionRequest) ([]Action, *Response, error) {
if tag == "" {
return nil, nil, NewArgError("tag", "cannot be empty")
}
@@ -271,18 +265,18 @@ func (s *DropletActionsServiceOp) doActionByTag(ctx context.Context, tag string,
path := dropletActionPathByTag(tag)
- req, err := s.client.NewRequest(ctx, "POST", path, request)
+ req, err := s.client.NewRequest(ctx, http.MethodPost, path, request)
if err != nil {
return nil, nil, err
}
- root := new(actionRoot)
- resp, err := s.client.Do(req, root)
+ root := new(actionsRoot)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
- return root.Event, resp, err
+ return root.Actions, resp, err
}
// Get an action for a particular Droplet by id.
@@ -311,13 +305,13 @@ func (s *DropletActionsServiceOp) GetByURI(ctx context.Context, rawurl string) (
}
func (s *DropletActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) {
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
diff --git a/vendor/github.com/digitalocean/godo/droplets.go b/vendor/github.com/digitalocean/godo/droplets.go
index b145d9805..ab508f1c0 100644
--- a/vendor/github.com/digitalocean/godo/droplets.go
+++ b/vendor/github.com/digitalocean/godo/droplets.go
@@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"fmt"
+ "net/http"
)
const dropletBasePath = "v2/droplets"
@@ -124,6 +125,10 @@ func (d Droplet) String() string {
return Stringify(d)
}
+func (d Droplet) URN() string {
+ return ToURN("Droplet", d.ID)
+}
+
// DropletRoot represents a Droplet root
type dropletRoot struct {
Droplet *Droplet `json:"droplet"`
@@ -274,13 +279,13 @@ func (n NetworkV6) String() string {
// Performs a list request given a path.
func (s *DropletsServiceOp) list(ctx context.Context, path string) ([]Droplet, *Response, error) {
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(dropletsRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -321,13 +326,13 @@ func (s *DropletsServiceOp) Get(ctx context.Context, dropletID int) (*Droplet, *
path := fmt.Sprintf("%s/%d", dropletBasePath, dropletID)
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(dropletRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -343,13 +348,13 @@ func (s *DropletsServiceOp) Create(ctx context.Context, createRequest *DropletCr
path := dropletBasePath
- req, err := s.client.NewRequest(ctx, "POST", path, createRequest)
+ req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)
if err != nil {
return nil, nil, err
}
root := new(dropletRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -368,13 +373,13 @@ func (s *DropletsServiceOp) CreateMultiple(ctx context.Context, createRequest *D
path := dropletBasePath
- req, err := s.client.NewRequest(ctx, "POST", path, createRequest)
+ req, err := s.client.NewRequest(ctx, http.MethodPost, path, createRequest)
if err != nil {
return nil, nil, err
}
root := new(dropletsRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -387,12 +392,12 @@ func (s *DropletsServiceOp) CreateMultiple(ctx context.Context, createRequest *D
// Performs a delete request given a path
func (s *DropletsServiceOp) delete(ctx context.Context, path string) (*Response, error) {
- req, err := s.client.NewRequest(ctx, "DELETE", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
if err != nil {
return nil, err
}
- resp, err := s.client.Do(req, nil)
+ resp, err := s.client.Do(ctx, req, nil)
return resp, err
}
@@ -431,13 +436,13 @@ func (s *DropletsServiceOp) Kernels(ctx context.Context, dropletID int, opt *Lis
return nil, nil, err
}
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(kernelsRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if l := root.Links; l != nil {
resp.Links = l
}
@@ -457,13 +462,13 @@ func (s *DropletsServiceOp) Actions(ctx context.Context, dropletID int, opt *Lis
return nil, nil, err
}
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(actionsRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -486,13 +491,13 @@ func (s *DropletsServiceOp) Backups(ctx context.Context, dropletID int, opt *Lis
return nil, nil, err
}
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(backupsRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -515,13 +520,13 @@ func (s *DropletsServiceOp) Snapshots(ctx context.Context, dropletID int, opt *L
return nil, nil, err
}
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(dropletSnapshotsRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -540,13 +545,13 @@ func (s *DropletsServiceOp) Neighbors(ctx context.Context, dropletID int) ([]Dro
path := fmt.Sprintf("%s/%d/neighbors", dropletBasePath, dropletID)
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(dropletsRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
diff --git a/vendor/github.com/digitalocean/godo/firewalls.go b/vendor/github.com/digitalocean/godo/firewalls.go
new file mode 100644
index 000000000..c28cac03b
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/firewalls.go
@@ -0,0 +1,267 @@
+package godo
+
+import (
+ "context"
+ "net/http"
+ "path"
+ "strconv"
+)
+
+const firewallsBasePath = "/v2/firewalls"
+
+// FirewallsService is an interface for managing Firewalls with the DigitalOcean API.
+// See: https://developers.digitalocean.com/documentation/documentation/v2/#firewalls
+type FirewallsService interface {
+ Get(context.Context, string) (*Firewall, *Response, error)
+ Create(context.Context, *FirewallRequest) (*Firewall, *Response, error)
+ Update(context.Context, string, *FirewallRequest) (*Firewall, *Response, error)
+ Delete(context.Context, string) (*Response, error)
+ List(context.Context, *ListOptions) ([]Firewall, *Response, error)
+ ListByDroplet(context.Context, int, *ListOptions) ([]Firewall, *Response, error)
+ AddDroplets(context.Context, string, ...int) (*Response, error)
+ RemoveDroplets(context.Context, string, ...int) (*Response, error)
+ AddTags(context.Context, string, ...string) (*Response, error)
+ RemoveTags(context.Context, string, ...string) (*Response, error)
+ AddRules(context.Context, string, *FirewallRulesRequest) (*Response, error)
+ RemoveRules(context.Context, string, *FirewallRulesRequest) (*Response, error)
+}
+
+// FirewallsServiceOp handles communication with Firewalls methods of the DigitalOcean API.
+type FirewallsServiceOp struct {
+ client *Client
+}
+
+// Firewall represents a DigitalOcean Firewall configuration.
+type Firewall struct {
+ ID string `json:"id"`
+ Name string `json:"name"`
+ Status string `json:"status"`
+ InboundRules []InboundRule `json:"inbound_rules"`
+ OutboundRules []OutboundRule `json:"outbound_rules"`
+ DropletIDs []int `json:"droplet_ids"`
+ Tags []string `json:"tags"`
+ Created string `json:"created_at"`
+ PendingChanges []PendingChange `json:"pending_changes"`
+}
+
+// String creates a human-readable description of a Firewall.
+func (fw Firewall) String() string {
+ return Stringify(fw)
+}
+
+func (fw Firewall) URN() string {
+ return ToURN("Firewall", fw.ID)
+}
+
+// FirewallRequest represents the configuration to be applied to an existing or a new Firewall.
+type FirewallRequest struct {
+ Name string `json:"name"`
+ InboundRules []InboundRule `json:"inbound_rules"`
+ OutboundRules []OutboundRule `json:"outbound_rules"`
+ DropletIDs []int `json:"droplet_ids"`
+ Tags []string `json:"tags"`
+}
+
+// FirewallRulesRequest represents rules configuration to be applied to an existing Firewall.
+type FirewallRulesRequest struct {
+ InboundRules []InboundRule `json:"inbound_rules"`
+ OutboundRules []OutboundRule `json:"outbound_rules"`
+}
+
+// InboundRule represents a DigitalOcean Firewall inbound rule.
+type InboundRule struct {
+ Protocol string `json:"protocol,omitempty"`
+ PortRange string `json:"ports,omitempty"`
+ Sources *Sources `json:"sources"`
+}
+
+// OutboundRule represents a DigitalOcean Firewall outbound rule.
+type OutboundRule struct {
+ Protocol string `json:"protocol,omitempty"`
+ PortRange string `json:"ports,omitempty"`
+ Destinations *Destinations `json:"destinations"`
+}
+
+// Sources represents a DigitalOcean Firewall InboundRule sources.
+type Sources struct {
+ Addresses []string `json:"addresses,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ DropletIDs []int `json:"droplet_ids,omitempty"`
+ LoadBalancerUIDs []string `json:"load_balancer_uids,omitempty"`
+}
+
+// PendingChange represents a DigitalOcean Firewall status details.
+type PendingChange struct {
+ DropletID int `json:"droplet_id,omitempty"`
+ Removing bool `json:"removing,omitempty"`
+ Status string `json:"status,omitempty"`
+}
+
+// Destinations represents a DigitalOcean Firewall OutboundRule destinations.
+type Destinations struct {
+ Addresses []string `json:"addresses,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ DropletIDs []int `json:"droplet_ids,omitempty"`
+ LoadBalancerUIDs []string `json:"load_balancer_uids,omitempty"`
+}
+
+var _ FirewallsService = &FirewallsServiceOp{}
+
+// Get an existing Firewall by its identifier.
+func (fw *FirewallsServiceOp) Get(ctx context.Context, fID string) (*Firewall, *Response, error) {
+ path := path.Join(firewallsBasePath, fID)
+
+ req, err := fw.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(firewallRoot)
+ resp, err := fw.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Firewall, resp, err
+}
+
+// Create a new Firewall with a given configuration.
+func (fw *FirewallsServiceOp) Create(ctx context.Context, fr *FirewallRequest) (*Firewall, *Response, error) {
+ req, err := fw.client.NewRequest(ctx, http.MethodPost, firewallsBasePath, fr)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(firewallRoot)
+ resp, err := fw.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Firewall, resp, err
+}
+
+// Update an existing Firewall with new configuration.
+func (fw *FirewallsServiceOp) Update(ctx context.Context, fID string, fr *FirewallRequest) (*Firewall, *Response, error) {
+ path := path.Join(firewallsBasePath, fID)
+
+ req, err := fw.client.NewRequest(ctx, "PUT", path, fr)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(firewallRoot)
+ resp, err := fw.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Firewall, resp, err
+}
+
+// Delete a Firewall by its identifier.
+func (fw *FirewallsServiceOp) Delete(ctx context.Context, fID string) (*Response, error) {
+ path := path.Join(firewallsBasePath, fID)
+ return fw.createAndDoReq(ctx, http.MethodDelete, path, nil)
+}
+
+// List Firewalls.
+func (fw *FirewallsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Firewall, *Response, error) {
+ path, err := addOptions(firewallsBasePath, opt)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return fw.listHelper(ctx, path)
+}
+
+// ListByDroplet Firewalls.
+func (fw *FirewallsServiceOp) ListByDroplet(ctx context.Context, dID int, opt *ListOptions) ([]Firewall, *Response, error) {
+ basePath := path.Join(dropletBasePath, strconv.Itoa(dID), "firewalls")
+ path, err := addOptions(basePath, opt)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ return fw.listHelper(ctx, path)
+}
+
+// AddDroplets to a Firewall.
+func (fw *FirewallsServiceOp) AddDroplets(ctx context.Context, fID string, dropletIDs ...int) (*Response, error) {
+ path := path.Join(firewallsBasePath, fID, "droplets")
+ return fw.createAndDoReq(ctx, http.MethodPost, path, &dropletsRequest{IDs: dropletIDs})
+}
+
+// RemoveDroplets from a Firewall.
+func (fw *FirewallsServiceOp) RemoveDroplets(ctx context.Context, fID string, dropletIDs ...int) (*Response, error) {
+ path := path.Join(firewallsBasePath, fID, "droplets")
+ return fw.createAndDoReq(ctx, http.MethodDelete, path, &dropletsRequest{IDs: dropletIDs})
+}
+
+// AddTags to a Firewall.
+func (fw *FirewallsServiceOp) AddTags(ctx context.Context, fID string, tags ...string) (*Response, error) {
+ path := path.Join(firewallsBasePath, fID, "tags")
+ return fw.createAndDoReq(ctx, http.MethodPost, path, &tagsRequest{Tags: tags})
+}
+
+// RemoveTags from a Firewall.
+func (fw *FirewallsServiceOp) RemoveTags(ctx context.Context, fID string, tags ...string) (*Response, error) {
+ path := path.Join(firewallsBasePath, fID, "tags")
+ return fw.createAndDoReq(ctx, http.MethodDelete, path, &tagsRequest{Tags: tags})
+}
+
+// AddRules to a Firewall.
+func (fw *FirewallsServiceOp) AddRules(ctx context.Context, fID string, rr *FirewallRulesRequest) (*Response, error) {
+ path := path.Join(firewallsBasePath, fID, "rules")
+ return fw.createAndDoReq(ctx, http.MethodPost, path, rr)
+}
+
+// RemoveRules from a Firewall.
+func (fw *FirewallsServiceOp) RemoveRules(ctx context.Context, fID string, rr *FirewallRulesRequest) (*Response, error) {
+ path := path.Join(firewallsBasePath, fID, "rules")
+ return fw.createAndDoReq(ctx, http.MethodDelete, path, rr)
+}
+
+type dropletsRequest struct {
+ IDs []int `json:"droplet_ids"`
+}
+
+type tagsRequest struct {
+ Tags []string `json:"tags"`
+}
+
+type firewallRoot struct {
+ Firewall *Firewall `json:"firewall"`
+}
+
+type firewallsRoot struct {
+ Firewalls []Firewall `json:"firewalls"`
+ Links *Links `json:"links"`
+}
+
+func (fw *FirewallsServiceOp) createAndDoReq(ctx context.Context, method, path string, v interface{}) (*Response, error) {
+ req, err := fw.client.NewRequest(ctx, method, path, v)
+ if err != nil {
+ return nil, err
+ }
+
+ return fw.client.Do(ctx, req, nil)
+}
+
+func (fw *FirewallsServiceOp) listHelper(ctx context.Context, path string) ([]Firewall, *Response, error) {
+ req, err := fw.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(firewallsRoot)
+ resp, err := fw.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.Firewalls, resp, err
+}
diff --git a/vendor/github.com/digitalocean/godo/floating_ips.go b/vendor/github.com/digitalocean/godo/floating_ips.go
index 13e983261..4545e9037 100644
--- a/vendor/github.com/digitalocean/godo/floating_ips.go
+++ b/vendor/github.com/digitalocean/godo/floating_ips.go
@@ -3,6 +3,7 @@ package godo
import (
"context"
"fmt"
+ "net/http"
)
const floatingBasePath = "v2/floating_ips"
@@ -36,6 +37,10 @@ func (f FloatingIP) String() string {
return Stringify(f)
}
+func (f FloatingIP) URN() string {
+ return ToURN("FloatingIP", f.IP)
+}
+
type floatingIPsRoot struct {
FloatingIPs []FloatingIP `json:"floating_ips"`
Links *Links `json:"links"`
@@ -62,13 +67,13 @@ func (f *FloatingIPsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Fl
return nil, nil, err
}
- req, err := f.client.NewRequest(ctx, "GET", path, nil)
+ req, err := f.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(floatingIPsRoot)
- resp, err := f.client.Do(req, root)
+ resp, err := f.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -83,13 +88,13 @@ func (f *FloatingIPsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Fl
func (f *FloatingIPsServiceOp) Get(ctx context.Context, ip string) (*FloatingIP, *Response, error) {
path := fmt.Sprintf("%s/%s", floatingBasePath, ip)
- req, err := f.client.NewRequest(ctx, "GET", path, nil)
+ req, err := f.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(floatingIPRoot)
- resp, err := f.client.Do(req, root)
+ resp, err := f.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -102,13 +107,13 @@ func (f *FloatingIPsServiceOp) Get(ctx context.Context, ip string) (*FloatingIP,
func (f *FloatingIPsServiceOp) Create(ctx context.Context, createRequest *FloatingIPCreateRequest) (*FloatingIP, *Response, error) {
path := floatingBasePath
- req, err := f.client.NewRequest(ctx, "POST", path, createRequest)
+ req, err := f.client.NewRequest(ctx, http.MethodPost, path, createRequest)
if err != nil {
return nil, nil, err
}
root := new(floatingIPRoot)
- resp, err := f.client.Do(req, root)
+ resp, err := f.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -123,12 +128,12 @@ func (f *FloatingIPsServiceOp) Create(ctx context.Context, createRequest *Floati
func (f *FloatingIPsServiceOp) Delete(ctx context.Context, ip string) (*Response, error) {
path := fmt.Sprintf("%s/%s", floatingBasePath, ip)
- req, err := f.client.NewRequest(ctx, "DELETE", path, nil)
+ req, err := f.client.NewRequest(ctx, http.MethodDelete, path, nil)
if err != nil {
return nil, err
}
- resp, err := f.client.Do(req, nil)
+ resp, err := f.client.Do(ctx, req, nil)
return resp, err
}
diff --git a/vendor/github.com/digitalocean/godo/floating_ips_actions.go b/vendor/github.com/digitalocean/godo/floating_ips_actions.go
index 5afa051d7..74ad279f9 100644
--- a/vendor/github.com/digitalocean/godo/floating_ips_actions.go
+++ b/vendor/github.com/digitalocean/godo/floating_ips_actions.go
@@ -3,6 +3,7 @@ package godo
import (
"context"
"fmt"
+ "net/http"
)
// FloatingIPActionsService is an interface for interfacing with the
@@ -56,13 +57,13 @@ func (s *FloatingIPActionsServiceOp) List(ctx context.Context, ip string, opt *L
func (s *FloatingIPActionsServiceOp) doAction(ctx context.Context, ip string, request *ActionRequest) (*Action, *Response, error) {
path := floatingIPActionPath(ip)
- req, err := s.client.NewRequest(ctx, "POST", path, request)
+ req, err := s.client.NewRequest(ctx, http.MethodPost, path, request)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -71,13 +72,13 @@ func (s *FloatingIPActionsServiceOp) doAction(ctx context.Context, ip string, re
}
func (s *FloatingIPActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) {
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -86,13 +87,13 @@ func (s *FloatingIPActionsServiceOp) get(ctx context.Context, path string) (*Act
}
func (s *FloatingIPActionsServiceOp) list(ctx context.Context, path string) ([]Action, *Response, error) {
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(actionsRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
diff --git a/vendor/github.com/digitalocean/godo/godo.go b/vendor/github.com/digitalocean/godo/godo.go
index 0f9d7a91b..ab4f0884d 100644
--- a/vendor/github.com/digitalocean/godo/godo.go
+++ b/vendor/github.com/digitalocean/godo/godo.go
@@ -18,7 +18,7 @@ import (
)
const (
- libraryVersion = "1.0.0"
+ libraryVersion = "1.6.0"
defaultBaseURL = "https://api.digitalocean.com/"
userAgent = "godo/" + libraryVersion
mediaType = "application/json"
@@ -46,6 +46,7 @@ type Client struct {
// Services used for communicating with the API
Account AccountService
Actions ActionsService
+ CDNs CDNService
Domains DomainsService
Droplets DropletsService
DropletActions DropletActionsService
@@ -62,6 +63,9 @@ type Client struct {
Tags TagsService
LoadBalancers LoadBalancersService
Certificates CertificatesService
+ Firewalls FirewallsService
+ Projects ProjectsService
+ Kubernetes KubernetesService
// Optional function called after every successful request made to the DO APIs
onRequestCompleted RequestCompletionCallback
@@ -156,22 +160,26 @@ func NewClient(httpClient *http.Client) *Client {
c := &Client{client: httpClient, BaseURL: baseURL, UserAgent: userAgent}
c.Account = &AccountServiceOp{client: c}
c.Actions = &ActionsServiceOp{client: c}
+ c.CDNs = &CDNServiceOp{client: c}
+ c.Certificates = &CertificatesServiceOp{client: c}
c.Domains = &DomainsServiceOp{client: c}
c.Droplets = &DropletsServiceOp{client: c}
c.DropletActions = &DropletActionsServiceOp{client: c}
+ c.Firewalls = &FirewallsServiceOp{client: c}
c.FloatingIPs = &FloatingIPsServiceOp{client: c}
c.FloatingIPActions = &FloatingIPActionsServiceOp{client: c}
c.Images = &ImagesServiceOp{client: c}
c.ImageActions = &ImageActionsServiceOp{client: c}
c.Keys = &KeysServiceOp{client: c}
+ c.LoadBalancers = &LoadBalancersServiceOp{client: c}
+ c.Projects = &ProjectsServiceOp{client: c}
c.Regions = &RegionsServiceOp{client: c}
- c.Snapshots = &SnapshotsServiceOp{client: c}
c.Sizes = &SizesServiceOp{client: c}
+ c.Snapshots = &SnapshotsServiceOp{client: c}
c.Storage = &StorageServiceOp{client: c}
c.StorageActions = &StorageActionsServiceOp{client: c}
c.Tags = &TagsServiceOp{client: c}
- c.LoadBalancers = &LoadBalancersServiceOp{client: c}
- c.Certificates = &CertificatesServiceOp{client: c}
+ c.Kubernetes = &KubernetesServiceOp{client: c}
return c
}
@@ -207,7 +215,7 @@ func SetBaseURL(bu string) ClientOpt {
// SetUserAgent is a client option for setting the user agent.
func SetUserAgent(ua string) ClientOpt {
return func(c *Client) error {
- c.UserAgent = fmt.Sprintf("%s+%s", ua, c.UserAgent)
+ c.UserAgent = fmt.Sprintf("%s %s", ua, c.UserAgent)
return nil
}
}
@@ -236,7 +244,6 @@ func (c *Client) NewRequest(ctx context.Context, method, urlStr string, body int
return nil, err
}
- req = req.WithContext(ctx)
req.Header.Add("Content-Type", mediaType)
req.Header.Add("Accept", mediaType)
req.Header.Add("User-Agent", c.UserAgent)
@@ -293,8 +300,8 @@ func (r *Response) populateRate() {
// Do sends an API request and returns the API response. The API response is JSON decoded and stored in the value
// pointed to by v, or returned as an error if an API error has occurred. If v implements the io.Writer interface,
// the raw response will be written to v, without attempting to decode it.
-func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
- resp, err := c.client.Do(req)
+func (c *Client) Do(ctx context.Context, req *http.Request, v interface{}) (*Response, error) {
+ resp, err := DoRequestWithClient(ctx, c.client, req)
if err != nil {
return nil, err
}
@@ -332,6 +339,21 @@ func (c *Client) Do(req *http.Request, v interface{}) (*Response, error) {
return response, err
}
+
+// DoRequest submits an HTTP request.
+func DoRequest(ctx context.Context, req *http.Request) (*http.Response, error) {
+ return DoRequestWithClient(ctx, http.DefaultClient, req)
+}
+
+// DoRequestWithClient submits an HTTP request using the specified client.
+func DoRequestWithClient(
+ ctx context.Context,
+ client *http.Client,
+ req *http.Request) (*http.Response, error) {
+ req = req.WithContext(ctx)
+ return client.Do(req)
+}
+
func (r *ErrorResponse) Error() string {
if r.RequestID != "" {
return fmt.Sprintf("%v %v: %d (request %q) %v",
@@ -354,7 +376,7 @@ func CheckResponse(r *http.Response) error {
if err == nil && len(data) > 0 {
err := json.Unmarshal(data, errorResponse)
if err != nil {
- return err
+ errorResponse.Message = string(data)
}
}
diff --git a/vendor/github.com/digitalocean/godo/image_actions.go b/vendor/github.com/digitalocean/godo/image_actions.go
index c4201c0de..976f7c687 100644
--- a/vendor/github.com/digitalocean/godo/image_actions.go
+++ b/vendor/github.com/digitalocean/godo/image_actions.go
@@ -3,6 +3,7 @@ package godo
import (
"context"
"fmt"
+ "net/http"
)
// ImageActionsService is an interface for interfacing with the image actions
@@ -34,13 +35,13 @@ func (i *ImageActionsServiceOp) Transfer(ctx context.Context, imageID int, trans
path := fmt.Sprintf("v2/images/%d/actions", imageID)
- req, err := i.client.NewRequest(ctx, "POST", path, transferRequest)
+ req, err := i.client.NewRequest(ctx, http.MethodPost, path, transferRequest)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
- resp, err := i.client.Do(req, root)
+ resp, err := i.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -60,13 +61,13 @@ func (i *ImageActionsServiceOp) Convert(ctx context.Context, imageID int) (*Acti
"type": "convert",
}
- req, err := i.client.NewRequest(ctx, "POST", path, convertRequest)
+ req, err := i.client.NewRequest(ctx, http.MethodPost, path, convertRequest)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
- resp, err := i.client.Do(req, root)
+ resp, err := i.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -86,13 +87,13 @@ func (i *ImageActionsServiceOp) Get(ctx context.Context, imageID, actionID int)
path := fmt.Sprintf("v2/images/%d/actions/%d", imageID, actionID)
- req, err := i.client.NewRequest(ctx, "GET", path, nil)
+ req, err := i.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
- resp, err := i.client.Do(req, root)
+ resp, err := i.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
diff --git a/vendor/github.com/digitalocean/godo/images.go b/vendor/github.com/digitalocean/godo/images.go
index c808af6ec..69de4c075 100644
--- a/vendor/github.com/digitalocean/godo/images.go
+++ b/vendor/github.com/digitalocean/godo/images.go
@@ -3,6 +3,7 @@ package godo
import (
"context"
"fmt"
+ "net/http"
)
const imageBasePath = "v2/images"
@@ -15,8 +16,10 @@ type ImagesService interface {
ListDistribution(ctx context.Context, opt *ListOptions) ([]Image, *Response, error)
ListApplication(ctx context.Context, opt *ListOptions) ([]Image, *Response, error)
ListUser(ctx context.Context, opt *ListOptions) ([]Image, *Response, error)
+ ListByTag(ctx context.Context, tag string, opt *ListOptions) ([]Image, *Response, error)
GetByID(context.Context, int) (*Image, *Response, error)
GetBySlug(context.Context, string) (*Image, *Response, error)
+ Create(context.Context, *CustomImageCreateRequest) (*Image, *Response, error)
Update(context.Context, int, *ImageUpdateRequest) (*Image, *Response, error)
Delete(context.Context, int) (*Response, error)
}
@@ -31,15 +34,20 @@ var _ ImagesService = &ImagesServiceOp{}
// Image represents a DigitalOcean Image
type Image struct {
- ID int `json:"id,float64,omitempty"`
- Name string `json:"name,omitempty"`
- Type string `json:"type,omitempty"`
- Distribution string `json:"distribution,omitempty"`
- Slug string `json:"slug,omitempty"`
- Public bool `json:"public,omitempty"`
- Regions []string `json:"regions,omitempty"`
- MinDiskSize int `json:"min_disk_size,omitempty"`
- Created string `json:"created_at,omitempty"`
+ ID int `json:"id,float64,omitempty"`
+ Name string `json:"name,omitempty"`
+ Type string `json:"type,omitempty"`
+ Distribution string `json:"distribution,omitempty"`
+ Slug string `json:"slug,omitempty"`
+ Public bool `json:"public,omitempty"`
+ Regions []string `json:"regions,omitempty"`
+ MinDiskSize int `json:"min_disk_size,omitempty"`
+ SizeGigaBytes float64 `json:"size_gigabytes,omitempty"`
+ Created string `json:"created_at,omitempty"`
+ Description string `json:"description,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ Status string `json:"status,omitempty"`
+ ErrorMessage string `json:"error_message,omitempty"`
}
// ImageUpdateRequest represents a request to update an image.
@@ -47,6 +55,16 @@ type ImageUpdateRequest struct {
Name string `json:"name"`
}
+// CustomImageCreateRequest represents a request to create a custom image.
+type CustomImageCreateRequest struct {
+ Name string `json:"name"`
+ Url string `json:"url"`
+ Region string `json:"region"`
+ Distribution string `json:"distribution,omitempty"`
+ Description string `json:"description,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+}
+
type imageRoot struct {
Image *Image
}
@@ -59,6 +77,7 @@ type imagesRoot struct {
type listImageOptions struct {
Private bool `url:"private,omitempty"`
Type string `url:"type,omitempty"`
+ Tag string `url:"tag_name,omitempty"`
}
func (i Image) String() string {
@@ -88,6 +107,12 @@ func (s *ImagesServiceOp) ListUser(ctx context.Context, opt *ListOptions) ([]Ima
return s.list(ctx, opt, &listOpt)
}
+// ListByTag lists all images with a specific tag applied.
+func (s *ImagesServiceOp) ListByTag(ctx context.Context, tag string, opt *ListOptions) ([]Image, *Response, error) {
+ listOpt := listImageOptions{Tag: tag}
+ return s.list(ctx, opt, &listOpt)
+}
+
// GetByID retrieves an image by id.
func (s *ImagesServiceOp) GetByID(ctx context.Context, imageID int) (*Image, *Response, error) {
if imageID < 1 {
@@ -106,6 +131,25 @@ func (s *ImagesServiceOp) GetBySlug(ctx context.Context, slug string) (*Image, *
return s.get(ctx, interface{}(slug))
}
+func (s *ImagesServiceOp) Create(ctx context.Context, createRequest *CustomImageCreateRequest) (*Image, *Response, error) {
+ if createRequest == nil {
+ return nil, nil, NewArgError("createRequest", "cannot be nil")
+ }
+
+ req, err := s.client.NewRequest(ctx, http.MethodPost, imageBasePath, createRequest)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(imageRoot)
+ resp, err := s.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Image, resp, err
+}
+
// Update an image name.
func (s *ImagesServiceOp) Update(ctx context.Context, imageID int, updateRequest *ImageUpdateRequest) (*Image, *Response, error) {
if imageID < 1 {
@@ -117,13 +161,13 @@ func (s *ImagesServiceOp) Update(ctx context.Context, imageID int, updateRequest
}
path := fmt.Sprintf("%s/%d", imageBasePath, imageID)
- req, err := s.client.NewRequest(ctx, "PUT", path, updateRequest)
+ req, err := s.client.NewRequest(ctx, http.MethodPut, path, updateRequest)
if err != nil {
return nil, nil, err
}
root := new(imageRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -139,12 +183,12 @@ func (s *ImagesServiceOp) Delete(ctx context.Context, imageID int) (*Response, e
path := fmt.Sprintf("%s/%d", imageBasePath, imageID)
- req, err := s.client.NewRequest(ctx, "DELETE", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
if err != nil {
return nil, err
}
- resp, err := s.client.Do(req, nil)
+ resp, err := s.client.Do(ctx, req, nil)
return resp, err
}
@@ -153,13 +197,13 @@ func (s *ImagesServiceOp) Delete(ctx context.Context, imageID int) (*Response, e
func (s *ImagesServiceOp) get(ctx context.Context, ID interface{}) (*Image, *Response, error) {
path := fmt.Sprintf("%s/%v", imageBasePath, ID)
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(imageRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -179,13 +223,13 @@ func (s *ImagesServiceOp) list(ctx context.Context, opt *ListOptions, listOpt *l
return nil, nil, err
}
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(imagesRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
diff --git a/vendor/github.com/digitalocean/godo/keys.go b/vendor/github.com/digitalocean/godo/keys.go
index 7b336c4ce..9695c21f4 100644
--- a/vendor/github.com/digitalocean/godo/keys.go
+++ b/vendor/github.com/digitalocean/godo/keys.go
@@ -3,6 +3,7 @@ package godo
import (
"context"
"fmt"
+ "net/http"
)
const keysBasePath = "v2/account/keys"
@@ -69,13 +70,13 @@ func (s *KeysServiceOp) List(ctx context.Context, opt *ListOptions) ([]Key, *Res
return nil, nil, err
}
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(keysRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -88,13 +89,13 @@ func (s *KeysServiceOp) List(ctx context.Context, opt *ListOptions) ([]Key, *Res
// Performs a get given a path
func (s *KeysServiceOp) get(ctx context.Context, path string) (*Key, *Response, error) {
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(keyRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -128,13 +129,13 @@ func (s *KeysServiceOp) Create(ctx context.Context, createRequest *KeyCreateRequ
return nil, nil, NewArgError("createRequest", "cannot be nil")
}
- req, err := s.client.NewRequest(ctx, "POST", keysBasePath, createRequest)
+ req, err := s.client.NewRequest(ctx, http.MethodPost, keysBasePath, createRequest)
if err != nil {
return nil, nil, err
}
root := new(keyRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -159,7 +160,7 @@ func (s *KeysServiceOp) UpdateByID(ctx context.Context, keyID int, updateRequest
}
root := new(keyRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -184,7 +185,7 @@ func (s *KeysServiceOp) UpdateByFingerprint(ctx context.Context, fingerprint str
}
root := new(keyRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -194,12 +195,12 @@ func (s *KeysServiceOp) UpdateByFingerprint(ctx context.Context, fingerprint str
// Delete key using a path
func (s *KeysServiceOp) delete(ctx context.Context, path string) (*Response, error) {
- req, err := s.client.NewRequest(ctx, "DELETE", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
if err != nil {
return nil, err
}
- resp, err := s.client.Do(req, nil)
+ resp, err := s.client.Do(ctx, req, nil)
return resp, err
}
diff --git a/vendor/github.com/digitalocean/godo/kubernetes.go b/vendor/github.com/digitalocean/godo/kubernetes.go
new file mode 100644
index 000000000..e5769de0e
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/kubernetes.go
@@ -0,0 +1,380 @@
+package godo
+
+import (
+ "bytes"
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+)
+
+const (
+ kubernetesBasePath = "/v2/kubernetes"
+ kubernetesClustersPath = kubernetesBasePath + "/clusters"
+ kubernetesOptionsPath = kubernetesBasePath + "/options"
+)
+
+// KubernetesService is an interface for interfacing with the kubernetes endpoints
+// of the DigitalOcean API.
+// See: https://developers.digitalocean.com/documentation/v2#kubernetes
+type KubernetesService interface {
+ Create(context.Context, *KubernetesClusterCreateRequest) (*KubernetesCluster, *Response, error)
+ Get(context.Context, string) (*KubernetesCluster, *Response, error)
+ GetKubeConfig(context.Context, string) (*KubernetesClusterConfig, *Response, error)
+ List(context.Context, *ListOptions) ([]*KubernetesCluster, *Response, error)
+ Update(context.Context, string, *KubernetesClusterUpdateRequest) (*KubernetesCluster, *Response, error)
+ Delete(context.Context, string) (*Response, error)
+
+ CreateNodePool(ctx context.Context, clusterID string, req *KubernetesNodePoolCreateRequest) (*KubernetesNodePool, *Response, error)
+ GetNodePool(ctx context.Context, clusterID, poolID string) (*KubernetesNodePool, *Response, error)
+ ListNodePools(ctx context.Context, clusterID string, opts *ListOptions) ([]*KubernetesNodePool, *Response, error)
+ UpdateNodePool(ctx context.Context, clusterID, poolID string, req *KubernetesNodePoolUpdateRequest) (*KubernetesNodePool, *Response, error)
+ RecycleNodePoolNodes(ctx context.Context, clusterID, poolID string, req *KubernetesNodePoolRecycleNodesRequest) (*Response, error)
+ DeleteNodePool(ctx context.Context, clusterID, poolID string) (*Response, error)
+
+ GetOptions(context.Context) (*KubernetesOptions, *Response, error)
+}
+
+var _ KubernetesService = &KubernetesServiceOp{}
+
+// KubernetesServiceOp handles communication with Kubernetes methods of the DigitalOcean API.
+type KubernetesServiceOp struct {
+ client *Client
+}
+
+// KubernetesClusterCreateRequest represents a request to create a Kubernetes cluster.
+type KubernetesClusterCreateRequest struct {
+ Name string `json:"name,omitempty"`
+ RegionSlug string `json:"region,omitempty"`
+ VersionSlug string `json:"version,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+
+ NodePools []*KubernetesNodePoolCreateRequest `json:"node_pools,omitempty"`
+}
+
+// KubernetesClusterUpdateRequest represents a request to update a Kubernetes cluster.
+type KubernetesClusterUpdateRequest struct {
+ Name string `json:"name,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+}
+
+// KubernetesNodePoolCreateRequest represents a request to create a node pool for a
+// Kubernetes cluster.
+type KubernetesNodePoolCreateRequest struct {
+ Name string `json:"name,omitempty"`
+ Size string `json:"size,omitempty"`
+ Count int `json:"count,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+}
+
+// KubernetesNodePoolUpdateRequest represents a request to update a node pool in a
+// Kubernetes cluster.
+type KubernetesNodePoolUpdateRequest struct {
+ Name string `json:"name,omitempty"`
+ Count int `json:"count,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+}
+
+// KubernetesNodePoolRecycleNodesRequest represents a request to recycle a set of
+// nodes in a node pool. This will recycle the nodes by ID.
+type KubernetesNodePoolRecycleNodesRequest struct {
+ Nodes []string `json:"nodes,omitempty"`
+}
+
+// KubernetesCluster represents a Kubernetes cluster.
+type KubernetesCluster struct {
+ ID string `json:"id,omitempty"`
+ Name string `json:"name,omitempty"`
+ RegionSlug string `json:"region,omitempty"`
+ VersionSlug string `json:"version,omitempty"`
+ ClusterSubnet string `json:"cluster_subnet,omitempty"`
+ ServiceSubnet string `json:"service_subnet,omitempty"`
+ IPv4 string `json:"ipv4,omitempty"`
+ Endpoint string `json:"endpoint,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+
+ NodePools []*KubernetesNodePool `json:"node_pools,omitempty"`
+
+ Status *KubernetesClusterStatus `json:"status,omitempty"`
+ CreatedAt time.Time `json:"created_at,omitempty"`
+ UpdatedAt time.Time `json:"updated_at,omitempty"`
+}
+
+// KubernetesClusterStatus describes the status of a cluster.
+type KubernetesClusterStatus struct {
+ State string `json:"state,omitempty"`
+ Message string `json:"message,omitempty"`
+}
+
+// KubernetesNodePool represents a node pool in a Kubernetes cluster.
+type KubernetesNodePool struct {
+ ID string `json:"id,omitempty"`
+ Name string `json:"name,omitempty"`
+ Size string `json:"size,omitempty"`
+ Count int `json:"count,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+
+ Nodes []*KubernetesNode `json:"nodes,omitempty"`
+}
+
+// KubernetesNode represents a Node in a node pool in a Kubernetes cluster.
+type KubernetesNode struct {
+ ID string `json:"id,omitempty"`
+ Name string `json:"name,omitempty"`
+ Status *KubernetesNodeStatus `json:"status,omitempty"`
+
+ CreatedAt time.Time `json:"created_at,omitempty"`
+ UpdatedAt time.Time `json:"updated_at,omitempty"`
+}
+
+// KubernetesNodeStatus represents the status of a particular Node in a Kubernetes cluster.
+type KubernetesNodeStatus struct {
+ State string `json:"state,omitempty"`
+ Message string `json:"message,omitempty"`
+}
+
+// KubernetesOptions represents options available for creating Kubernetes clusters.
+type KubernetesOptions struct {
+ Versions []*KubernetesVersion `json:"versions,omitempty"`
+}
+
+// KubernetesVersion is a DigitalOcean Kubernetes release.
+type KubernetesVersion struct {
+ Slug string `json:"slug,omitempty"`
+ KubernetesVersion string `json:"kubernetes_version,omitempty"`
+}
+
+type kubernetesClustersRoot struct {
+ Clusters []*KubernetesCluster `json:"kubernetes_clusters,omitempty"`
+ Links *Links `json:"links,omitempty"`
+}
+
+type kubernetesClusterRoot struct {
+ Cluster *KubernetesCluster `json:"kubernetes_cluster,omitempty"`
+}
+
+type kubernetesNodePoolRoot struct {
+ NodePool *KubernetesNodePool `json:"node_pool,omitempty"`
+}
+
+type kubernetesNodePoolsRoot struct {
+ NodePools []*KubernetesNodePool `json:"node_pools,omitempty"`
+ Links *Links `json:"links,omitempty"`
+}
+
+// Get retrieves the details of a Kubernetes cluster.
+func (svc *KubernetesServiceOp) Get(ctx context.Context, clusterID string) (*KubernetesCluster, *Response, error) {
+ path := fmt.Sprintf("%s/%s", kubernetesClustersPath, clusterID)
+ req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+ root := new(kubernetesClusterRoot)
+ resp, err := svc.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ return root.Cluster, resp, nil
+}
+
+// Create creates a Kubernetes cluster.
+func (svc *KubernetesServiceOp) Create(ctx context.Context, create *KubernetesClusterCreateRequest) (*KubernetesCluster, *Response, error) {
+ path := kubernetesClustersPath
+ req, err := svc.client.NewRequest(ctx, http.MethodPost, path, create)
+ if err != nil {
+ return nil, nil, err
+ }
+ root := new(kubernetesClusterRoot)
+ resp, err := svc.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ return root.Cluster, resp, nil
+}
+
+// Delete deletes a Kubernetes cluster. There is no way to recover a cluster
+// once it has been destroyed.
+func (svc *KubernetesServiceOp) Delete(ctx context.Context, clusterID string) (*Response, error) {
+ path := fmt.Sprintf("%s/%s", kubernetesClustersPath, clusterID)
+ req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := svc.client.Do(ctx, req, nil)
+ if err != nil {
+ return resp, err
+ }
+ return resp, nil
+}
+
+// List returns a list of the Kubernetes clusters visible with the caller's API token.
+func (svc *KubernetesServiceOp) List(ctx context.Context, opts *ListOptions) ([]*KubernetesCluster, *Response, error) {
+ path := kubernetesClustersPath
+ path, err := addOptions(path, opts)
+ if err != nil {
+ return nil, nil, err
+ }
+ req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+ root := new(kubernetesClustersRoot)
+ resp, err := svc.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ return root.Clusters, resp, nil
+}
+
+// KubernetesClusterConfig is the content of a Kubernetes config file, which can be
+// used to interact with your Kubernetes cluster using `kubectl`.
+// See: https://kubernetes.io/docs/tasks/tools/install-kubectl/
+type KubernetesClusterConfig struct {
+ KubeconfigYAML []byte
+}
+
+// GetKubeConfig returns a Kubernetes config file for the specified cluster.
+func (svc *KubernetesServiceOp) GetKubeConfig(ctx context.Context, clusterID string) (*KubernetesClusterConfig, *Response, error) {
+ path := fmt.Sprintf("%s/%s/kubeconfig", kubernetesClustersPath, clusterID)
+ req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+ configBytes := bytes.NewBuffer(nil)
+ resp, err := svc.client.Do(ctx, req, configBytes)
+ if err != nil {
+ return nil, resp, err
+ }
+ res := &KubernetesClusterConfig{
+ KubeconfigYAML: configBytes.Bytes(),
+ }
+ return res, resp, nil
+}
+
+// Update updates a Kubernetes cluster's properties.
+func (svc *KubernetesServiceOp) Update(ctx context.Context, clusterID string, update *KubernetesClusterUpdateRequest) (*KubernetesCluster, *Response, error) {
+ path := fmt.Sprintf("%s/%s", kubernetesClustersPath, clusterID)
+ req, err := svc.client.NewRequest(ctx, http.MethodPut, path, update)
+ if err != nil {
+ return nil, nil, err
+ }
+ root := new(kubernetesClusterRoot)
+ resp, err := svc.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ return root.Cluster, resp, nil
+}
+
+// CreateNodePool creates a new node pool in an existing Kubernetes cluster.
+func (svc *KubernetesServiceOp) CreateNodePool(ctx context.Context, clusterID string, create *KubernetesNodePoolCreateRequest) (*KubernetesNodePool, *Response, error) {
+ path := fmt.Sprintf("%s/%s/node_pools", kubernetesClustersPath, clusterID)
+ req, err := svc.client.NewRequest(ctx, http.MethodPost, path, create)
+ if err != nil {
+ return nil, nil, err
+ }
+ root := new(kubernetesNodePoolRoot)
+ resp, err := svc.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ return root.NodePool, resp, nil
+}
+
+// GetNodePool retrieves an existing node pool in a Kubernetes cluster.
+func (svc *KubernetesServiceOp) GetNodePool(ctx context.Context, clusterID, poolID string) (*KubernetesNodePool, *Response, error) {
+ path := fmt.Sprintf("%s/%s/node_pools/%s", kubernetesClustersPath, clusterID, poolID)
+ req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+ root := new(kubernetesNodePoolRoot)
+ resp, err := svc.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ return root.NodePool, resp, nil
+}
+
+// ListNodePools lists all the node pools found in a Kubernetes cluster.
+func (svc *KubernetesServiceOp) ListNodePools(ctx context.Context, clusterID string, opts *ListOptions) ([]*KubernetesNodePool, *Response, error) {
+ path := fmt.Sprintf("%s/%s/node_pools", kubernetesClustersPath, clusterID)
+ path, err := addOptions(path, opts)
+ if err != nil {
+ return nil, nil, err
+ }
+ req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+ root := new(kubernetesNodePoolsRoot)
+ resp, err := svc.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ return root.NodePools, resp, nil
+}
+
+// UpdateNodePool updates the details of an existing node pool.
+func (svc *KubernetesServiceOp) UpdateNodePool(ctx context.Context, clusterID, poolID string, update *KubernetesNodePoolUpdateRequest) (*KubernetesNodePool, *Response, error) {
+ path := fmt.Sprintf("%s/%s/node_pools/%s", kubernetesClustersPath, clusterID, poolID)
+ req, err := svc.client.NewRequest(ctx, http.MethodPut, path, update)
+ if err != nil {
+ return nil, nil, err
+ }
+ root := new(kubernetesNodePoolRoot)
+ resp, err := svc.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ return root.NodePool, resp, nil
+}
+
+// RecycleNodePoolNodes schedules nodes in a node pool for recycling.
+func (svc *KubernetesServiceOp) RecycleNodePoolNodes(ctx context.Context, clusterID, poolID string, recycle *KubernetesNodePoolRecycleNodesRequest) (*Response, error) {
+ path := fmt.Sprintf("%s/%s/node_pools/%s/recycle", kubernetesClustersPath, clusterID, poolID)
+ req, err := svc.client.NewRequest(ctx, http.MethodPost, path, recycle)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := svc.client.Do(ctx, req, nil)
+ if err != nil {
+ return resp, err
+ }
+ return resp, nil
+}
+
+// DeleteNodePool deletes a node pool, and subsequently all the nodes in that pool.
+func (svc *KubernetesServiceOp) DeleteNodePool(ctx context.Context, clusterID, poolID string) (*Response, error) {
+ path := fmt.Sprintf("%s/%s/node_pools/%s", kubernetesClustersPath, clusterID, poolID)
+ req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
+ if err != nil {
+ return nil, err
+ }
+ resp, err := svc.client.Do(ctx, req, nil)
+ if err != nil {
+ return resp, err
+ }
+ return resp, nil
+}
+
+type kubernetesOptionsRoot struct {
+ Options *KubernetesOptions `json:"options,omitempty"`
+ Links *Links `json:"links,omitempty"`
+}
+
+// GetOptions returns options about the Kubernetes service, such as the versions available for
+// cluster creation.
+func (svc *KubernetesServiceOp) GetOptions(ctx context.Context) (*KubernetesOptions, *Response, error) {
+ path := kubernetesOptionsPath
+ req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+ root := new(kubernetesOptionsRoot)
+ resp, err := svc.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ return root.Options, resp, nil
+}
diff --git a/vendor/github.com/digitalocean/godo/load_balancers.go b/vendor/github.com/digitalocean/godo/load_balancers.go
index 48b9c2654..1472fff0b 100644
--- a/vendor/github.com/digitalocean/godo/load_balancers.go
+++ b/vendor/github.com/digitalocean/godo/load_balancers.go
@@ -3,10 +3,12 @@ package godo
import (
"context"
"fmt"
+ "net/http"
)
const loadBalancersBasePath = "/v2/load_balancers"
const forwardingRulesPath = "forwarding_rules"
+
const dropletsPath = "droplets"
// LoadBalancersService is an interface for managing load balancers with the DigitalOcean API.
@@ -45,6 +47,36 @@ func (l LoadBalancer) String() string {
return Stringify(l)
}
+func (l LoadBalancer) URN() string {
+ return ToURN("LoadBalancer", l.ID)
+}
+
+// AsRequest creates a LoadBalancerRequest that can be submitted to Update with the current values of the LoadBalancer.
+// Modifying the returned LoadBalancerRequest will not modify the original LoadBalancer.
+func (l LoadBalancer) AsRequest() *LoadBalancerRequest {
+ r := LoadBalancerRequest{
+ Name: l.Name,
+ Algorithm: l.Algorithm,
+ ForwardingRules: append([]ForwardingRule(nil), l.ForwardingRules...),
+ DropletIDs: append([]int(nil), l.DropletIDs...),
+ Tag: l.Tag,
+ RedirectHttpToHttps: l.RedirectHttpToHttps,
+ HealthCheck: l.HealthCheck,
+ }
+ if l.HealthCheck != nil {
+ r.HealthCheck = &HealthCheck{}
+ *r.HealthCheck = *l.HealthCheck
+ }
+ if l.StickySessions != nil {
+ r.StickySessions = &StickySessions{}
+ *r.StickySessions = *l.StickySessions
+ }
+ if l.Region != nil {
+ r.Region = l.Region.Slug
+ }
+ return &r
+}
+
// ForwardingRule represents load balancer forwarding rules.
type ForwardingRule struct {
EntryProtocol string `json:"entry_protocol,omitempty"`
@@ -142,13 +174,13 @@ var _ LoadBalancersService = &LoadBalancersServiceOp{}
func (l *LoadBalancersServiceOp) Get(ctx context.Context, lbID string) (*LoadBalancer, *Response, error) {
path := fmt.Sprintf("%s/%s", loadBalancersBasePath, lbID)
- req, err := l.client.NewRequest(ctx, "GET", path, nil)
+ req, err := l.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(loadBalancerRoot)
- resp, err := l.client.Do(req, root)
+ resp, err := l.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -163,13 +195,13 @@ func (l *LoadBalancersServiceOp) List(ctx context.Context, opt *ListOptions) ([]
return nil, nil, err
}
- req, err := l.client.NewRequest(ctx, "GET", path, nil)
+ req, err := l.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(loadBalancersRoot)
- resp, err := l.client.Do(req, root)
+ resp, err := l.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -182,13 +214,13 @@ func (l *LoadBalancersServiceOp) List(ctx context.Context, opt *ListOptions) ([]
// Create a new load balancer with a given configuration.
func (l *LoadBalancersServiceOp) Create(ctx context.Context, lbr *LoadBalancerRequest) (*LoadBalancer, *Response, error) {
- req, err := l.client.NewRequest(ctx, "POST", loadBalancersBasePath, lbr)
+ req, err := l.client.NewRequest(ctx, http.MethodPost, loadBalancersBasePath, lbr)
if err != nil {
return nil, nil, err
}
root := new(loadBalancerRoot)
- resp, err := l.client.Do(req, root)
+ resp, err := l.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -206,7 +238,7 @@ func (l *LoadBalancersServiceOp) Update(ctx context.Context, lbID string, lbr *L
}
root := new(loadBalancerRoot)
- resp, err := l.client.Do(req, root)
+ resp, err := l.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -218,58 +250,58 @@ func (l *LoadBalancersServiceOp) Update(ctx context.Context, lbID string, lbr *L
func (l *LoadBalancersServiceOp) Delete(ctx context.Context, ldID string) (*Response, error) {
path := fmt.Sprintf("%s/%s", loadBalancersBasePath, ldID)
- req, err := l.client.NewRequest(ctx, "DELETE", path, nil)
+ req, err := l.client.NewRequest(ctx, http.MethodDelete, path, nil)
if err != nil {
return nil, err
}
- return l.client.Do(req, nil)
+ return l.client.Do(ctx, req, nil)
}
// AddDroplets adds droplets to a load balancer.
func (l *LoadBalancersServiceOp) AddDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error) {
path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, dropletsPath)
- req, err := l.client.NewRequest(ctx, "POST", path, &dropletIDsRequest{IDs: dropletIDs})
+ req, err := l.client.NewRequest(ctx, http.MethodPost, path, &dropletIDsRequest{IDs: dropletIDs})
if err != nil {
return nil, err
}
- return l.client.Do(req, nil)
+ return l.client.Do(ctx, req, nil)
}
// RemoveDroplets removes droplets from a load balancer.
func (l *LoadBalancersServiceOp) RemoveDroplets(ctx context.Context, lbID string, dropletIDs ...int) (*Response, error) {
path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, dropletsPath)
- req, err := l.client.NewRequest(ctx, "DELETE", path, &dropletIDsRequest{IDs: dropletIDs})
+ req, err := l.client.NewRequest(ctx, http.MethodDelete, path, &dropletIDsRequest{IDs: dropletIDs})
if err != nil {
return nil, err
}
- return l.client.Do(req, nil)
+ return l.client.Do(ctx, req, nil)
}
// AddForwardingRules adds forwarding rules to a load balancer.
func (l *LoadBalancersServiceOp) AddForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error) {
path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, forwardingRulesPath)
- req, err := l.client.NewRequest(ctx, "POST", path, &forwardingRulesRequest{Rules: rules})
+ req, err := l.client.NewRequest(ctx, http.MethodPost, path, &forwardingRulesRequest{Rules: rules})
if err != nil {
return nil, err
}
- return l.client.Do(req, nil)
+ return l.client.Do(ctx, req, nil)
}
// RemoveForwardingRules removes forwarding rules from a load balancer.
func (l *LoadBalancersServiceOp) RemoveForwardingRules(ctx context.Context, lbID string, rules ...ForwardingRule) (*Response, error) {
path := fmt.Sprintf("%s/%s/%s", loadBalancersBasePath, lbID, forwardingRulesPath)
- req, err := l.client.NewRequest(ctx, "DELETE", path, &forwardingRulesRequest{Rules: rules})
+ req, err := l.client.NewRequest(ctx, http.MethodDelete, path, &forwardingRulesRequest{Rules: rules})
if err != nil {
return nil, err
}
- return l.client.Do(req, nil)
+ return l.client.Do(ctx, req, nil)
}
diff --git a/vendor/github.com/digitalocean/godo/projects.go b/vendor/github.com/digitalocean/godo/projects.go
new file mode 100644
index 000000000..52291a1e0
--- /dev/null
+++ b/vendor/github.com/digitalocean/godo/projects.go
@@ -0,0 +1,302 @@
+package godo
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "path"
+)
+
+const (
+ // DefaultProject is the ID you should use if you are working with your
+ // default project.
+ DefaultProject = "default"
+
+ projectsBasePath = "/v2/projects"
+)
+
+// ProjectsService is an interface for creating and managing Projects with the DigitalOcean API.
+// See: https://developers.digitalocean.com/documentation/documentation/v2/#projects
+type ProjectsService interface {
+ List(context.Context, *ListOptions) ([]Project, *Response, error)
+ GetDefault(context.Context) (*Project, *Response, error)
+ Get(context.Context, string) (*Project, *Response, error)
+ Create(context.Context, *CreateProjectRequest) (*Project, *Response, error)
+ Update(context.Context, string, *UpdateProjectRequest) (*Project, *Response, error)
+ Delete(context.Context, string) (*Response, error)
+
+ ListResources(context.Context, string, *ListOptions) ([]ProjectResource, *Response, error)
+ AssignResources(context.Context, string, ...interface{}) ([]ProjectResource, *Response, error)
+}
+
+// ProjectsServiceOp handles communication with Projects methods of the DigitalOcean API.
+type ProjectsServiceOp struct {
+ client *Client
+}
+
+// Project represents a DigitalOcean Project configuration.
+type Project struct {
+ ID string `json:"id"`
+ OwnerUUID string `json:"owner_uuid"`
+ OwnerID uint64 `json:"owner_id"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Purpose string `json:"purpose"`
+ Environment string `json:"environment"`
+ IsDefault bool `json:"is_default"`
+ CreatedAt string `json:"created_at"`
+ UpdatedAt string `json:"updated_at"`
+}
+
+// String creates a human-readable description of a Project.
+func (p Project) String() string {
+ return Stringify(p)
+}
+
+// CreateProjectRequest represents the request to create a new project.
+type CreateProjectRequest struct {
+ Name string `json:"name"`
+ Description string `json:"description"`
+ Purpose string `json:"purpose"`
+ Environment string `json:"environment"`
+}
+
+// UpdateProjectRequest represents the request to update project information.
+// This type expects certain attribute types, but is built this way to allow
+// nil values as well. See `updateProjectRequest` for the "real" types.
+type UpdateProjectRequest struct {
+ Name interface{}
+ Description interface{}
+ Purpose interface{}
+ Environment interface{}
+ IsDefault interface{}
+}
+
+type updateProjectRequest struct {
+ Name *string `json:"name"`
+ Description *string `json:"description"`
+ Purpose *string `json:"purpose"`
+ Environment *string `json:"environment"`
+ IsDefault *bool `json:"is_default"`
+}
+
+// MarshalJSON takes an UpdateRequest and converts it to the "typed" request
+// which is sent to the projects API. This is a PATCH request, which allows
+// partial attributes, so `null` values are OK.
+func (upr *UpdateProjectRequest) MarshalJSON() ([]byte, error) {
+ d := &updateProjectRequest{}
+ if str, ok := upr.Name.(string); ok {
+ d.Name = &str
+ }
+ if str, ok := upr.Description.(string); ok {
+ d.Description = &str
+ }
+ if str, ok := upr.Purpose.(string); ok {
+ d.Purpose = &str
+ }
+ if str, ok := upr.Environment.(string); ok {
+ d.Environment = &str
+ }
+ if val, ok := upr.IsDefault.(bool); ok {
+ d.IsDefault = &val
+ }
+
+ return json.Marshal(d)
+}
+
+type assignResourcesRequest struct {
+ Resources []string `json:"resources"`
+}
+
+// ProjectResource is the projects API's representation of a resource.
+type ProjectResource struct {
+ URN string `json:"urn"`
+ AssignedAt string `json:"assigned_at"`
+ Links *ProjectResourceLinks `json:"links"`
+ Status string `json:"status,omitempty"`
+}
+
+// ProjetResourceLinks specify the link for more information about the resource.
+type ProjectResourceLinks struct {
+ Self string `json:"self"`
+}
+
+type projectsRoot struct {
+ Projects []Project `json:"projects"`
+ Links *Links `json:"links"`
+}
+
+type projectRoot struct {
+ Project *Project `json:"project"`
+}
+
+type projectResourcesRoot struct {
+ Resources []ProjectResource `json:"resources"`
+ Links *Links `json:"links,omitempty"`
+}
+
+var _ ProjectsService = &ProjectsServiceOp{}
+
+// List Projects.
+func (p *ProjectsServiceOp) List(ctx context.Context, opts *ListOptions) ([]Project, *Response, error) {
+ path, err := addOptions(projectsBasePath, opts)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ req, err := p.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(projectsRoot)
+ resp, err := p.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.Projects, resp, err
+}
+
+// GetDefault project.
+func (p *ProjectsServiceOp) GetDefault(ctx context.Context) (*Project, *Response, error) {
+ return p.getHelper(ctx, "default")
+}
+
+// Get retrieves a single project by its ID.
+func (p *ProjectsServiceOp) Get(ctx context.Context, projectID string) (*Project, *Response, error) {
+ return p.getHelper(ctx, projectID)
+}
+
+// Create a new project.
+func (p *ProjectsServiceOp) Create(ctx context.Context, cr *CreateProjectRequest) (*Project, *Response, error) {
+ req, err := p.client.NewRequest(ctx, http.MethodPost, projectsBasePath, cr)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(projectRoot)
+ resp, err := p.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Project, resp, err
+}
+
+// Update an existing project.
+func (p *ProjectsServiceOp) Update(ctx context.Context, projectID string, ur *UpdateProjectRequest) (*Project, *Response, error) {
+ path := path.Join(projectsBasePath, projectID)
+ req, err := p.client.NewRequest(ctx, http.MethodPatch, path, ur)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(projectRoot)
+ resp, err := p.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Project, resp, err
+}
+
+// Delete an existing project. You cannot have any resources in a project
+// before deleting it. See the API documentation for more details.
+func (p *ProjectsServiceOp) Delete(ctx context.Context, projectID string) (*Response, error) {
+ path := path.Join(projectsBasePath, projectID)
+ req, err := p.client.NewRequest(ctx, http.MethodDelete, path, nil)
+ if err != nil {
+ return nil, err
+ }
+
+ return p.client.Do(ctx, req, nil)
+}
+
+// ListResources lists all resources in a project.
+func (p *ProjectsServiceOp) ListResources(ctx context.Context, projectID string, opts *ListOptions) ([]ProjectResource, *Response, error) {
+ basePath := path.Join(projectsBasePath, projectID, "resources")
+ path, err := addOptions(basePath, opts)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ req, err := p.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(projectResourcesRoot)
+ resp, err := p.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.Resources, resp, err
+}
+
+// AssignResources assigns one or more resources to a project. AssignResources
+// accepts resources in two possible formats:
+
+// 1. The resource type, like `&Droplet{ID: 1}` or `&FloatingIP{IP: "1.2.3.4"}`
+// 2. A valid DO URN as a string, like "do:droplet:1234"
+//
+// There is no unassign. To move a resource to another project, just assign
+// it to that other project.
+func (p *ProjectsServiceOp) AssignResources(ctx context.Context, projectID string, resources ...interface{}) ([]ProjectResource, *Response, error) {
+ path := path.Join(projectsBasePath, projectID, "resources")
+
+ ar := &assignResourcesRequest{
+ Resources: make([]string, len(resources)),
+ }
+
+ for i, resource := range resources {
+ switch resource.(type) {
+ case ResourceWithURN:
+ ar.Resources[i] = resource.(ResourceWithURN).URN()
+ case string:
+ ar.Resources[i] = resource.(string)
+ default:
+ return nil, nil, fmt.Errorf("%T must either be a string or have a valid URN method", resource)
+ }
+ }
+ req, err := p.client.NewRequest(ctx, http.MethodPost, path, ar)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(projectResourcesRoot)
+ resp, err := p.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+ if l := root.Links; l != nil {
+ resp.Links = l
+ }
+
+ return root.Resources, resp, err
+}
+
+func (p *ProjectsServiceOp) getHelper(ctx context.Context, projectID string) (*Project, *Response, error) {
+ path := path.Join(projectsBasePath, projectID)
+
+ req, err := p.client.NewRequest(ctx, http.MethodGet, path, nil)
+ if err != nil {
+ return nil, nil, err
+ }
+
+ root := new(projectRoot)
+ resp, err := p.client.Do(ctx, req, root)
+ if err != nil {
+ return nil, resp, err
+ }
+
+ return root.Project, resp, err
+}
diff --git a/vendor/github.com/digitalocean/godo/regions.go b/vendor/github.com/digitalocean/godo/regions.go
index ccfe02926..409959d95 100644
--- a/vendor/github.com/digitalocean/godo/regions.go
+++ b/vendor/github.com/digitalocean/godo/regions.go
@@ -1,6 +1,9 @@
package godo
-import "context"
+import (
+ "context"
+ "net/http"
+)
// RegionsService is an interface for interfacing with the regions
// endpoints of the DigitalOcean API
@@ -43,13 +46,13 @@ func (s *RegionsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Region
return nil, nil, err
}
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(regionsRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
diff --git a/vendor/github.com/digitalocean/godo/sizes.go b/vendor/github.com/digitalocean/godo/sizes.go
index b69579278..da8207c08 100644
--- a/vendor/github.com/digitalocean/godo/sizes.go
+++ b/vendor/github.com/digitalocean/godo/sizes.go
@@ -1,6 +1,9 @@
package godo
-import "context"
+import (
+ "context"
+ "net/http"
+)
// SizesService is an interface for interfacing with the size
// endpoints of the DigitalOcean API
@@ -47,13 +50,13 @@ func (s *SizesServiceOp) List(ctx context.Context, opt *ListOptions) ([]Size, *R
return nil, nil, err
}
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(sizesRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
diff --git a/vendor/github.com/digitalocean/godo/snapshots.go b/vendor/github.com/digitalocean/godo/snapshots.go
index 5a9e5c5d8..181188db2 100644
--- a/vendor/github.com/digitalocean/godo/snapshots.go
+++ b/vendor/github.com/digitalocean/godo/snapshots.go
@@ -3,6 +3,7 @@ package godo
import (
"context"
"fmt"
+ "net/http"
)
const snapshotBasePath = "v2/snapshots"
@@ -81,12 +82,12 @@ func (s *SnapshotsServiceOp) Get(ctx context.Context, snapshotID string) (*Snaps
func (s *SnapshotsServiceOp) Delete(ctx context.Context, snapshotID string) (*Response, error) {
path := fmt.Sprintf("%s/%s", snapshotBasePath, snapshotID)
- req, err := s.client.NewRequest(ctx, "DELETE", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
if err != nil {
return nil, err
}
- resp, err := s.client.Do(req, nil)
+ resp, err := s.client.Do(ctx, req, nil)
return resp, err
}
@@ -95,13 +96,13 @@ func (s *SnapshotsServiceOp) Delete(ctx context.Context, snapshotID string) (*Re
func (s *SnapshotsServiceOp) get(ctx context.Context, ID string) (*Snapshot, *Response, error) {
path := fmt.Sprintf("%s/%s", snapshotBasePath, ID)
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(snapshotRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -121,13 +122,13 @@ func (s *SnapshotsServiceOp) list(ctx context.Context, opt *ListOptions, listOpt
return nil, nil, err
}
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(snapshotsRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
diff --git a/vendor/github.com/digitalocean/godo/storage.go b/vendor/github.com/digitalocean/godo/storage.go
index 00d5157d6..a79332a79 100644
--- a/vendor/github.com/digitalocean/godo/storage.go
+++ b/vendor/github.com/digitalocean/godo/storage.go
@@ -3,6 +3,7 @@ package godo
import (
"context"
"fmt"
+ "net/http"
"time"
)
@@ -43,19 +44,25 @@ var _ StorageService = &StorageServiceOp{}
// Volume represents a Digital Ocean block store volume.
type Volume struct {
- ID string `json:"id"`
- Region *Region `json:"region"`
- Name string `json:"name"`
- SizeGigaBytes int64 `json:"size_gigabytes"`
- Description string `json:"description"`
- DropletIDs []int `json:"droplet_ids"`
- CreatedAt time.Time `json:"created_at"`
+ ID string `json:"id"`
+ Region *Region `json:"region"`
+ Name string `json:"name"`
+ SizeGigaBytes int64 `json:"size_gigabytes"`
+ Description string `json:"description"`
+ DropletIDs []int `json:"droplet_ids"`
+ CreatedAt time.Time `json:"created_at"`
+ FilesystemType string `json:"filesystem_type"`
+ FilesystemLabel string `json:"filesystem_label"`
}
func (f Volume) String() string {
return Stringify(f)
}
+func (f Volume) URN() string {
+ return ToURN("Volume", f.ID)
+}
+
type storageVolumesRoot struct {
Volumes []Volume `json:"volumes"`
Links *Links `json:"links"`
@@ -69,11 +76,13 @@ type storageVolumeRoot struct {
// VolumeCreateRequest represents a request to create a block store
// volume.
type VolumeCreateRequest struct {
- Region string `json:"region"`
- Name string `json:"name"`
- Description string `json:"description"`
- SizeGigaBytes int64 `json:"size_gigabytes"`
- SnapshotID string `json:"snapshot_id"`
+ Region string `json:"region"`
+ Name string `json:"name"`
+ Description string `json:"description"`
+ SizeGigaBytes int64 `json:"size_gigabytes"`
+ SnapshotID string `json:"snapshot_id"`
+ FilesystemType string `json:"filesystem_type"`
+ FilesystemLabel string `json:"filesystem_label"`
}
// ListVolumes lists all storage volumes.
@@ -82,6 +91,10 @@ func (svc *StorageServiceOp) ListVolumes(ctx context.Context, params *ListVolume
if params != nil {
if params.Region != "" && params.Name != "" {
path = fmt.Sprintf("%s?name=%s®ion=%s", path, params.Name, params.Region)
+ } else if params.Region != "" {
+ path = fmt.Sprintf("%s?region=%s", path, params.Region)
+ } else if params.Name != "" {
+ path = fmt.Sprintf("%s?name=%s", path, params.Name)
}
if params.ListOptions != nil {
@@ -93,13 +106,13 @@ func (svc *StorageServiceOp) ListVolumes(ctx context.Context, params *ListVolume
}
}
- req, err := svc.client.NewRequest(ctx, "GET", path, nil)
+ req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(storageVolumesRoot)
- resp, err := svc.client.Do(req, root)
+ resp, err := svc.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -115,13 +128,13 @@ func (svc *StorageServiceOp) ListVolumes(ctx context.Context, params *ListVolume
func (svc *StorageServiceOp) CreateVolume(ctx context.Context, createRequest *VolumeCreateRequest) (*Volume, *Response, error) {
path := storageAllocPath
- req, err := svc.client.NewRequest(ctx, "POST", path, createRequest)
+ req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createRequest)
if err != nil {
return nil, nil, err
}
root := new(storageVolumeRoot)
- resp, err := svc.client.Do(req, root)
+ resp, err := svc.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -132,13 +145,13 @@ func (svc *StorageServiceOp) CreateVolume(ctx context.Context, createRequest *Vo
func (svc *StorageServiceOp) GetVolume(ctx context.Context, id string) (*Volume, *Response, error) {
path := fmt.Sprintf("%s/%s", storageAllocPath, id)
- req, err := svc.client.NewRequest(ctx, "GET", path, nil)
+ req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(storageVolumeRoot)
- resp, err := svc.client.Do(req, root)
+ resp, err := svc.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -150,11 +163,11 @@ func (svc *StorageServiceOp) GetVolume(ctx context.Context, id string) (*Volume,
func (svc *StorageServiceOp) DeleteVolume(ctx context.Context, id string) (*Response, error) {
path := fmt.Sprintf("%s/%s", storageAllocPath, id)
- req, err := svc.client.NewRequest(ctx, "DELETE", path, nil)
+ req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
if err != nil {
return nil, err
}
- return svc.client.Do(req, nil)
+ return svc.client.Do(ctx, req, nil)
}
// SnapshotCreateRequest represents a request to create a block store
@@ -173,13 +186,13 @@ func (svc *StorageServiceOp) ListSnapshots(ctx context.Context, volumeID string,
return nil, nil, err
}
- req, err := svc.client.NewRequest(ctx, "GET", path, nil)
+ req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(snapshotsRoot)
- resp, err := svc.client.Do(req, root)
+ resp, err := svc.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -195,13 +208,13 @@ func (svc *StorageServiceOp) ListSnapshots(ctx context.Context, volumeID string,
func (svc *StorageServiceOp) CreateSnapshot(ctx context.Context, createRequest *SnapshotCreateRequest) (*Snapshot, *Response, error) {
path := fmt.Sprintf("%s/%s/snapshots", storageAllocPath, createRequest.VolumeID)
- req, err := svc.client.NewRequest(ctx, "POST", path, createRequest)
+ req, err := svc.client.NewRequest(ctx, http.MethodPost, path, createRequest)
if err != nil {
return nil, nil, err
}
root := new(snapshotRoot)
- resp, err := svc.client.Do(req, root)
+ resp, err := svc.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -212,13 +225,13 @@ func (svc *StorageServiceOp) CreateSnapshot(ctx context.Context, createRequest *
func (svc *StorageServiceOp) GetSnapshot(ctx context.Context, id string) (*Snapshot, *Response, error) {
path := fmt.Sprintf("%s/%s", storageSnapPath, id)
- req, err := svc.client.NewRequest(ctx, "GET", path, nil)
+ req, err := svc.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(snapshotRoot)
- resp, err := svc.client.Do(req, root)
+ resp, err := svc.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -230,9 +243,9 @@ func (svc *StorageServiceOp) GetSnapshot(ctx context.Context, id string) (*Snaps
func (svc *StorageServiceOp) DeleteSnapshot(ctx context.Context, id string) (*Response, error) {
path := fmt.Sprintf("%s/%s", storageSnapPath, id)
- req, err := svc.client.NewRequest(ctx, "DELETE", path, nil)
+ req, err := svc.client.NewRequest(ctx, http.MethodDelete, path, nil)
if err != nil {
return nil, err
}
- return svc.client.Do(req, nil)
+ return svc.client.Do(ctx, req, nil)
}
diff --git a/vendor/github.com/digitalocean/godo/storage_actions.go b/vendor/github.com/digitalocean/godo/storage_actions.go
index 21f82659f..948897568 100644
--- a/vendor/github.com/digitalocean/godo/storage_actions.go
+++ b/vendor/github.com/digitalocean/godo/storage_actions.go
@@ -3,6 +3,7 @@ package godo
import (
"context"
"fmt"
+ "net/http"
)
// StorageActionsService is an interface for interfacing with the
@@ -76,13 +77,13 @@ func (s *StorageActionsServiceOp) Resize(ctx context.Context, volumeID string, s
func (s *StorageActionsServiceOp) doAction(ctx context.Context, volumeID string, request *ActionRequest) (*Action, *Response, error) {
path := storageAllocationActionPath(volumeID)
- req, err := s.client.NewRequest(ctx, "POST", path, request)
+ req, err := s.client.NewRequest(ctx, http.MethodPost, path, request)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -91,13 +92,13 @@ func (s *StorageActionsServiceOp) doAction(ctx context.Context, volumeID string,
}
func (s *StorageActionsServiceOp) get(ctx context.Context, path string) (*Action, *Response, error) {
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(actionRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -106,13 +107,13 @@ func (s *StorageActionsServiceOp) get(ctx context.Context, path string) (*Action
}
func (s *StorageActionsServiceOp) list(ctx context.Context, path string) ([]Action, *Response, error) {
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(actionsRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
diff --git a/vendor/github.com/digitalocean/godo/strings.go b/vendor/github.com/digitalocean/godo/strings.go
index 4a8bfb636..4d5c0ad22 100644
--- a/vendor/github.com/digitalocean/godo/strings.go
+++ b/vendor/github.com/digitalocean/godo/strings.go
@@ -5,10 +5,20 @@ import (
"fmt"
"io"
"reflect"
+ "strings"
)
var timestampType = reflect.TypeOf(Timestamp{})
+type ResourceWithURN interface {
+ URN() string
+}
+
+// ToURN converts the resource type and ID to a valid DO API URN.
+func ToURN(resourceType string, id interface{}) string {
+ return fmt.Sprintf("%s:%s:%v", "do", strings.ToLower(resourceType), id)
+}
+
// Stringify attempts to create a string representation of DigitalOcean types
func Stringify(message interface{}) string {
var buf bytes.Buffer
diff --git a/vendor/github.com/digitalocean/godo/tags.go b/vendor/github.com/digitalocean/godo/tags.go
index 391864434..6a9c4dae3 100644
--- a/vendor/github.com/digitalocean/godo/tags.go
+++ b/vendor/github.com/digitalocean/godo/tags.go
@@ -3,6 +3,7 @@ package godo
import (
"context"
"fmt"
+ "net/http"
)
const tagsBasePath = "v2/tags"
@@ -34,6 +35,8 @@ type ResourceType string
const (
//DropletResourceType holds the string representing our ResourceType of Droplet.
DropletResourceType ResourceType = "droplet"
+ //ImageResourceType holds the string representing our ResourceType of Image.
+ ImageResourceType ResourceType = "image"
)
// Resource represent a single resource for associating/disassociating with tags
@@ -44,13 +47,23 @@ type Resource struct {
// TaggedResources represent the set of resources a tag is attached to
type TaggedResources struct {
- Droplets *TaggedDropletsResources `json:"droplets,omitempty"`
+ Count int `json:"count"`
+ LastTaggedURI string `json:"last_tagged_uri,omitempty"`
+ Droplets *TaggedDropletsResources `json:"droplets,omitempty"`
+ Images *TaggedImagesResources `json:"images"`
}
// TaggedDropletsResources represent the droplet resources a tag is attached to
type TaggedDropletsResources struct {
- Count int `json:"count,float64,omitempty"`
- LastTagged *Droplet `json:"last_tagged,omitempty"`
+ Count int `json:"count,float64,omitempty"`
+ LastTagged *Droplet `json:"last_tagged,omitempty"`
+ LastTaggedURI string `json:"last_tagged_uri,omitempty"`
+}
+
+// TaggedImagesResources represent the image resources a tag is attached to
+type TaggedImagesResources struct {
+ Count int `json:"count,float64,omitempty"`
+ LastTaggedURI string `json:"last_tagged_uri,omitempty"`
}
// Tag represent DigitalOcean tag
@@ -92,13 +105,13 @@ func (s *TagsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Tag, *Res
return nil, nil, err
}
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(tagsRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -113,13 +126,13 @@ func (s *TagsServiceOp) List(ctx context.Context, opt *ListOptions) ([]Tag, *Res
func (s *TagsServiceOp) Get(ctx context.Context, name string) (*Tag, *Response, error) {
path := fmt.Sprintf("%s/%s", tagsBasePath, name)
- req, err := s.client.NewRequest(ctx, "GET", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, nil, err
}
root := new(tagRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -133,13 +146,13 @@ func (s *TagsServiceOp) Create(ctx context.Context, createRequest *TagCreateRequ
return nil, nil, NewArgError("createRequest", "cannot be nil")
}
- req, err := s.client.NewRequest(ctx, "POST", tagsBasePath, createRequest)
+ req, err := s.client.NewRequest(ctx, http.MethodPost, tagsBasePath, createRequest)
if err != nil {
return nil, nil, err
}
root := new(tagRoot)
- resp, err := s.client.Do(req, root)
+ resp, err := s.client.Do(ctx, req, root)
if err != nil {
return nil, resp, err
}
@@ -154,12 +167,12 @@ func (s *TagsServiceOp) Delete(ctx context.Context, name string) (*Response, err
}
path := fmt.Sprintf("%s/%s", tagsBasePath, name)
- req, err := s.client.NewRequest(ctx, "DELETE", path, nil)
+ req, err := s.client.NewRequest(ctx, http.MethodDelete, path, nil)
if err != nil {
return nil, err
}
- resp, err := s.client.Do(req, nil)
+ resp, err := s.client.Do(ctx, req, nil)
return resp, err
}
@@ -175,12 +188,12 @@ func (s *TagsServiceOp) TagResources(ctx context.Context, name string, tagReques
}
path := fmt.Sprintf("%s/%s/resources", tagsBasePath, name)
- req, err := s.client.NewRequest(ctx, "POST", path, tagRequest)
+ req, err := s.client.NewRequest(ctx, http.MethodPost, path, tagRequest)
if err != nil {
return nil, err
}
- resp, err := s.client.Do(req, nil)
+ resp, err := s.client.Do(ctx, req, nil)
return resp, err
}
@@ -196,12 +209,12 @@ func (s *TagsServiceOp) UntagResources(ctx context.Context, name string, untagRe
}
path := fmt.Sprintf("%s/%s/resources", tagsBasePath, name)
- req, err := s.client.NewRequest(ctx, "DELETE", path, untagRequest)
+ req, err := s.client.NewRequest(ctx, http.MethodDelete, path, untagRequest)
if err != nil {
return nil, err
}
- resp, err := s.client.Do(req, nil)
+ resp, err := s.client.Do(ctx, req, nil)
return resp, err
}
diff --git a/vendor/vendor.json b/vendor/vendor.json
index d12f143bb..0defc2d99 100644
--- a/vendor/vendor.json
+++ b/vendor/vendor.json
@@ -717,11 +717,11 @@
"revisionTime": "2018-03-08T23:13:08Z"
},
{
- "checksumSHA1": "W1LGm0UNirwMDVCMFv5vZrOpUJI=",
+ "checksumSHA1": "u6kWbz1BRsZT433AooftjLNNtdw=",
"comment": "v0.9.0-24-g6ca5b77",
"path": "github.com/digitalocean/godo",
- "revision": "4c04abe183f449bd9ede285f0e5c7ee575d0dbe4",
- "revisionTime": "2017-04-07T15:15:42Z"
+ "revision": "c4ae66932b5d6fb727d54e9ded9ee722f67f3ca9",
+ "revisionTime": "2018-11-13T19:55:25Z"
},
{
"checksumSHA1": "1n5MBJthemxmfqU2gN3qLCd8s04=",
@@ -1487,18 +1487,18 @@
"revision": "2d205ac6ec17a839a94bdbfd16d2fa6c6dada2e0",
"revisionTime": "2016-03-31T20:48:55Z"
},
- {
- "checksumSHA1": "6JP37UqrI0H80Gpk0Y2P+KXgn5M=",
- "path": "github.com/ryanuber/go-glob",
- "revision": "256dc444b735e061061cf46c809487313d5b0065",
- "revisionTime": "2017-01-28T01:21:29Z"
- },
{
"checksumSHA1": "4gVuzkHbQoznf+lCSJhIJnvS5tc=",
"path": "github.com/rwtodd/Go.Sed/sed",
"revision": "d6d5d585814e4c3560c684f52e3d8aeed721313d",
"revisionTime": "2017-05-07T04:53:31Z"
},
+ {
+ "checksumSHA1": "6JP37UqrI0H80Gpk0Y2P+KXgn5M=",
+ "path": "github.com/ryanuber/go-glob",
+ "revision": "256dc444b735e061061cf46c809487313d5b0065",
+ "revisionTime": "2017-01-28T01:21:29Z"
+ },
{
"checksumSHA1": "zmC8/3V4ls53DJlNTKDZwPSC/dA=",
"path": "github.com/satori/go.uuid",
diff --git a/website/source/community-plugins.html.md b/website/source/community-plugins.html.md
index bb0cb2490..537cdc3ca 100644
--- a/website/source/community-plugins.html.md
+++ b/website/source/community-plugins.html.md
@@ -25,6 +25,7 @@ still distributed with Packer.
## Provisioners
- File
+- InSpec
- PowerShell
- Shell
- Windows Restart
diff --git a/website/source/docs/builders/alicloud-ecs.html.md b/website/source/docs/builders/alicloud-ecs.html.md
index 8585cee65..945b95090 100644
--- a/website/source/docs/builders/alicloud-ecs.html.md
+++ b/website/source/docs/builders/alicloud-ecs.html.md
@@ -147,28 +147,30 @@ builder.
Snapshots from on or before July 15, 2013 cannot be used to create a
disk.
-
-- `image_ignore_data_disks`(boolean) - If this value is true, the image created
- will not include any snapshot of data disks. This option would be useful for
- any circumstance that default data disks with instance types are not concerned.
- The default value is false.
-- `wait_snapshot_ready_timeout`(number) - Timeout of creating snapshot(s). The
- default timeout is 3600 seconds if this option is not set or is set to 0. For
- those disks containing lots of data, it may require a higher timeout value.
+- `image_ignore_data_disks`(boolean) - If this value is true, the image
+ created will not include any snapshot of data disks. This option would be
+ useful for any circumstance that default data disks with instance types are
+ not concerned. The default value is false.
+
+- `wait_snapshot_ready_timeout`(number) - Timeout of creating snapshot(s).
+ The default timeout is 3600 seconds if this option is not set or is set
+ to 0. For those disks containing lots of data, it may require a higher
+ timeout value.
- `image_force_delete` (boolean) - If this value is true, when the target
- image names including those copied are duplicated with existing images,
- it will delete the existing images and then create the target images,
- otherwise, the creation will fail. The default value is false. Check
- `image_name` and `image_copy_names` options for names of target images.
- If [-force](https://packer.io/docs/commands/build.html#force) option is provided
- in `build` command, this option can be omitted and taken as true.
+ image names including those copied are duplicated with existing images, it
+ will delete the existing images and then create the target images,
+ otherwise, the creation will fail. The default value is false. Check
+ `image_name` and `image_copy_names` options for names of target images. If
+ [-force](https://packer.io/docs/commands/build.html#force) option is
+ provided in `build` command, this option can be omitted and taken as true.
- `image_force_delete_snapshots` (boolean) - If this value is true, when
delete the duplicated existing images, the source snapshots of those images
- will be delete either. If [-force](https://packer.io/docs/commands/build.html#force)
- option is provided in `build` command, this option can be omitted and taken as true.
+ will be delete either. If
+ [-force](https://packer.io/docs/commands/build.html#force) option is
+ provided in `build` command, this option can be omitted and taken as true.
- `image_share_account` (array of string) - The IDs of to-be-added Aliyun
accounts to which the image is shared. The number of accounts is 1 to 10.
@@ -299,10 +301,13 @@ Here is a basic example for Alicloud.
\~> Note: Images can become deprecated after a while; run
`aliyun ecs DescribeImages` to find one that exists.
-\~> Note: Since WinRM is closed by default in the system image. If you are planning
-to use Windows as the base image, you need enable it by userdata in order to connect to
-the instance, check [alicloud_windows.json](https://github.com/hashicorp/packer/tree/master/examples/alicloud/basic/alicloud_windows.json)
-and [winrm_enable_userdata.ps1](https://github.com/hashicorp/packer/tree/master/examples/alicloud/basic/winrm_enable_userdata.ps1) for details.
+\~> Note: Since WinRM is closed by default in the system image. If you are
+planning to use Windows as the base image, you need enable it by userdata in
+order to connect to the instance, check
+[alicloud\_windows.json](https://github.com/hashicorp/packer/tree/master/examples/alicloud/basic/alicloud_windows.json)
+and
+[winrm\_enable\_userdata.ps1](https://github.com/hashicorp/packer/tree/master/examples/alicloud/basic/winrm_enable_userdata.ps1)
+for details.
See the
[examples/alicloud](https://github.com/hashicorp/packer/tree/master/examples/alicloud)
diff --git a/website/source/docs/builders/amazon-chroot.html.md b/website/source/docs/builders/amazon-chroot.html.md
index 24622e049..3b97c38eb 100644
--- a/website/source/docs/builders/amazon-chroot.html.md
+++ b/website/source/docs/builders/amazon-chroot.html.md
@@ -24,7 +24,7 @@ builder is able to build an EBS-backed AMI without launching a new EC2
instance. This can dramatically speed up AMI builds for organizations who need
the extra fast build.
-~> **This is an advanced builder** If you're just getting started with
+\~> **This is an advanced builder** If you're just getting started with
Packer, we recommend starting with the [amazon-ebs
builder](/docs/builders/amazon-ebs.html), which is much easier to use.
@@ -154,8 +154,8 @@ each category, the available configuration keys are alphabetized.
associated with AMIs, which have been deregistered by `force_deregister`.
Default `false`.
-- `insecure_skip_tls_verify` (boolean) - This allows skipping TLS verification of
- the AWS EC2 endpoint. The default is `false`.
+- `insecure_skip_tls_verify` (boolean) - This allows skipping TLS
+ verification of the AWS EC2 endpoint. The default is `false`.
- `kms_key_id` (string) - ID, alias or ARN of the KMS key to use for boot
volume encryption. This only applies to the main `region`, other regions
@@ -362,6 +362,34 @@ each category, the available configuration keys are alphabetized.
[template engine](/docs/templates/engine.html), see [Build template
data](#build-template-data) for more information.
+- `vault_aws_engine` (object) - Get credentials from Hashicorp Vault's aws
+ secrets engine. You must already have created a role to use. For more
+ information about generating credentials via the Vault engine, see the
+ [Vault
+ docs.](https://www.vaultproject.io/api/secret/aws/index.html#generate-credentials)
+ If you set this flag, you must also set the below options:
+ - `name` (string) - Required. Specifies the name of the role to generate
+ credentials against. This is part of the request URL.
+ - `engine_name` (string) - The name of the aws secrets engine. In the
+ Vault docs, this is normally referred to as "aws", and Packer will
+ default to "aws" if `engine_name` is not set.
+ - `role_arn` (string)- The ARN of the role to assume if credential\_type
+ on the Vault role is assumed\_role. Must match one of the allowed role
+ ARNs in the Vault role. Optional if the Vault role only allows a single
+ AWS role ARN; required otherwise.
+ - `ttl` (string) - Specifies the TTL for the use of the STS token. This
+ is specified as a string with a duration suffix. Valid only when
+ credential\_type is assumed\_role or federation\_token. When not
+ specified, the default\_sts\_ttl set for the role will be used. If that
+ is also not set, then the default value of 3600s will be used. AWS
+ places limits on the maximum TTL allowed. See the AWS documentation on
+ the DurationSeconds parameter for AssumeRole (for assumed\_role
+ credential types) and GetFederationToken (for federation\_token
+ credential types) for more details.
+
+ Example:
+ `json { "vault_aws_engine": { "name": "myrole", "role_arn": "myarn", "ttl": "3600s" } }`
+
## Basic Example
Here is a basic example. It is completely valid except for the access keys:
@@ -457,8 +485,8 @@ services:
### Ansible provisioner
-Running ansible against `amazon-chroot` requires changing the Ansible connection
-to chroot and running Ansible as root/sudo.
+Running ansible against `amazon-chroot` requires changing the Ansible
+connection to chroot and running Ansible as root/sudo.
### Using Instances with NVMe block devices.
diff --git a/website/source/docs/builders/amazon-ebs.html.md b/website/source/docs/builders/amazon-ebs.html.md
index 35e0112c9..314bbb47c 100644
--- a/website/source/docs/builders/amazon-ebs.html.md
+++ b/website/source/docs/builders/amazon-ebs.html.md
@@ -46,7 +46,9 @@ builder.
### Required:
- `access_key` (string) - The access key used to communicate with AWS. [Learn
- how to set this](amazon.html#specifying-amazon-credentials)
+ how to set this](amazon.html#specifying-amazon-credentials). This is not
+ required if you are using `use_vault_aws_engine` for authentication
+ instead.
- `ami_name` (string) - The name of the resulting AMI that will appear when
managing AMIs in the AWS console or via APIs. This must be unique. To help
@@ -60,7 +62,9 @@ builder.
to launch the EC2 instance to create the AMI.
- `secret_key` (string) - The secret key used to communicate with AWS. [Learn
- how to set this](amazon.html#specifying-amazon-credentials)
+ how to set this](amazon.html#specifying-amazon-credentials). This is not
+ required if you are using `use_vault_aws_engine` for authentication
+ instead.
- `source_ami` (string) - The initial AMI used as a base for the newly
created machine. `source_ami_filter` may be used instead to populate this
@@ -505,6 +509,41 @@ builder.
- `user_data_file` (string) - Path to a file that will be used for the user
data when launching the instance.
+- `vault_aws_engine` (object) - Get credentials from Hashicorp Vault's aws
+ secrets engine. You must already have created a role to use. For more
+ information about generating credentials via the Vault engine, see the
+ [Vault
+ docs.](https://www.vaultproject.io/api/secret/aws/index.html#generate-credentials)
+ If you set this flag, you must also set the below options:
+ - `name` (string) - Required. Specifies the name of the role to generate
+ credentials against. This is part of the request URL.
+ - `engine_name` (string) - The name of the aws secrets engine. In the
+ Vault docs, this is normally referred to as "aws", and Packer will
+ default to "aws" if `engine_name` is not set.
+ - `role_arn` (string)- The ARN of the role to assume if credential\_type
+ on the Vault role is assumed\_role. Must match one of the allowed role
+ ARNs in the Vault role. Optional if the Vault role only allows a single
+ AWS role ARN; required otherwise.
+ - `ttl` (string) - Specifies the TTL for the use of the STS token. This
+ is specified as a string with a duration suffix. Valid only when
+ credential\_type is assumed\_role or federation\_token. When not
+ specified, the default\_sts\_ttl set for the role will be used. If that
+ is also not set, then the default value of 3600s will be used. AWS
+ places limits on the maximum TTL allowed. See the AWS documentation on
+ the DurationSeconds parameter for AssumeRole (for assumed\_role
+ credential types) and GetFederationToken (for federation\_token
+ credential types) for more details.
+
+ ``` json
+ {
+ "vault_aws_engine": {
+ "name": "myrole",
+ "role_arn": "myarn",
+ "ttl": "3600s"
+ }
+ }
+ ```
+
- `vpc_id` (string) - If launching into a VPC subnet, Packer needs the VPC ID
in order to create a temporary security group within the VPC. Requires
`subnet_id` to be set. If this field is left blank, Packer will try to get
diff --git a/website/source/docs/builders/amazon-ebssurrogate.html.md b/website/source/docs/builders/amazon-ebssurrogate.html.md
index becffc4f4..04ddad84e 100644
--- a/website/source/docs/builders/amazon-ebssurrogate.html.md
+++ b/website/source/docs/builders/amazon-ebssurrogate.html.md
@@ -497,6 +497,33 @@ builder.
- `user_data_file` (string) - Path to a file that will be used for the user
data when launching the instance.
+- `vault_aws_engine` (object) - Get credentials from Hashicorp Vault's aws
+ secrets engine. You must already have created a role to use. For more
+ information about generating credentials via the Vault engine, see the
+ [Vault
+ docs.](https://www.vaultproject.io/api/secret/aws/index.html#generate-credentials)
+ If you set this flag, you must also set the below options:
+- `name` (string) - Required. Specifies the name of the role to generate
+ credentials against. This is part of the request URL.
+- `engine_name` (string) - The name of the aws secrets engine. In the Vault
+ docs, this is normally referred to as "aws", and Packer will default to
+ "aws" if `engine_name` is not set.
+- `role_arn` (string)- The ARN of the role to assume if credential\_type on
+ the Vault role is assumed\_role. Must match one of the allowed role ARNs in
+ the Vault role. Optional if the Vault role only allows a single AWS role
+ ARN; required otherwise.
+- `ttl` (string) - Specifies the TTL for the use of the STS token. This is
+ specified as a string with a duration suffix. Valid only when
+ credential\_type is assumed\_role or federation\_token. When not specified,
+ the default\_sts\_ttl set for the role will be used. If that is also not
+ set, then the default value of 3600s will be used. AWS places limits on the
+ maximum TTL allowed. See the AWS documentation on the DurationSeconds
+ parameter for AssumeRole (for assumed\_role credential types) and
+ GetFederationToken (for federation\_token credential types) for more
+ details.
+
+ Example:
+ `json { "vault_aws_engine": { "name": "myrole", "role_arn": "myarn", "ttl": "3600s" } }`
- `vpc_id` (string) - If launching into a VPC subnet, Packer needs the VPC ID
in order to create a temporary security group within the VPC. Requires
`subnet_id` to be set. If this field is left blank, Packer will try to get
diff --git a/website/source/docs/builders/amazon-ebsvolume.html.md b/website/source/docs/builders/amazon-ebsvolume.html.md
index 9cd438a8a..18b1ebadd 100644
--- a/website/source/docs/builders/amazon-ebsvolume.html.md
+++ b/website/source/docs/builders/amazon-ebsvolume.html.md
@@ -407,6 +407,43 @@ builder.
- `user_data_file` (string) - Path to a file that will be used for the user
data when launching the instance.
+- `vault_aws_engine` (object) - Get credentials from Hashicorp Vault's aws
+ secrets engine. You must already have created a role to use. For more
+ information about generating credentials via the Vault engine, see the
+ [Vault docs.]
+ (https://www.vaultproject.io/api/secret/aws/index.html#generate-credentials)
+ If you set this
+ flag, you must also set the below options:
+ - `name` (string) - Required. Specifies the name of the role to generate
+ credentials against. This is part of the request URL.
+ - `engine_name` (string) - The name of the aws secrets engine. In the Vault
+ docs, this is normally referred to as "aws", and Packer will default to
+ "aws" if `engine_name` is not set.
+ - `role_arn` (string)- The ARN of the role to assume if credential_type on
+ the Vault role is assumed_role. Must match one of the allowed role ARNs
+ in the Vault role. Optional if the Vault role only allows a single AWS
+ role ARN; required otherwise.
+ - `ttl` (string) - Specifies the TTL for the use of the STS token. This is
+ specified as a string with a duration suffix. Valid only when
+ credential_type is assumed_role or federation_token. When not specified,
+ the default_sts_ttl set for the role will be used. If that is also not
+ set, then the default value of 3600s will be used. AWS places limits on
+ the maximum TTL allowed. See the AWS documentation on the DurationSeconds
+ parameter for AssumeRole (for assumed_role credential types) and
+ GetFederationToken (for federation_token credential types) for more
+ details.
+
+ Example:
+ ``` json
+ {
+ "vault_aws_engine": {
+ "name": "myrole",
+ "role_arn": "myarn",
+ "ttl": "3600s"
+ }
+ }
+ ```
+
- `vpc_id` (string) - If launching into a VPC subnet, Packer needs the VPC ID
in order to create a temporary security group within the VPC. Requires
`subnet_id` to be set. If this field is left blank, Packer will try to get
diff --git a/website/source/docs/builders/amazon-instance.html.md b/website/source/docs/builders/amazon-instance.html.md
index 4ce4a0255..d4c81e3c9 100644
--- a/website/source/docs/builders/amazon-instance.html.md
+++ b/website/source/docs/builders/amazon-instance.html.md
@@ -489,6 +489,34 @@ builder.
- `user_data_file` (string) - Path to a file that will be used for the user
data when launching the instance.
+- `vault_aws_engine` (object) - Get credentials from Hashicorp Vault's aws
+ secrets engine. You must already have created a role to use. For more
+ information about generating credentials via the Vault engine, see the
+ [Vault
+ docs.](https://www.vaultproject.io/api/secret/aws/index.html#generate-credentials)
+ If you set this flag, you must also set the below options:
+ - `name` (string) - Required. Specifies the name of the role to generate
+ credentials against. This is part of the request URL.
+ - `engine_name` (string) - The name of the aws secrets engine. In the
+ Vault docs, this is normally referred to as "aws", and Packer will
+ default to "aws" if `engine_name` is not set.
+ - `role_arn` (string)- The ARN of the role to assume if credential\_type
+ on the Vault role is assumed\_role. Must match one of the allowed role
+ ARNs in the Vault role. Optional if the Vault role only allows a single
+ AWS role ARN; required otherwise.
+ - `ttl` (string) - Specifies the TTL for the use of the STS token. This
+ is specified as a string with a duration suffix. Valid only when
+ credential\_type is assumed\_role or federation\_token. When not
+ specified, the default\_sts\_ttl set for the role will be used. If that
+ is also not set, then the default value of 3600s will be used. AWS
+ places limits on the maximum TTL allowed. See the AWS documentation on
+ the DurationSeconds parameter for AssumeRole (for assumed\_role
+ credential types) and GetFederationToken (for federation\_token
+ credential types) for more details.
+
+ Example:
+ `json { "vault_aws_engine": { "name": "myrole", "role_arn": "myarn", "ttl": "3600s" } }`
+
- `vpc_id` (string) - If launching into a VPC subnet, Packer needs the VPC ID
in order to create a temporary security group within the VPC. Requires
`subnet_id` to be set. If this field is left blank, Packer will try to get
diff --git a/website/source/docs/builders/amazon.html.md b/website/source/docs/builders/amazon.html.md
index 931ff86ae..bbab565e8 100644
--- a/website/source/docs/builders/amazon.html.md
+++ b/website/source/docs/builders/amazon.html.md
@@ -151,6 +151,7 @@ for Packer to work:
"ec2:DescribeImageAttribute",
"ec2:DescribeImages",
"ec2:DescribeInstances",
+ "ec2:DescribeInstanceStatus",
"ec2:DescribeRegions",
"ec2:DescribeSecurityGroups",
"ec2:DescribeSnapshots",
@@ -218,9 +219,9 @@ fail. If that's the case, you might see an error like this:
==> amazon-ebs: Error querying AMI: AuthFailure: AWS was not able to validate the provided access credentials
If you suspect your system's date is wrong, you can compare it against
-. On Linux/OS X, you can run the `date` command to get
-the current time. If you're on Linux, you can try setting the time with ntp by
-running `sudo ntpd -q`.
+. On
+Linux/OS X, you can run the `date` command to get the current time. If you're
+on Linux, you can try setting the time with ntp by running `sudo ntpd -q`.
### `exceeded wait attempts` while waiting for tasks to complete
diff --git a/website/source/docs/builders/azure.html.md b/website/source/docs/builders/azure.html.md
index 8caf44848..a4b86d2c7 100644
--- a/website/source/docs/builders/azure.html.md
+++ b/website/source/docs/builders/azure.html.md
@@ -206,8 +206,8 @@ Providing `temp_resource_group_name` or `location` in combination with
- `os_disk_size_gb` (number) Specify the size of the OS disk in GB
(gigabytes). Values of zero or less than zero are ignored.
-- `disk_caching_type` (string) Specify the disk caching type. Valid values are None, ReadOnly,
- and ReadWrite. The default value is ReadWrite.
+- `disk_caching_type` (string) Specify the disk caching type. Valid values
+ are None, ReadOnly, and ReadWrite. The default value is ReadWrite.
- `disk_additional_size` (array of integers) - The size(s) of any additional
hard disks for the VM in gigabytes. If this is not specified then the VM
@@ -325,13 +325,14 @@ Providing `temp_resource_group_name` or `location` in combination with
value and defaults to false. **Important** Setting this true means that
your builds are faster, however any failed deletes are not reported.
-- `managed_image_os_disk_snapshot_name` (string) If managed\_image\_os\_disk\_snapshot\_name
- is set, a snapshot of the OS disk is created with the same name as this value before the
- VM is captured.
+- `managed_image_os_disk_snapshot_name` (string) If
+ managed\_image\_os\_disk\_snapshot\_name is set, a snapshot of the OS disk
+ is created with the same name as this value before the VM is captured.
-- `managed_image_data_disk_snapshot_prefix` (string) If managed\_image\_data\_disk\_snapshot\_prefix
- is set, snapshot of the data disk(s) is created with the same prefix as this value before the VM
- is captured.
+- `managed_image_data_disk_snapshot_prefix` (string) If
+ managed\_image\_data\_disk\_snapshot\_prefix is set, snapshot of the data
+ disk(s) is created with the same prefix as this value before the VM is
+ captured.
## Basic Example
@@ -403,14 +404,14 @@ here](https://technet.microsoft.com/en-us/library/hh824815.aspx)
}
```
-The Windows Guest Agent participates in the Sysprep process. The
-agent must be fully installed before the VM can be sysprep'ed. To
-ensure this is true all agent services must be running before
-executing sysprep.exe. The above JSON snippet shows one way to do this
-in the PowerShell provisioner. This snippet is **only** required if
-the VM is configured to install the agent, which is the default. To
-learn more about disabling the Windows Guest Agent please see [Install the VM Agent](https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/agent-windows#install-the-vm-agent).
-
+The Windows Guest Agent participates in the Sysprep process. The agent must be
+fully installed before the VM can be sysprep'ed. To ensure this is true all
+agent services must be running before executing sysprep.exe. The above JSON
+snippet shows one way to do this in the PowerShell provisioner. This snippet is
+**only** required if the VM is configured to install the agent, which is the
+default. To learn more about disabling the Windows Guest Agent please see
+[Install the VM
+Agent](https://docs.microsoft.com/en-us/azure/virtual-machines/extensions/agent-windows#install-the-vm-agent).
### Linux
diff --git a/website/source/docs/builders/docker.html.md b/website/source/docs/builders/docker.html.md
index 6612bb464..a0dddfc78 100644
--- a/website/source/docs/builders/docker.html.md
+++ b/website/source/docs/builders/docker.html.md
@@ -184,8 +184,8 @@ You must specify (only) one of `commit`, `discard`, or `export_path`.
`login_username`, and `login_password` will be ignored. For more
information see the [section on ECR](#amazon-ec2-container-registry).
-- `exec_user` (string) - Username or UID (format:
- <name\\\|uid>\[:<group\\\|gid>\]) to run remote commands with.
+- `exec_user` (string) - Username (UID) to run remote commands with. You can
+ also set the group name/ID if you want: (UID or UID:GID).
You may need this if you get permission errors trying to run the `shell` or
other provisioners.
@@ -380,8 +380,8 @@ portable provisioning scripts.
## Overriding the host directory
-By default, Packer creates a temporary folder under your home directory, and
-uses that to stage files for uploading into the container. If you would like to
+By default, Packer creates a temporary folder under your home directory, and
+uses that to stage files for uploading into the container. If you would like to
change the path to this temporary folder, you can set the `PACKER_TMP_DIR`.
This can be useful, for example, if you have your home directory permissions
set up to disallow access from the docker daemon.
diff --git a/website/source/docs/builders/googlecompute.html.md b/website/source/docs/builders/googlecompute.html.md
index f1d672adb..681fad15e 100644
--- a/website/source/docs/builders/googlecompute.html.md
+++ b/website/source/docs/builders/googlecompute.html.md
@@ -18,9 +18,8 @@ based on existing images.
It is possible to build images from scratch, but not with the `googlecompute`
Packer builder. The process is recommended only for advanced users, please see
-\[Building GCE Images from Scratch\]
-() and the
-[Google Compute Import
+[Building GCE Images from Scratch](https://cloud.google.com/compute/docs/tutorials/building-images)
+and the [Google Compute Import
Post-Processor](/docs/post-processors/googlecompute-import.html) for more
information.
@@ -139,7 +138,7 @@ using the gcloud command.
gcloud compute firewall-rules create allow-winrm --allow tcp:5986
Or alternatively by navigating to
-.
+https://console.cloud.google.com/networking/firewalls/list.
Once this is set up, the following is a complete working packer config after
setting a valid `account_file` and `project_id`:
diff --git a/website/source/docs/builders/hetzner-cloud.html.md b/website/source/docs/builders/hetzner-cloud.html.md
index 96e945521..71b710b1b 100644
--- a/website/source/docs/builders/hetzner-cloud.html.md
+++ b/website/source/docs/builders/hetzner-cloud.html.md
@@ -75,7 +75,9 @@ builder.
- `ssh_keys` (array of strings) - List of SSH keys by name or id to be added
to image on launch.
-- `rescue` (string) - Enable and boot in to the specified rescue system. This enables simple installation of custom operating systems. `linux64` `linux32` or `freebsd64`
+- `rescue` (string) - Enable and boot in to the specified rescue system. This
+ enables simple installation of custom operating systems. `linux64`
+ `linux32` or `freebsd64`
## Basic Example
@@ -84,11 +86,13 @@ access tokens:
``` json
{
- "type": "hcloud",
- "token": "YOUR API KEY",
- "image": "ubuntu-18.04",
- "location": "nbg1",
- "server_type": "cx11",
- "ssh_username": "root"
+ "builders": [{
+ "type": "hcloud",
+ "token": "YOUR API KEY",
+ "image": "ubuntu-18.04",
+ "location": "nbg1",
+ "server_type": "cx11",
+ "ssh_username": "root"
+ }]
}
```
diff --git a/website/source/docs/builders/lxc.html.md b/website/source/docs/builders/lxc.html.md
index f17b20e5f..c94042856 100644
--- a/website/source/docs/builders/lxc.html.md
+++ b/website/source/docs/builders/lxc.html.md
@@ -19,7 +19,7 @@ as a tar.gz of the root file system.
The LXC builder requires a modern linux kernel and the `lxc` or `lxc1` package.
This builder does not work with LXD.
-~> Note: to build Centos images on a Debian family host, you will need the
+\~> Note: to build Centos images on a Debian family host, you will need the
`yum` package installed.
Some provisioners such as `ansible-local` get
confused when running in a container of a different family. E.G. it will
attempt to use `apt-get` to install packages, when running in a Centos
diff --git a/website/source/docs/builders/lxd.html.md b/website/source/docs/builders/lxd.html.md
index 2523f1541..45a2937a9 100644
--- a/website/source/docs/builders/lxd.html.md
+++ b/website/source/docs/builders/lxd.html.md
@@ -47,7 +47,7 @@ Below is a fully functioning example.
container. This can be a (local or remote) image (name or fingerprint).
E.G. `my-base-image`, `ubuntu-daily:x`, `08fababf6f27`, ...
- ~> Note: The builder may appear to pause if required to download a
+ \~> Note: The builder may appear to pause if required to download a
remote image, as they are usually 100-200MB. `/var/log/lxd/lxd.log` will
mention starting such downloads.
@@ -68,8 +68,8 @@ Below is a fully functioning example.
- `publish_properties` (map\[string\]string) - Pass key values to the publish
step to be set as properties on the output image. This is most helpful to
set the description, but can be used to set anything needed. See
- for more
- properties.
+ https://stgraber.org/2016/03/30/lxd-2-0-image-management-512/
+ for more properties.
- `launch_config` (map\[string\]string) - List of key/value pairs you wish to
pass to `lxc launch` via `--config`. Defaults to empty.
diff --git a/website/source/docs/builders/ncloud.html.md b/website/source/docs/builders/ncloud.html.md
index 1aacf633f..ddc91b257 100644
--- a/website/source/docs/builders/ncloud.html.md
+++ b/website/source/docs/builders/ncloud.html.md
@@ -90,7 +90,6 @@ Here is a basic example for windows server.
]
}
-
Here is a basic example for linux server.
{
diff --git a/website/source/docs/builders/openstack.html.md b/website/source/docs/builders/openstack.html.md
index c67460b4c..837409e9f 100644
--- a/website/source/docs/builders/openstack.html.md
+++ b/website/source/docs/builders/openstack.html.md
@@ -278,8 +278,8 @@ builder.
isn't specified, the default enforced by your OpenStack cluster will be
used.
-- `volume_size` (int) - Size of the Block Storage service volume in GB. If this
- isn't specified, it is set to source image min disk value (if set) or
+- `volume_size` (int) - Size of the Block Storage service volume in GB. If
+ this isn't specified, it is set to source image min disk value (if set) or
calculated from the source image bytes size. Note that in some cases this
needs to be specified, if `use_blockstorage_volume` is true.
diff --git a/website/source/docs/builders/oracle-classic.html.md b/website/source/docs/builders/oracle-classic.html.md
index e917abeda..1fef81830 100644
--- a/website/source/docs/builders/oracle-classic.html.md
+++ b/website/source/docs/builders/oracle-classic.html.md
@@ -141,9 +141,10 @@ If this is set, a few more options become available.
- `builder_communicator` (communicator) - This represents an
[`ssh communicator`](/docs/templates/communicator.html#ssh-communicator),
and can be configured as such. If you use a different builder image, you
- may need to change the `ssh_username`, for example. That might look like this:
+ may need to change the `ssh_username`, for example. That might look like
+ this:
- ```json
+ ``` json
{
"builders": [
{
@@ -170,7 +171,7 @@ If this is set, a few more options become available.
amount of memory, and other resources allocated to the builder instance.
Default: `oc3`.
-* `builder_upload_image_command` (string) - The command to run to upload the
+- `builder_upload_image_command` (string) - The command to run to upload the
image to Object Storage Classic. This is for advanced users only, and you
should consult the default in code to decide on the changes to make. For
most users the default should suffice.
@@ -272,7 +273,7 @@ attributes file:
Here is an example using a persistent volume. Note the `persistent_volume_size`
setting.
-```json
+``` json
{
"variables": {
"opc_username": "{{ env `OPC_USERNAME`}}",
diff --git a/website/source/docs/builders/scaleway.html.md b/website/source/docs/builders/scaleway.html.md
index e2ce32676..4678b4222 100644
--- a/website/source/docs/builders/scaleway.html.md
+++ b/website/source/docs/builders/scaleway.html.md
@@ -48,8 +48,8 @@ builder.
- `image` (string) - The UUID of the base image to use. This is the image
that will be used to launch a new server and provision it. See
- get the complete list of the
- accepted image UUID.
+ [the images list](https://api-marketplace.scaleway.com/images)
+ get the complete list of the accepted image UUID.
- `region` (string) - The name of the region to launch the server in (`par1`
or `ams1`). Consequently, this is the region where the snapshot will be
diff --git a/website/source/docs/builders/triton.html.md b/website/source/docs/builders/triton.html.md
index df5173022..e3b003190 100644
--- a/website/source/docs/builders/triton.html.md
+++ b/website/source/docs/builders/triton.html.md
@@ -30,7 +30,7 @@ This reusable image can then be used to launch new machines.
The builder does *not* manage images. Once it creates an image, it is up to you
to use it or delete it.
-~> **Private installations of Triton must have custom images enabled!** To
+\~> **Private installations of Triton must have custom images enabled!** To
use the Triton builder with a private/on-prem installation of Joyent's Triton
software, you'll need an operator to manually [enable custom
images](https://docs.joyent.com/private-cloud/install/image-management) after
diff --git a/website/source/docs/builders/vagrant.html.md b/website/source/docs/builders/vagrant.html.md
new file mode 100644
index 000000000..fc93d00c9
--- /dev/null
+++ b/website/source/docs/builders/vagrant.html.md
@@ -0,0 +1,101 @@
+The Vagrant builder is intended for building new boxes from already-existing
+boxes. Your source should be a URL or path to a .box file or a Vagrant Cloud
+box name such as `hashicorp/precise64`.
+
+Packer will not install vagrant, nor will it install the underlying
+virtualization platforms or extra providers; We expect when you run this
+builder that you have already installed what you need.
+
+By default, this builder will initialize a new Vagrant workspace, launch your
+box from that workspace, provision it, call `vagrant package` to package it
+into a new box, and then destroy the original box. Please note that vagrant
+will _not_ remove the box file from your system (we don't call
+`vagrant box remove`).
+
+You can change the behavior so that the builder doesn't destroy the box by
+setting the `teardown_method` option. You can change the behavior so the builder
+doesn't package it (not all provisioners support the `vagrant package` command)
+by setting the `skip package` option. You can also change the behavior so that
+rather than inititalizing a new Vagrant workspace, you use an already defined
+one, by using `global_id` instead of `source_box`.
+
+Required:
+
+- `source_path` (string) - URL of the vagrant box to use, or the name of the
+ vagrant box. `hashicorp/precise64`, `./mylocalbox.box` and
+ `https://example.com/my-box.box` are all valid source boxes. If your
+ source is a .box file, whether locally or from a URL like the latter example
+ above, you will also need to provide a `box_name`. This option is required,
+ unless you set `global_id`. You may only set one or the other, not both.
+
+ or
+
+- `global_id` (string) - the global id of a Vagrant box already added to Vagrant
+ on your system. You can find the global id of your Vagrant boxes using the
+ command `vagrant global-status`; your global_id will be a 7-digit number and
+ letter comination that you'll find in the leftmost column of the
+ global-status output. If you choose to use `global_id` instead of
+ `source_box`, Packer will skip the Vagrant initialize and add steps, and
+ simply launch the box directly using the global id.
+
+Optional:
+
+- `output_dir` (string) - The directory to create that will contain
+ your output box. We always create this directory and run from inside of it to
+ prevent Vagrant init collisions. If unset, it will be set to `packer-` plus
+ your buildname.
+
+- `box_name` (string) - if your source\_box is a boxfile that we need to add
+ to Vagrant, this is the name to give it. If left blank, will default to
+ "packer_" plus your buildname.
+
+- `checksum` (string) - The checksum for the .box file. The type of the
+ checksum is specified with `checksum_type`, documented below.
+
+- `checksum_type` (string) - The type of the checksum specified in `checksum`.
+ Valid values are `none`, `md5`, `sha1`, `sha256`, or `sha512`. Although the
+ checksum will not be verified when `checksum_type` is set to "none", this is
+ not recommended since OVA files can be very large and corruption does happen
+ from time to time.
+
+- `vagrantfile_template` (string) - a path to a golang template for a
+ vagrantfile. Our default template can be found
+ [here](https://github.com/hashicorp/packer/tree/master/builder/vagrant/step_initialize_vagrant.go#L23-L30). So far the only template variables available to you are {{ .BoxName }} and
+ {{ .SyncedFolder }}, which correspond to the Packer options `box_name` and
+ `synced_folder`
+
+- `skip_add` (string) - Don't call "vagrant add" to add the box to your local
+ environment; this is necesasry if you want to launch a box that is already
+ added to your vagrant environment.
+
+- `teardown_method` (string) - Whether to halt, suspend, or destroy the box when
+ the build has completed. Defaults to "halt"
+
+- `box_version` (string) - What box version to use when initializing Vagrant.
+
+- `add_cacert` (string) - Equivalent to setting the
+ [`--cacert`](https://www.vagrantup.com/docs/cli/box.html#cacert-certfile)
+ option in `vagrant add`; defaults to unset.
+
+- `add_capath` (string) - Equivalent to setting the
+ [`--capath`](https://www.vagrantup.com/docs/cli/box.html#capath-certdir) option
+ in `vagrant add`; defaults to unset.
+
+- `add_cert` (string) - Equivalent to setting the
+ [`--cert`](https://www.vagrantup.com/docs/cli/box.html#cert-certfile) option in
+ `vagrant add`; defaults to unset.
+
+- `add_clean` (bool) - Equivalent to setting the
+ [`--clean`](https://www.vagrantup.com/docs/cli/box.html#clean) flag in
+ `vagrant add`; defaults to unset.
+
+- `add_force` (bool) - Equivalent to setting the
+ [`--force`](https://www.vagrantup.com/docs/cli/box.html#force) flag in
+ `vagrant add`; defaults to unset.
+
+- `add_insecure` (bool) - Equivalent to setting the
+ [`--force`](https://www.vagrantup.com/docs/cli/box.html#insecure) flag in
+ `vagrant add`; defaults to unset.
+
+- `skip_package` (bool) - if true, Packer will not call `vagrant package` to
+ package your base box into its own standalone .box file.
diff --git a/website/source/docs/builders/virtualbox-iso.html.md.erb b/website/source/docs/builders/virtualbox-iso.html.md.erb
index 4337cb7c8..e7ec0b84d 100644
--- a/website/source/docs/builders/virtualbox-iso.html.md.erb
+++ b/website/source/docs/builders/virtualbox-iso.html.md.erb
@@ -164,6 +164,12 @@ builder.
- `format` (string) - Either `ovf` or `ova`, this specifies the output format
of the exported virtual machine. This defaults to `ovf`.
+- `guest_additions_interface` (string) - The interface type to use to mount
+ guest additions when `guest_additions_mode` is set to `attach`. Will
+ default to the value set in `iso_interface`, if `iso_interface` is set.
+ Will default to "ide", if `iso_interface` is not set. Options are "ide" and
+ "sata".
+
- `guest_additions_mode` (string) - The method by which guest additions are
made available to the guest for installation. Valid options are `upload`,
`attach`, or `disable`. If the mode is `attach` the guest additions ISO will
diff --git a/website/source/docs/builders/virtualbox-ovf.html.md.erb b/website/source/docs/builders/virtualbox-ovf.html.md.erb
index d3ca4d7ae..379e5cb8a 100644
--- a/website/source/docs/builders/virtualbox-ovf.html.md.erb
+++ b/website/source/docs/builders/virtualbox-ovf.html.md.erb
@@ -152,6 +152,10 @@ builder.
- `format` (string) - Either `ovf` or `ova`, this specifies the output format
of the exported virtual machine. This defaults to `ovf`.
+- `guest_additions_interface` (string) - The interface type to use to mount
+ guest additions when `guest_additions_mode` is set to `attach`. Will
+ default to "ide" if not set. Options are "ide" and "sata".
+
- `guest_additions_mode` (string) - The method by which guest additions are
made available to the guest for installation. Valid options are `upload`,
`attach`, or `disable`. If the mode is `attach` the guest additions ISO will
diff --git a/website/source/docs/commands/index.html.md b/website/source/docs/commands/index.html.md
index 8f71e386c..2aeecde6f 100644
--- a/website/source/docs/commands/index.html.md
+++ b/website/source/docs/commands/index.html.md
@@ -58,7 +58,7 @@ The format will be covered in more detail later. But as you can see, the output
immediately becomes machine-friendly. Try some other commands with the
`-machine-readable` flag to see!
-~> The `-machine-readable` flag is designed for automated environments and
+\~> The `-machine-readable` flag is designed for automated environments and
is mutually-exclusive with the `-debug` flag, which is designed for interactive
environments.
diff --git a/website/source/docs/extending/custom-builders.html.md b/website/source/docs/extending/custom-builders.html.md
index 6d1cbe03a..302401533 100644
--- a/website/source/docs/extending/custom-builders.html.md
+++ b/website/source/docs/extending/custom-builders.html.md
@@ -19,7 +19,7 @@ plugin interface, and this page documents how to do that.
Prior to reading this page, it is assumed you have read the page on [plugin
development basics](/docs/extending/plugins.html).
-~> **Warning!** This is an advanced topic. If you're new to Packer, we
+\~> **Warning!** This is an advanced topic. If you're new to Packer, we
recommend getting a bit more comfortable before you dive into writing plugins.
## The Interface
diff --git a/website/source/docs/extending/custom-post-processors.html.md b/website/source/docs/extending/custom-post-processors.html.md
index b177cf0a3..0ec5f71a5 100644
--- a/website/source/docs/extending/custom-post-processors.html.md
+++ b/website/source/docs/extending/custom-post-processors.html.md
@@ -24,7 +24,7 @@ development basics](/docs/extending/plugins.html).
Post-processor plugins implement the `packer.PostProcessor` interface and are
served using the `plugin.ServePostProcessor` function.
-~> **Warning!** This is an advanced topic. If you're new to Packer, we
+\~> **Warning!** This is an advanced topic. If you're new to Packer, we
recommend getting a bit more comfortable before you dive into writing plugins.
## The Interface
diff --git a/website/source/docs/extending/custom-provisioners.html.md b/website/source/docs/extending/custom-provisioners.html.md
index 75f412e4f..3d3a1721d 100644
--- a/website/source/docs/extending/custom-provisioners.html.md
+++ b/website/source/docs/extending/custom-provisioners.html.md
@@ -23,7 +23,7 @@ development basics](/docs/extending/plugins.html).
Provisioner plugins implement the `packer.Provisioner` interface and are served
using the `plugin.ServeProvisioner` function.
-~> **Warning!** This is an advanced topic. If you're new to Packer, we
+\~> **Warning!** This is an advanced topic. If you're new to Packer, we
recommend getting a bit more comfortable before you dive into writing plugins.
## The Interface
diff --git a/website/source/docs/extending/plugins.html.md b/website/source/docs/extending/plugins.html.md
index 1a027c4b4..901bfc91b 100644
--- a/website/source/docs/extending/plugins.html.md
+++ b/website/source/docs/extending/plugins.html.md
@@ -56,7 +56,8 @@ later, it will take precedence over one found earlier.
3. The `%APPDATA%/packer.d/plugins` if `%APPDATA%` is defined (windows)
-4. The `%USERPROFILE%/packer.d/plugins` if `%USERPROFILE%` is defined (windows)
+4. The `%USERPROFILE%/packer.d/plugins` if `%USERPROFILE%` is defined
+ (windows)
5. The current working directory.
@@ -83,7 +84,7 @@ assumed that you're familiar with the language. This page will not be a Go
language tutorial. Thankfully, if you are familiar with Go, the Go toolchain
provides many conveniences to help to develop Packer plugins.
-~> **Warning!** This is an advanced topic. If you're new to Packer, we
+\~> **Warning!** This is an advanced topic. If you're new to Packer, we
recommend getting a bit more comfortable before you dive into writing plugins.
### Plugin System Architecture
@@ -159,7 +160,7 @@ using standard installation procedures.
The specifics of how to implement each type of interface are covered in the
relevant subsections available in the navigation to the left.
-~> **Lock your dependencies!** Using `govendor` is highly recommended since
+\~> **Lock your dependencies!** Using `govendor` is highly recommended since
the Packer codebase will continue to improve, potentially breaking APIs along
the way until there is a stable release. By locking your dependencies, your
plugins will continue to work with the version of Packer you lock to.
diff --git a/website/source/docs/post-processors/alicloud-import.html.md b/website/source/docs/post-processors/alicloud-import.html.md
index c421883d7..7f26c42d7 100644
--- a/website/source/docs/post-processors/alicloud-import.html.md
+++ b/website/source/docs/post-processors/alicloud-import.html.md
@@ -42,7 +42,7 @@ are two categories: required and optional parameters.
- `image_name` (string) - The name of the user-defined image, \[2, 128\]
English or Chinese characters. It must begin with an uppercase/lowercase
letter or a Chinese character, and may contain numbers, `_` or `-`. It
- cannot begin with or .
+ cannot begin with `http://` or `https://`
- `oss_bucket_name` (string) - The name of the OSS bucket where the RAW or
VHD file will be copied to for import. If the Bucket isn't exist,
@@ -52,8 +52,8 @@ are two categories: required and optional parameters.
- `image_platform` (string) - platform such `CentOS`
-- `image_architecture` (string) - Platform type of the image system:i386 \|
- x86\_64
+- `image_architecture` (string) - Platform type of the image system: `i386` or
+ `x86_64`
- `format` (string) - The format of the image for import, now alicloud only
support RAW and VHD.
@@ -70,7 +70,7 @@ are two categories: required and optional parameters.
- `image_description` (string) - The description of the image, with a length
limit of 0 to 256 characters. Leaving it blank means null, which is the
- default value. It cannot begin with or .
+ default value. It cannot begin with `http://` or `https://`.
- `image_force_delete` (boolean) - If this value is true, when the target
image name is duplicated with an existing image, it will delete the
@@ -79,9 +79,9 @@ are two categories: required and optional parameters.
- `image_system_size` (number) - Size of the system disk, in GB, values
range:
- - cloud - 5 ~ 2000
- - cloud\_efficiency - 20 ~ 2048
- - cloud\_ssd - 20 ~ 2048
+ - cloud - 5 \~ 2000
+ - cloud\_efficiency - 20 \~ 2048
+ - cloud\_ssd - 20 \~ 2048
## Basic Example
diff --git a/website/source/docs/post-processors/amazon-import.html.md b/website/source/docs/post-processors/amazon-import.html.md
index 700542a40..34b6c01a7 100644
--- a/website/source/docs/post-processors/amazon-import.html.md
+++ b/website/source/docs/post-processors/amazon-import.html.md
@@ -14,7 +14,7 @@ Type: `amazon-import`
The Packer Amazon Import post-processor takes an OVA artifact from various
builders and imports it to an AMI available to Amazon Web Services EC2.
-~> This post-processor is for advanced users. It depends on specific IAM
+\~> This post-processor is for advanced users. It depends on specific IAM
roles inside AWS and is best used with images that operate with the EC2
configuration model (eg, cloud-init for Linux systems). Please ensure you read
the [prerequisites for
@@ -85,12 +85,13 @@ Optional:
provider whose API is compatible with aws EC2. Specify another endpoint
like this `https://ec2.custom.endpoint.com`.
-- `format` (string) - One of: `ova`, `raw`, `vhd`, `vhdx`, or `vmdk`. This specifies
- the format of the source virtual machine image. The resulting artifact from the builder
- is assumed to have a file extension matching the format. This defaults to `ova`.
+- `format` (string) - One of: `ova`, `raw`, `vhd`, `vhdx`, or `vmdk`. This
+ specifies the format of the source virtual machine image. The resulting
+ artifact from the builder is assumed to have a file extension matching the
+ format. This defaults to `ova`.
-- `insecure_skip_tls_verify` (boolean) - This allows skipping TLS verification of
- the AWS EC2 endpoint. The default is `false`.
+- `insecure_skip_tls_verify` (boolean) - This allows skipping TLS
+ verification of the AWS EC2 endpoint. The default is `false`.
- `license_type` (string) - The license type to be used for the Amazon
Machine Image (AMI) after importing. Valid values: `AWS` or `BYOL`
@@ -185,6 +186,22 @@ using ovftool.
]
```
+## Amazon Permissions
+
+You'll need at least the following permissions in the policy for your IAM user
+in order to successfully upload an image via the amazon-import post-processor.
+``` json
+ "ec2:CancelImportTask",
+ "ec2:CopyImage",
+ "ec2:CreateTags",
+ "ec2:DescribeImages",
+ "ec2:DescribeImportImageTasks",
+ "ec2:ImportImage",
+ "ec2:ModifyImageAttribute"
+ "ec2:DeregisterImage"
+```
+
+
## Troubleshooting Timeouts
The amazon-import feature can take a long time to upload and convert your OVAs
diff --git a/website/source/docs/post-processors/digitalocean-import.html.md b/website/source/docs/post-processors/digitalocean-import.html.md
new file mode 100644
index 000000000..3120b2acb
--- /dev/null
+++ b/website/source/docs/post-processors/digitalocean-import.html.md
@@ -0,0 +1,100 @@
+---
+description: |
+ The Packer DigitalOcean Import post-processor takes an image artifact
+ from various builders and imports it to DigitalOcean.
+layout: docs
+page_title: 'DigitalOcean Import - Post-Processors'
+sidebar_current: 'docs-post-processors-digitalocean-import'
+---
+
+# DigitalOcean Import Post-Processor
+
+Type: `digitalocean-import`
+
+The Packer DigitalOcean Import post-processor takes an image artifact from
+various builders and imports it to DigitalOcean.
+
+## How Does it Work?
+
+The import process operates uploading a temporary copy of the image to
+DigitalOcean Spaces and then importing it as a custom image via the
+DigialOcean API. The temporary copy in Spaces can be discarded after the
+import is complete.
+
+For information about the requirements to use an image for a DigitalOcean
+Droplet, see DigitalOcean's [Custom Images documentation](https://www.digitalocean.com/docs/images/custom-images/overview/).
+
+## Configuration
+
+There are some configuration options available for the post-processor.
+
+Required:
+
+- `api_token` (string) - A personal access token used to communicate with
+ the DigitalOcean v2 API. This may also be set using the
+ `DIGITALOCEAN_API_TOKEN` environmental variable.
+
+- `spaces_key` (string) - The access key used to communicate with Spaces.
+ This may also be set using the `DIGITALOCEAN_SPACES_ACCESS_KEY`
+ environmental variable.
+
+- `spaces_secret` (string) - The secret key used to communicate with Spaces.
+ This may also be set using the `DIGITALOCEAN_SPACES_SECRET_KEY`
+ environmental variable.
+
+- `spaces_region` (string) - The name of the region, such as `nyc3`, in which
+ to upload the image to Spaces.
+
+- `space_name` (string) - The name of the specific Space where the image file
+ will be copied to for import. This Space must exist when the
+ post-processor is run.
+
+- `image_name` (string) - The name to be used for the resulting DigitalOcean
+ custom image.
+
+- `image_regions` (array of string) - A list of DigitalOcean regions, such
+ as `nyc3`, where the resulting image will be available for use in creating
+ Droplets.
+
+Optional:
+
+- `image_description` (string) - The description to set for the resulting
+ imported image.
+
+- `image_distribution` (string) - The name of the distribution to set for
+ the resulting imported image.
+
+- `image_tags` (array of strings) - A list of tags to apply to the resulting
+ imported image.
+
+- `skip_clean` (boolean) - Whether we should skip removing the image file
+ uploaded to Spaces after the import process has completed. "true" means
+ that we should leave it in the Space, "false" means to clean it out.
+ Defaults to `false`.
+
+- `space_object_name` (string) - The name of the key used in the Space where
+ the image file will be copied to for import. If not specified, this will default to "packer-import-{{timestamp}}".
+
+- `timeout` (number) - The length of time in minutes to wait for individual
+ steps in the process to successfully complete. This includes both importing
+ the image from Spaces as well as distributing the resulting image to
+ additional regions. If not specified, this will default to 20.
+
+## Basic Example
+
+Here is a basic example:
+
+``` json
+{
+ "type": "digitalocean-import",
+ "api_token": "{{user `token`}}",
+ "spaces_key": "{{user `key`}}",
+ "spaces_secret": "{{user `secret`}}",
+ "spaces_region": "nyc3",
+ "space_name": "import-bucket",
+ "image_name": "ubuntu-18.10-minimal-amd64",
+ "image_description": "Packer import {{timestamp}}",
+ "image_regions": ["nyc3", "nyc2"],
+ "image_tags": ["custom", "packer"]
+}
+```
diff --git a/website/source/docs/post-processors/docker-import.html.md b/website/source/docs/post-processors/docker-import.html.md
index 8802a18e7..4fb3b0fe3 100644
--- a/website/source/docs/post-processors/docker-import.html.md
+++ b/website/source/docs/post-processors/docker-import.html.md
@@ -38,7 +38,6 @@ is optional.
commit. Example of instructions are `CMD`, `ENTRYPOINT`, `ENV`, and
`EXPOSE`. Example: `[ "USER ubuntu", "WORKDIR /app", "EXPOSE 8080" ]`
-
## Example
An example is shown below, showing only the post-processor configuration:
@@ -61,9 +60,9 @@ to a registry, if you want.
## Changing Metadata
Below is an example using the changes argument of the post-processor. This
-feature allows the tarball metadata to be changed when imported into the
-Docker environment. It is derived from the `docker import --change` command
-line [option to
+feature allows the tarball metadata to be changed when imported into the Docker
+environment. It is derived from the `docker import --change` command line
+[option to
Docker](https://docs.docker.com/engine/reference/commandline/import/).
Example uses of all of the options, assuming one is building an NGINX image
diff --git a/website/source/docs/post-processors/googlecompute-import.html.md b/website/source/docs/post-processors/googlecompute-import.html.md
index 4676dfd3c..c7ff21f09 100644
--- a/website/source/docs/post-processors/googlecompute-import.html.md
+++ b/website/source/docs/post-processors/googlecompute-import.html.md
@@ -14,7 +14,7 @@ Type: `googlecompute-import`
The Google Compute Image Import post-processor takes a compressed raw disk
image and imports it to a GCE image available to Google Compute Engine.
-~> This post-processor is for advanced users. Please ensure you read the
+\~> This post-processor is for advanced users. Please ensure you read the
[GCE import
documentation](https://cloud.google.com/compute/docs/images/import-existing-image)
before using this post-processor.
diff --git a/website/source/docs/post-processors/shell-local.html.md b/website/source/docs/post-processors/shell-local.html.md
index 279548b10..7599febf2 100644
--- a/website/source/docs/post-processors/shell-local.html.md
+++ b/website/source/docs/post-processors/shell-local.html.md
@@ -60,14 +60,13 @@ Optional parameters:
Packer injects some environmental variables by default into the
environment, as well, which are covered in the section below.
-- `env_var_format` (string) - When we parse the environment_vars that you
+- `env_var_format` (string) - When we parse the environment\_vars that you
provide, this gives us a string template to use in order to make sure that
we are setting the environment vars correctly. By default on Windows hosts
- this format is `set %s=%s && ` and on Unix, it is `%s='%s' `. You probably
+ this format is `set %s=%s && ` and on Unix, it is `%s='%s' `. You probably
won't need to change this format, but you can see usage examples for where
it is necessary below.
-
- `execute_command` (array of strings) - The command used to execute the
script. By default this is `["/bin/sh", "-c", "{{.Vars}}", "{{.Script}}"]`
on unix and `["cmd", "/c", "{{.Vars}}", "{{.Script}}"]` on windows. This is
@@ -250,10 +249,10 @@ are cleaned up.
For a shell script, that means the script **must** exit with a zero code. You
*must* be extra careful to `exit 0` when necessary.
-
## Usage Examples:
### Windows Host
+
Example of running a .cmd file on windows:
{
@@ -295,7 +294,6 @@ Contents of "example\_bash.sh":
Example of running a powershell script on windows: Required customizations:
env\_var\_format and execute\_command
-
{
"type": "shell-local",
"environment_vars": ["SHELLLOCALTEST=ShellTest4"],
@@ -317,6 +315,7 @@ customizations: env\_var\_format, tempfile\_extension, and execute\_command
}
### Unix Host
+
Example of running a bash script on unix:
{
@@ -336,19 +335,15 @@ Example of running a bash "inline" on unix:
Example of running a python script on unix:
-```
- {
- "type": "shell-local",
- "script": "hello.py",
- "environment_vars": ["HELLO_USER=packeruser"],
- "execute_command": ["/bin/sh", "-c", "{{.Vars}} /usr/local/bin/python {{.Script}}"]
- }
-```
+ {
+ "type": "shell-local",
+ "script": "hello.py",
+ "environment_vars": ["HELLO_USER=packeruser"],
+ "execute_command": ["/bin/sh", "-c", "{{.Vars}} /usr/local/bin/python {{.Script}}"]
+ }
Where "hello.py" contains:
-```
-import os
+ import os
-print('Hello, %s!' % os.getenv("HELLO_USER"))
-```
\ No newline at end of file
+ print('Hello, %s!' % os.getenv("HELLO_USER"))
diff --git a/website/source/docs/post-processors/vagrant.html.md b/website/source/docs/post-processors/vagrant.html.md
index c0ccf6241..253472ed3 100644
--- a/website/source/docs/post-processors/vagrant.html.md
+++ b/website/source/docs/post-processors/vagrant.html.md
@@ -144,5 +144,5 @@ The following Docker input artifacts are supported:
### QEMU/libvirt
-The `libvirt` provider supports QEMU artifacts built using any these accelerators: none,
-kvm, tcg, or hvf.
+The `libvirt` provider supports QEMU artifacts built using any these
+accelerators: none, kvm, tcg, or hvf.
diff --git a/website/source/docs/post-processors/vsphere-template.html.md b/website/source/docs/post-processors/vsphere-template.html.md
index 9057aca14..a3ef0e34e 100644
--- a/website/source/docs/post-processors/vsphere-template.html.md
+++ b/website/source/docs/post-processors/vsphere-template.html.md
@@ -60,16 +60,16 @@ Optional:
- `insecure` (boolean) - If it's true skip verification of server
certificate. Default is false
-
-- `snapshot_enable` (boolean) - Create a snapshot before marking as a
+
+- `snapshot_enable` (boolean) - Create a snapshot before marking as a
template. Default is false
-
-- `snapshot_name` (string) - Name for the snapshot.
- Required when `snapshot_enable` is `true`
-
-- `snapshot_description` (string) - Description for the snapshot.
- Required when `snapshot_enable` is `true`
-
+
+- `snapshot_name` (string) - Name for the snapshot. Required when
+ `snapshot_enable` is `true`
+
+- `snapshot_description` (string) - Description for the snapshot. Required
+ when `snapshot_enable` is `true`
+
## Using the vSphere Template with local builders
Once the [vSphere](/docs/post-processors/vsphere.html) takes an artifact from
@@ -91,7 +91,11 @@ for more information):
"type": "vsphere-template",
...
}
- ]
+ ],
+ {
+ "type": "...",
+ ...
+ }
]
}
```
@@ -100,4 +104,5 @@ In the example above, the result of each builder is passed through the defined
sequence of post-processors starting with the `vsphere` post-processor which
will upload the artifact to a vSphere endpoint. The resulting artifact is then
passed on to the `vsphere-template` post-processor which handles marking a VM
-as a template.
+as a template. Note that the `vsphere` and `vsphere-template` post-processors
+are paired together in their own JSON array.
diff --git a/website/source/docs/provisioners/ansible-local.html.md b/website/source/docs/provisioners/ansible-local.html.md
index 5972cff1d..63cd15125 100644
--- a/website/source/docs/provisioners/ansible-local.html.md
+++ b/website/source/docs/provisioners/ansible-local.html.md
@@ -68,6 +68,7 @@ Optional:
example:
+
"extra_arguments": [ "--extra-vars \"Region={{user `Region`}} Stage={{user `Stage`}}\"" ]
- `inventory_groups` (string) - A comma-separated list of groups to which
diff --git a/website/source/docs/provisioners/breakpoint.html.md b/website/source/docs/provisioners/breakpoint.html.md
index edcc48405..d251e534b 100644
--- a/website/source/docs/provisioners/breakpoint.html.md
+++ b/website/source/docs/provisioners/breakpoint.html.md
@@ -1,8 +1,8 @@
---
description: |
- The breakpoint provisioner will pause until the user presses "enter" to
- resume the build. This is intended for debugging purposes, and allows you
- to halt at a particular part of the provisioning process.
+ The breakpoint provisioner will pause until the user presses "enter" to resume
+ the build. This is intended for debugging purposes, and allows you to halt at a
+ particular part of the provisioning process.
layout: docs
page_title: 'breakpoint - Provisioners'
sidebar_current: 'docs-provisioners-breakpoint'
@@ -12,9 +12,9 @@ sidebar_current: 'docs-provisioners-breakpoint'
Type: `breakpoint`
-The breakpoint provisioner will pause until the user presses "enter" to
-resume the build. This is intended for debugging purposes, and allows you
-to halt at a particular part of the provisioning process.
+The breakpoint provisioner will pause until the user presses "enter" to resume
+the build. This is intended for debugging purposes, and allows you to halt at a
+particular part of the provisioning process.
This is independent of the `-debug` flag, which will instead halt at every step
and between every provisioner.
@@ -33,8 +33,8 @@ and between every provisioner.
### Optional
- `disable` (boolean) - If `true`, skip the breakpoint. Useful for when you
- have set multiple breakpoints and want to toggle them off or on.
- Default: `false`
+ have set multiple breakpoints and want to toggle them off or on. Default:
+ `false`
- `note` (string) - a string to include explaining the purpose or location of
the breakpoint. For example, you may find it useful to number your
@@ -48,10 +48,8 @@ output prompting you to press "enter" to continue the build when you are ready.
For example:
-```
-==> docker: Pausing at breakpoint provisioner with note "foo bar baz".
-==> docker: Press enter to continue.
-```
+ ==> docker: Pausing at breakpoint provisioner with note "foo bar baz".
+ ==> docker: Press enter to continue.
Once you press enter, the build will resume and run normally until it either
-completes or errors.
\ No newline at end of file
+completes or errors.
diff --git a/website/source/docs/provisioners/inspec.html.md b/website/source/docs/provisioners/inspec.html.md
new file mode 100644
index 000000000..56809050d
--- /dev/null
+++ b/website/source/docs/provisioners/inspec.html.md
@@ -0,0 +1,131 @@
+---
+description: |
+ The inspec Packer provisioner allows inspec profiles to be run to test the
+ machine.
+layout: docs
+page_title: 'InSpec - Provisioners'
+sidebar_current: 'docs-provisioners-inspec'
+---
+
+# InSpec Provisioner
+
+Type: `inspec`
+
+The `inspec` Packer provisioner runs InSpec profiles. It dynamically creates a
+target configured to use SSH, runs an SSH server, executes `inspec exec`, and
+marshals InSpec tests through the SSH server to the machine being provisioned
+by Packer.
+
+## Basic Example
+
+This is a fully functional template that will test an image on DigitalOcean.
+Replace the mock `api_token` value with your own.
+
+``` json
+{
+ "provisioners": [
+ {
+ "type": "inspec",
+ "profile": "https://github.com/dev-sec/linux-baseline"
+ }
+ ],
+
+ "builders": [
+ {
+ "type": "digitalocean",
+ "api_token": "",
+ "image": "ubuntu-14-04-x64",
+ "region": "sfo1"
+ }
+ ]
+}
+```
+
+## Configuration Reference
+
+Required Parameters:
+
+- `profile` (string) - The profile to be executed by InSpec.
+
+Optional Parameters:
+
+- `inspec_env_vars` (array of strings) - Environment variables to set before
+ running InSpec. Usage example:
+
+ ``` json
+ "inspec_env_vars": [ "FOO=bar" ]
+ ```
+
+- `command` (string) - The command to invoke InSpec. Defaults to `inspec`.
+
+- `extra_arguments` (array of strings) - Extra arguments to pass to InSpec.
+ These arguments *will not* be passed through a shell and arguments should
+ not be quoted. Usage example:
+
+ ``` json
+ "extra_arguments": [ "--sudo", "--reporter", "json" ]
+ ```
+
+- `attributes` (array of strings) - Attribute Files used by InSpec which will
+ be passed to the `--attrs` argument of the `inspec` command when this
+ provisioner runs InSpec. Specify this if you want a different location.
+ Note using also `"--attrs"` in `extra_arguments` will override this
+ setting.
+
+- `attributes_directory` (string) - The directory in which to place the
+ temporary generated InSpec Attributes file. By default, this is the
+ system-specific temporary file location. The fully-qualified name of this
+ temporary file will be passed to the `--attrs` argument of the `inspec`
+ command when this provisioner runs InSpec. Specify this if you want a
+ different location.
+
+- `backend` (string) - Backend used by InSpec for connection. Defaults to
+ SSH.
+
+- `host` (string) - Host used for by InSpec for connection. Defaults to
+ localhost.
+
+- `local_port` (uint) - The port on which to attempt to listen for SSH
+ connections. This value is a starting point. The provisioner will attempt to
+ listen for SSH connections on the first available of ten ports, starting at
+ `local_port`. A system-chosen port is used when `local_port` is missing or
+ empty.
+
+- `ssh_host_key_file` (string) - The SSH key that will be used to run the SSH
+ server on the host machine to forward commands to the target machine.
+ InSpec connects to this server and will validate the identity of the server
+ using the system known\_hosts. The default behavior is to generate and use
+ a onetime key.
+
+- `ssh_authorized_key_file` (string) - The SSH public key of the InSpec
+ `ssh_user`. The default behavior is to generate and use a onetime key. If
+ this key is generated, the corresponding private key is passed to `inspec`
+ command with the `-i inspec_ssh_private_key_file` option.
+
+- `user` (string) - The `--user` to use. Defaults to the user running Packer.
+
+## Default Extra Variables
+
+In addition to being able to specify extra arguments using the
+`extra_arguments` configuration, the provisioner automatically defines certain
+commonly useful InSpec Attributes:
+
+- `packer_build_name` is set to the name of the build that Packer is running.
+ This is most useful when Packer is making multiple builds and you want to
+ distinguish them slightly when using a common profile.
+
+- `packer_builder_type` is the type of the builder that was used to create
+ the machine that the script is running on. This is useful if you want to
+ run only certain parts of the profile on systems built with certain
+ builders.
+
+## Debugging
+
+To debug underlying issues with InSpec, add `"-l"` to `"extra_arguments"` to
+enable verbose logging.
+
+``` json
+{
+ "extra_arguments": [ "-l", "debug" ]
+}
+```
diff --git a/website/source/docs/provisioners/puppet-masterless.html.md b/website/source/docs/provisioners/puppet-masterless.html.md
index e28a10e7d..41ced3fde 100644
--- a/website/source/docs/provisioners/puppet-masterless.html.md
+++ b/website/source/docs/provisioners/puppet-masterless.html.md
@@ -73,7 +73,8 @@ Optional parameters:
]
- `facter` (object of key:value strings) - Additional
- [facts](https://docs.puppet.com/facter/) to make available to the Puppet run.
+ [facts](https://docs.puppet.com/facter/) to make available to the Puppet
+ run.
- `guest_os_type` (string) - The remote host's OS type ('windows' or 'unix')
to tailor command-line and path separators. (default: unix).
@@ -88,9 +89,9 @@ Optional parameters:
This is useful if your main manifest uses imports, but the directory might
not contain the `manifest_file` itself.
-~> `manifest_dir` is passed to Puppet as `--manifestdir` option. This option
-was deprecated in puppet 3.6, and removed in puppet 4.0. If you have multiple
-manifests you should use `manifest_file` instead.
+\~> `manifest_dir` is passed to Puppet as `--manifestdir` option. This
+option was deprecated in puppet 3.6, and removed in puppet 4.0. If you have
+multiple manifests you should use `manifest_file` instead.
- `module_paths` (array of strings) - Array of local module directories to be
uploaded.
diff --git a/website/source/docs/provisioners/shell-local.html.md b/website/source/docs/provisioners/shell-local.html.md
index cf9b6cc60..3dc81fe66 100644
--- a/website/source/docs/provisioners/shell-local.html.md
+++ b/website/source/docs/provisioners/shell-local.html.md
@@ -74,10 +74,10 @@ Optional parameters:
this as an environment variable. For example:
`"environment_vars": "WINRMPASS={{.WinRMPassword}}"`
-- `env_var_format` (string) - When we parse the environment_vars that you
+- `env_var_format` (string) - When we parse the environment\_vars that you
provide, this gives us a string template to use in order to make sure that
we are setting the environment vars correctly. By default on Windows hosts
- this format is `set %s=%s && ` and on Unix, it is `%s='%s' `. You probably
+ this format is `set %s=%s && ` and on Unix, it is `%s='%s' `. You probably
won't need to change this format, but you can see usage examples for where
it is necessary below.
@@ -230,6 +230,7 @@ For a shell script, that means the script **must** exit with a zero code. You
## Usage Examples:
### Windows Host
+
Example of running a .cmd file on windows:
{
@@ -271,7 +272,6 @@ Contents of "example\_bash.sh":
Example of running a powershell script on windows: Required customizations:
env\_var\_format and execute\_command
-
{
"type": "shell-local",
"environment_vars": ["SHELLLOCALTEST=ShellTest4"],
@@ -293,6 +293,7 @@ customizations: env\_var\_format, tempfile\_extension, and execute\_command
}
### Unix Host
+
Example of running a bash script on unix:
{
@@ -312,19 +313,15 @@ Example of running a bash "inline" on unix:
Example of running a python script on unix:
-```
- {
- "type": "shell-local",
- "script": "hello.py",
- "environment_vars": ["HELLO_USER=packeruser"],
- "execute_command": ["/bin/sh", "-c", "{{.Vars}} /usr/local/bin/python {{.Script}}"]
- }
-```
+ {
+ "type": "shell-local",
+ "script": "hello.py",
+ "environment_vars": ["HELLO_USER=packeruser"],
+ "execute_command": ["/bin/sh", "-c", "{{.Vars}} /usr/local/bin/python {{.Script}}"]
+ }
Where "hello.py" contains:
-```
-import os
+ import os
-print('Hello, %s!' % os.getenv("HELLO_USER"))
-```
\ No newline at end of file
+ print('Hello, %s!' % os.getenv("HELLO_USER"))
diff --git a/website/source/docs/provisioners/windows-restart.html.md b/website/source/docs/provisioners/windows-restart.html.md
index b9ee04567..bb340efc8 100644
--- a/website/source/docs/provisioners/windows-restart.html.md
+++ b/website/source/docs/provisioners/windows-restart.html.md
@@ -41,10 +41,10 @@ Optional parameters:
- `check_registry` (bool) - if `true`, checks for several registry keys that
indicate that the system is going to reboot. This is useful if an
installation kicks off a reboot and you want the provisioner to wait for
- that reboot to complete before reconnecting. Please note that this option is
- a beta feature, and we generally recommend that you finish installs that
- auto-reboot (like windows updates) during your autounattend phase before our
- winrm provisioner connects.
+ that reboot to complete before reconnecting. Please note that this option
+ is a beta feature, and we generally recommend that you finish installs that
+ auto-reboot (like windows updates) during your autounattend phase before
+ our winrm provisioner connects.
- `registry_keys` (array of strings) - if `check-registry` is `true`,
windows-restart will not reconnect until after all of the listed keys are
@@ -52,13 +52,11 @@ Optional parameters:
default:
- ```
- var DefaultRegistryKeys = []string{
- "HKLM:SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootPending",
- "HKLM:SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\PackagesPending",
- "HKLM:Software\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootInProgress",
- }
- ```
+ var DefaultRegistryKeys = []string{
+ "HKLM:SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootPending",
+ "HKLM:SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\PackagesPending",
+ "HKLM:Software\\Microsoft\\Windows\\CurrentVersion\\Component Based Servicing\\RebootInProgress",
+ }
- `restart_command` (string) - The command to execute to initiate the
restart. By default this is `shutdown /r /f /t 0 /c "packer restart"`.
diff --git a/website/source/docs/templates/communicator.html.md b/website/source/docs/templates/communicator.html.md
index b2133f825..42a4c2e94 100644
--- a/website/source/docs/templates/communicator.html.md
+++ b/website/source/docs/templates/communicator.html.md
@@ -75,9 +75,9 @@ The SSH communicator has the following options:
- `ssh_bastion_port` (number) - The port of the bastion host. Defaults to
`22`.
-- `ssh_bastion_private_key_file` (string) - Path to a PEM encoded private
- key file to use to authenticate with the bastion host. The `~` can be used
- in path and will be expanded to the home directory of current user.
+- `ssh_bastion_private_key_file` (string) - Path to a PEM encoded private key
+ file to use to authenticate with the bastion host. The `~` can be used in
+ path and will be expanded to the home directory of current user.
- `ssh_bastion_username` (string) - The username to connect to the bastion
host.
@@ -155,23 +155,25 @@ Packer supports the following ciphers:
- arcfour128
- arcfour256
- arcfour
--
--
+- `es128-gcm@openssh.com`
+- `acha20-poly1305@openssh.com`
And the following MACs:
- hmac-sha1
- hmac-sha1-96
- hmac-sha2-256
--
+- `hmac-sha2-256-etm@openssh.com`
## WinRM Communicator
The WinRM communicator has the following options.
- `winrm_host` (string) - The address for WinRM to connect to.
-
- NOTE: If using an Amazon EBS builder, you can specify the interface WinRM connects to via [`ssh_interface`](https://www.packer.io/docs/builders/amazon-ebs.html#ssh_interface)
+
+ NOTE: If using an Amazon EBS builder, you can specify the interface WinRM
+ connects to via
+ [`ssh_interface`](https://www.packer.io/docs/builders/amazon-ebs.html#ssh_interface)
- `winrm_insecure` (boolean) - If `true`, do not check server certificate
chain and host name.
@@ -196,3 +198,22 @@ The WinRM communicator has the following options.
- `winrm_use_ssl` (boolean) - If `true`, use HTTPS for WinRM.
- `winrm_username` (string) - The username to use to connect to WinRM.
+
+## Pausing Before Connecting
+We recommend that you enable SSH or WinRM as the very last step in your
+guest's bootstrap script, but sometimes you may have a race condition where
+you need Packer to wait before attempting to connect to your guest.
+
+If you end up in this situation, you can use the template option
+`pause_before_connecting`. By default, there is no pause. For example:
+
+{
+ "communicator": "ssh"
+ "ssh_username": "myuser",
+ "pause_before_connecting": "10m"
+}
+
+In this example, Packer will wait 10 minutes before attempting to connect to
+the guest.
+
+
diff --git a/website/source/docs/templates/engine.html.md b/website/source/docs/templates/engine.html.md
index aae419098..1cebe5fc5 100644
--- a/website/source/docs/templates/engine.html.md
+++ b/website/source/docs/templates/engine.html.md
@@ -44,7 +44,8 @@ Here is a full list of the available functions for reference.
reference](/docs/templates/engine.html#isotime-function-format-reference).
- `lower` - Lowercases the string.
- `pwd` - The working directory while executing Packer.
-- `sed` - Use [a golang implementation of sed](https://github.com/rwtodd/Go.Sed) to parse an input string.
+- `sed` - Use [a golang implementation of
+ sed](https://github.com/rwtodd/Go.Sed) to parse an input string.
- `split` - Split an input string using separator and return the requested
substring.
- `template_dir` - The directory to the template for the build.
@@ -227,6 +228,7 @@ Formatting for the function `isotime` uses the magic reference date **Mon Jan 2
+
*The values in parentheses are the abbreviated, or 24-hour clock values*
Note that "-0700" is always formatted into "+0000" because `isotime` is always
@@ -272,10 +274,10 @@ builder in this example.
The function `split` takes an input string, a seperator string, and a numeric
component value and returns the requested substring.
-Please note that you cannot use the `split` function on user variables,
-because we can't nest the functions currently. This function is indended to
-be used on builder variables like build_name. If you need a split user
-variable, the best way to do it is to create a separate variable.
+Please note that you cannot use the `split` function on user variables, because
+we can't nest the functions currently. This function is indended to be used on
+builder variables like build\_name. If you need a split user variable, the best
+way to do it is to create a separate variable.
Here are some examples using the above options:
@@ -310,16 +312,15 @@ this case, on the `fixed-string` value):
# sed Function Format Reference
-See the library documentation https://github.com/rwtodd/Go.Sed for notes about
-the difference between this golang implementation of sed and the regexes you may
-be used to. A very simple example of this functionality:
+See the library documentation
+https://github.com/rwtodd/Go.Sed
+for notes about the difference between this golang implementation of sed and
+the regexes you may be used to. A very simple example of this functionality:
-```
- "provisioners": [
- {
- "type": "shell-local",
- "environment_vars": ["EXAMPLE={{ sed \"s/null/awesome/\" build_type}}"],
- "inline": ["echo build_type is $EXAMPLE\n"]
- }
- ]
-```
+ "provisioners": [
+ {
+ "type": "shell-local",
+ "environment_vars": ["EXAMPLE={{ sed \"s/null/awesome/\" build_type}}"],
+ "inline": ["echo build_type is $EXAMPLE\n"]
+ }
+ ]
diff --git a/website/source/layouts/docs.erb b/website/source/layouts/docs.erb
index 66889dc88..bd3971b59 100644
--- a/website/source/layouts/docs.erb
+++ b/website/source/layouts/docs.erb
@@ -222,6 +222,9 @@
>
File
+ >
+ InSpec
+
>
PowerShell
@@ -269,6 +272,9 @@
>
Checksum
+ >
+ DigitalOcean Import
+
>
Docker Import