remove digitalocean directories, revendor, add to vendored_plugins, regenerate code, and update website paths (#10961)

This commit is contained in:
Megan Marsh 2021-04-22 02:45:27 -07:00 committed by GitHub
parent d0a15f9a15
commit 6b59525408
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 24 additions and 2684 deletions

View File

@ -1,56 +0,0 @@
package digitalocean
import (
"context"
"fmt"
"log"
"strconv"
"strings"
"github.com/digitalocean/godo"
)
type Artifact struct {
// The name of the snapshot
SnapshotName string
// The ID of the image
SnapshotId int
// The name of the region
RegionNames []string
// The client for making API calls
Client *godo.Client
// StateData should store data such as GeneratedData
// to be shared with post-processors
StateData map[string]interface{}
}
func (*Artifact) BuilderId() string {
return BuilderId
}
func (*Artifact) Files() []string {
// No files with DigitalOcean
return nil
}
func (a *Artifact) Id() string {
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[:], ","))
}
func (a *Artifact) State(name string) interface{} {
return a.StateData[name]
}
func (a *Artifact) Destroy() error {
log.Printf("Destroying image: %d (%s)", a.SnapshotId, a.SnapshotName)
_, err := a.Client.Images.Delete(context.TODO(), a.SnapshotId)
return err
}

View File

@ -1,81 +0,0 @@
package digitalocean
import (
"testing"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
func generatedData() map[string]interface{} {
return make(map[string]interface{})
}
func TestArtifact_Impl(t *testing.T) {
var raw interface{}
raw = &Artifact{}
if _, ok := raw.(packersdk.Artifact); !ok {
t.Fatalf("Artifact should be artifact")
}
}
func TestArtifactId(t *testing.T) {
a := &Artifact{"packer-foobar", 42, []string{"sfo", "tor1"}, nil, generatedData()}
expected := "sfo,tor1:42"
if a.Id() != expected {
t.Fatalf("artifact ID should match: %v", expected)
}
}
func TestArtifactIdWithoutMultipleRegions(t *testing.T) {
a := &Artifact{"packer-foobar", 42, []string{"sfo"}, nil, generatedData()}
expected := "sfo:42"
if a.Id() != expected {
t.Fatalf("artifact ID should match: %v", expected)
}
}
func TestArtifactString(t *testing.T) {
a := &Artifact{"packer-foobar", 42, []string{"sfo", "tor1"}, nil, generatedData()}
expected := "A snapshot was created: 'packer-foobar' (ID: 42) in regions 'sfo,tor1'"
if a.String() != expected {
t.Fatalf("artifact string should match: %v", expected)
}
}
func TestArtifactStringWithoutMultipleRegions(t *testing.T) {
a := &Artifact{"packer-foobar", 42, []string{"sfo"}, nil, generatedData()}
expected := "A snapshot was created: 'packer-foobar' (ID: 42) in regions 'sfo'"
if a.String() != expected {
t.Fatalf("artifact string should match: %v", expected)
}
}
func TestArtifactState_StateData(t *testing.T) {
expectedData := "this is the data"
artifact := &Artifact{
StateData: map[string]interface{}{"state_data": expectedData},
}
// Valid state
result := artifact.State("state_data")
if result != expectedData {
t.Fatalf("Bad: State data was %s instead of %s", result, expectedData)
}
// Invalid state
result = artifact.State("invalid_key")
if result != nil {
t.Fatalf("Bad: State should be nil for invalid state data name")
}
// Nil StateData should not fail and should return nil
artifact = &Artifact{}
result = artifact.State("key")
if result != nil {
t.Fatalf("Bad: State should be nil for nil StateData")
}
}

View File

@ -1,135 +0,0 @@
// The digitalocean package contains a packersdk.Builder implementation
// that builds DigitalOcean images (snapshots).
package digitalocean
import (
"context"
"fmt"
"log"
"net/url"
"github.com/digitalocean/godo"
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/hashicorp/packer-plugin-sdk/communicator"
"github.com/hashicorp/packer-plugin-sdk/multistep"
"github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"golang.org/x/oauth2"
)
// The unique id for the builder
const BuilderId = "pearkes.digitalocean"
type Builder struct {
config Config
runner multistep.Runner
}
func (b *Builder) ConfigSpec() hcldec.ObjectSpec { return b.config.FlatMapstructure().HCL2Spec() }
func (b *Builder) Prepare(raws ...interface{}) ([]string, []string, error) {
warnings, errs := b.config.Prepare(raws...)
if errs != nil {
return nil, warnings, errs
}
return nil, nil, nil
}
func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) (packersdk.Artifact, error) {
client := godo.NewClient(oauth2.NewClient(context.TODO(), &apiTokenSource{
AccessToken: b.config.APIToken,
}))
if b.config.APIURL != "" {
u, err := url.Parse(b.config.APIURL)
if err != nil {
return nil, fmt.Errorf("DigitalOcean: Invalid API URL, %s.", err)
}
client.BaseURL = u
}
if len(b.config.SnapshotRegions) > 0 {
opt := &godo.ListOptions{
Page: 1,
PerPage: 200,
}
regions, _, err := client.Regions.List(context.TODO(), opt)
if err != nil {
return nil, fmt.Errorf("DigitalOcean: Unable to get regions, %s", err)
}
validRegions := make(map[string]struct{})
for _, val := range regions {
validRegions[val.Slug] = struct{}{}
}
for _, region := range append(b.config.SnapshotRegions, b.config.Region) {
if _, ok := validRegions[region]; !ok {
return nil, fmt.Errorf("DigitalOcean: Invalid region, %s", region)
}
}
}
// Set up the state
state := new(multistep.BasicStateBag)
state.Put("config", &b.config)
state.Put("client", client)
state.Put("hook", hook)
state.Put("ui", ui)
// Build the steps
steps := []multistep.Step{
&communicator.StepSSHKeyGen{
CommConf: &b.config.Comm,
SSHTemporaryKeyPair: b.config.Comm.SSH.SSHTemporaryKeyPair,
},
multistep.If(b.config.PackerDebug && b.config.Comm.SSHPrivateKeyFile == "",
&communicator.StepDumpSSHKey{
Path: fmt.Sprintf("do_%s.pem", b.config.PackerBuildName),
SSH: &b.config.Comm.SSH,
},
),
&stepCreateSSHKey{},
new(stepCreateDroplet),
new(stepDropletInfo),
&communicator.StepConnect{
Config: &b.config.Comm,
Host: communicator.CommHost(b.config.Comm.Host(), "droplet_ip"),
SSHConfig: b.config.Comm.SSHConfigFunc(),
},
new(commonsteps.StepProvision),
&commonsteps.StepCleanupTempKeys{
Comm: &b.config.Comm,
},
new(stepShutdown),
new(stepPowerOff),
&stepSnapshot{
snapshotTimeout: b.config.SnapshotTimeout,
},
}
// Run the steps
b.runner = commonsteps.NewRunner(steps, b.config.PackerConfig, ui)
b.runner.Run(ctx, state)
// If there was an error, return that
if rawErr, ok := state.GetOk("error"); ok {
return nil, rawErr.(error)
}
if _, ok := state.GetOk("snapshot_name"); !ok {
log.Println("Failed to find snapshot_name in state. Bug?")
return nil, nil
}
artifact := &Artifact{
SnapshotName: state.Get("snapshot_name").(string),
SnapshotId: state.Get("snapshot_image_id").(int),
RegionNames: state.Get("regions").([]string),
Client: client,
StateData: map[string]interface{}{"generated_data": state.Get("generated_data")},
}
return artifact, nil
}

View File

@ -1,65 +0,0 @@
package digitalocean
import (
"context"
"fmt"
"os"
"testing"
"github.com/digitalocean/godo"
builderT "github.com/hashicorp/packer/acctest"
"golang.org/x/oauth2"
)
func TestBuilderAcc_basic(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: fmt.Sprintf(testBuilderAccBasic, "ubuntu-20-04-x64"),
})
}
func TestBuilderAcc_imageId(t *testing.T) {
builderT.Test(t, builderT.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Builder: &Builder{},
Template: makeTemplateWithImageId(t),
})
}
func testAccPreCheck(t *testing.T) {
if v := os.Getenv("DIGITALOCEAN_API_TOKEN"); v == "" {
t.Fatal("DIGITALOCEAN_API_TOKEN must be set for acceptance tests")
}
}
func makeTemplateWithImageId(t *testing.T) string {
if os.Getenv(builderT.TestEnvVar) != "" {
token := os.Getenv("DIGITALOCEAN_API_TOKEN")
client := godo.NewClient(oauth2.NewClient(context.TODO(), &apiTokenSource{
AccessToken: token,
}))
image, _, err := client.Images.GetBySlug(context.TODO(), "ubuntu-20-04-x64")
if err != nil {
t.Fatalf("failed to retrieve image ID: %s", err)
}
return fmt.Sprintf(testBuilderAccBasic, image.ID)
}
return ""
}
const testBuilderAccBasic = `
{
"builders": [{
"type": "test",
"region": "nyc2",
"size": "s-1vcpu-1gb",
"image": "%v",
"ssh_username": "root",
"user_data": "",
"user_data_file": ""
}]
}
`

View File

@ -1,416 +0,0 @@
package digitalocean
import (
"strconv"
"testing"
"time"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
func testConfig() map[string]interface{} {
return map[string]interface{}{
"api_token": "bar",
"region": "nyc2",
"size": "512mb",
"ssh_username": "root",
"image": "foo",
}
}
func TestBuilder_ImplementsBuilder(t *testing.T) {
var raw interface{}
raw = &Builder{}
if _, ok := raw.(packersdk.Builder); !ok {
t.Fatalf("Builder should be a builder")
}
}
func TestBuilder_Prepare_BadType(t *testing.T) {
b := &Builder{}
c := map[string]interface{}{
"api_key": []string{},
}
_, warnings, err := b.Prepare(c)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatalf("prepare should fail")
}
}
func TestBuilderPrepare_InvalidKey(t *testing.T) {
var b Builder
config := testConfig()
// Add a random key
config["i_should_not_be_valid"] = true
_, warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_Region(t *testing.T) {
var b Builder
config := testConfig()
// Test default
delete(config, "region")
_, warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatalf("should error")
}
expected := "sfo1"
// Test set
config["region"] = expected
b = Builder{}
_, warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.Region != expected {
t.Errorf("found %s, expected %s", b.config.Region, expected)
}
}
func TestBuilderPrepare_Size(t *testing.T) {
var b Builder
config := testConfig()
// Test default
delete(config, "size")
_, warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatalf("should error")
}
expected := "1024mb"
// Test set
config["size"] = expected
b = Builder{}
_, warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.Size != expected {
t.Errorf("found %s, expected %s", b.config.Size, expected)
}
}
func TestBuilderPrepare_Image(t *testing.T) {
var b Builder
config := testConfig()
// Test default
delete(config, "image")
_, warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should error")
}
expected := "ubuntu-14-04-x64"
// Test set
config["image"] = expected
b = Builder{}
_, warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.Image != expected {
t.Errorf("found %s, expected %s", b.config.Image, expected)
}
}
func TestBuilderPrepare_StateTimeout(t *testing.T) {
var b Builder
config := testConfig()
// Test default
_, warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.StateTimeout != 6*time.Minute {
t.Errorf("invalid: %s", b.config.StateTimeout)
}
// Test set
config["state_timeout"] = "5m"
b = Builder{}
_, warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["state_timeout"] = "tubes"
b = Builder{}
_, warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_SnapshotTimeout(t *testing.T) {
var b Builder
config := testConfig()
// Test default
_, warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SnapshotTimeout != 60*time.Minute {
t.Errorf("invalid: %s", b.config.SnapshotTimeout)
}
// Test set
config["snapshot_timeout"] = "15m"
b = Builder{}
_, warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test bad
config["snapshot_timeout"] = "badstring"
b = Builder{}
_, warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_PrivateNetworking(t *testing.T) {
var b Builder
config := testConfig()
// Test default
_, warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.PrivateNetworking != false {
t.Errorf("invalid: %t", b.config.PrivateNetworking)
}
// Test set
config["private_networking"] = true
b = Builder{}
_, warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.PrivateNetworking != true {
t.Errorf("invalid: %t", b.config.PrivateNetworking)
}
}
func TestBuilderPrepare_SnapshotName(t *testing.T) {
var b Builder
config := testConfig()
// Test default
_, warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.SnapshotName == "" {
t.Errorf("invalid: %s", b.config.SnapshotName)
}
// Test set
config["snapshot_name"] = "foobarbaz"
b = Builder{}
_, warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test set with template
config["snapshot_name"] = "{{timestamp}}"
b = Builder{}
_, warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
_, err = strconv.ParseInt(b.config.SnapshotName, 0, 0)
if err != nil {
t.Fatalf("failed to parse int in template: %s", err)
}
}
func TestBuilderPrepare_DropletName(t *testing.T) {
var b Builder
config := testConfig()
// Test default
_, warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
if b.config.DropletName == "" {
t.Errorf("invalid: %s", b.config.DropletName)
}
// Test normal set
config["droplet_name"] = "foobar"
b = Builder{}
_, warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test with template
config["droplet_name"] = "foobar-{{timestamp}}"
b = Builder{}
_, warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatalf("should not have error: %s", err)
}
// Test with bad template
config["droplet_name"] = "foobar-{{"
b = Builder{}
_, warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatal("should have error")
}
}
func TestBuilderPrepare_VPCUUID(t *testing.T) {
var b Builder
config := testConfig()
// Test with the case vpc_uuid is defined but private_networking is not enabled
config["vpc_uuid"] = "554c41b3-425f-5403-8860-7f24fb108098"
_, warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatalf("should have error: 'private networking should be enabled to use vpc_uuid'")
}
// Test with the case both vpc_uuid and private_networking are defined/enabled
config["private_networking"] = true
b = Builder{}
_, warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatal("should not have error")
}
}
func TestBuilderPrepare_ConnectWithPrivateIP(t *testing.T) {
var b Builder
config := testConfig()
// Test with the case connect_with_private_ip is defined but private_networking is not enabled
config["connect_with_private_ip"] = true
_, warnings, err := b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err == nil {
t.Fatalf("should have error: 'private networking should be enabled to use connect_with_private_ip'")
}
// Test with the case both connect_with_private_ip and private_networking are enabled
config["private_networking"] = true
b = Builder{}
_, warnings, err = b.Prepare(config)
if len(warnings) > 0 {
t.Fatalf("bad: %#v", warnings)
}
if err != nil {
t.Fatal("should not have error")
}
}

View File

@ -1,218 +0,0 @@
//go:generate packer-sdc struct-markdown
//go:generate packer-sdc mapstructure-to-hcl2 -type Config
package digitalocean
import (
"errors"
"fmt"
"os"
"regexp"
"time"
"github.com/hashicorp/packer-plugin-sdk/common"
"github.com/hashicorp/packer-plugin-sdk/communicator"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template/config"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
"github.com/hashicorp/packer-plugin-sdk/uuid"
"github.com/mitchellh/mapstructure"
)
type Config struct {
common.PackerConfig `mapstructure:",squash"`
Comm communicator.Config `mapstructure:",squash"`
// The client TOKEN to use to access your account. It
// can also be specified via environment variable DIGITALOCEAN_API_TOKEN, if
// set.
APIToken string `mapstructure:"api_token" required:"true"`
// Non standard api endpoint URL. Set this if you are
// using a DigitalOcean API compatible service. It can also be specified via
// environment variable DIGITALOCEAN_API_URL.
APIURL string `mapstructure:"api_url" required:"false"`
// The name (or slug) of the region to launch the droplet
// in. Consequently, this is the region where the snapshot will be available.
// See
// https://developers.digitalocean.com/documentation/v2/#list-all-regions
// for the accepted region names/slugs.
Region string `mapstructure:"region" required:"true"`
// The name (or slug) of the droplet size to use. See
// https://developers.digitalocean.com/documentation/v2/#list-all-sizes
// for the accepted size names/slugs.
Size string `mapstructure:"size" required:"true"`
// The name (or slug) of the base image to use. This is the
// image that will be used to launch a new droplet and provision it. See
// https://developers.digitalocean.com/documentation/v2/#list-all-images
// for details on how to get a list of the accepted image names/slugs.
Image string `mapstructure:"image" required:"true"`
// Set to true to enable private networking
// for the droplet being created. This defaults to false, or not enabled.
PrivateNetworking bool `mapstructure:"private_networking" required:"false"`
// Set to true to enable monitoring for the droplet
// being created. This defaults to false, or not enabled.
Monitoring bool `mapstructure:"monitoring" required:"false"`
// Set to true to enable ipv6 for the droplet being
// created. This defaults to false, or not enabled.
IPv6 bool `mapstructure:"ipv6" required:"false"`
// The name of the resulting snapshot that will
// appear in your account. Defaults to `packer-{{timestamp}}` (see
// configuration templates for more info).
SnapshotName string `mapstructure:"snapshot_name" required:"false"`
// The regions of the resulting
// snapshot that will appear in your account.
SnapshotRegions []string `mapstructure:"snapshot_regions" required:"false"`
// The time to wait, as a duration string, for a
// droplet to enter a desired state (such as "active") before timing out. The
// default state timeout is "6m".
StateTimeout time.Duration `mapstructure:"state_timeout" required:"false"`
// How long to wait for an image to be published to the shared image
// gallery before timing out. If your Packer build is failing on the
// Publishing to Shared Image Gallery step with the error `Original Error:
// context deadline exceeded`, but the image is present when you check your
// Azure dashboard, then you probably need to increase this timeout from
// its default of "60m" (valid time units include `s` for seconds, `m` for
// minutes, and `h` for hours.)
SnapshotTimeout time.Duration `mapstructure:"snapshot_timeout" required:"false"`
// The name assigned to the droplet. DigitalOcean
// sets the hostname of the machine to this value.
DropletName string `mapstructure:"droplet_name" required:"false"`
// User data to launch with the Droplet. Packer will
// not automatically wait for a user script to finish before shutting down the
// instance this must be handled in a provisioner.
UserData string `mapstructure:"user_data" required:"false"`
// Path to a file that will be used for the user
// data when launching the Droplet.
UserDataFile string `mapstructure:"user_data_file" required:"false"`
// Tags to apply to the droplet when it is created
Tags []string `mapstructure:"tags" required:"false"`
// UUID of the VPC which the droplet will be created in. Before using this,
// private_networking should be enabled.
VPCUUID string `mapstructure:"vpc_uuid" required:"false"`
// Wheter the communicators should use private IP or not (public IP in that case).
// If the droplet is or going to be accessible only from the local network because
// it is at behind a firewall, then communicators should use the private IP
// instead of the public IP. Before using this, private_networking should be enabled.
ConnectWithPrivateIP bool `mapstructure:"connect_with_private_ip" required:"false"`
ctx interpolate.Context
}
func (c *Config) Prepare(raws ...interface{}) ([]string, error) {
var md mapstructure.Metadata
err := config.Decode(c, &config.DecodeOpts{
Metadata: &md,
Interpolate: true,
InterpolateContext: &c.ctx,
InterpolateFilter: &interpolate.RenderFilter{
Exclude: []string{
"run_command",
},
},
}, raws...)
if err != nil {
return nil, err
}
// Defaults
if c.APIToken == "" {
// Default to environment variable for api_token, if it exists
c.APIToken = os.Getenv("DIGITALOCEAN_API_TOKEN")
}
if c.APIURL == "" {
c.APIURL = os.Getenv("DIGITALOCEAN_API_URL")
}
if c.SnapshotName == "" {
def, err := interpolate.Render("packer-{{timestamp}}", nil)
if err != nil {
panic(err)
}
// Default to packer-{{ unix timestamp (utc) }}
c.SnapshotName = def
}
if c.DropletName == "" {
// Default to packer-[time-ordered-uuid]
c.DropletName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
}
if c.StateTimeout == 0 {
// Default to 6 minute timeouts waiting for
// desired state. i.e waiting for droplet to become active
c.StateTimeout = 6 * time.Minute
}
if c.SnapshotTimeout == 0 {
// Default to 60 minutes timeout, waiting for snapshot action to finish
c.SnapshotTimeout = 60 * time.Minute
}
var errs *packersdk.MultiError
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
errs = packersdk.MultiErrorAppend(errs, es...)
}
if c.APIToken == "" {
// Required configurations that will display errors if not set
errs = packersdk.MultiErrorAppend(
errs, errors.New("api_token for auth must be specified"))
}
if c.Region == "" {
errs = packersdk.MultiErrorAppend(
errs, errors.New("region is required"))
}
if c.Size == "" {
errs = packersdk.MultiErrorAppend(
errs, errors.New("size is required"))
}
if c.Image == "" {
errs = packersdk.MultiErrorAppend(
errs, errors.New("image is required"))
}
if c.UserData != "" && c.UserDataFile != "" {
errs = packersdk.MultiErrorAppend(
errs, errors.New("only one of user_data or user_data_file can be specified"))
} else if c.UserDataFile != "" {
if _, err := os.Stat(c.UserDataFile); err != nil {
errs = packersdk.MultiErrorAppend(
errs, errors.New(fmt.Sprintf("user_data_file not found: %s", c.UserDataFile)))
}
}
if c.Tags == nil {
c.Tags = make([]string, 0)
}
tagRe := regexp.MustCompile("^[[:alnum:]:_-]{1,255}$")
for _, t := range c.Tags {
if !tagRe.MatchString(t) {
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("invalid tag: %s", t))
}
}
// Check if the PrivateNetworking is enabled by user before use VPC UUID
if c.VPCUUID != "" {
if c.PrivateNetworking != true {
errs = packersdk.MultiErrorAppend(errs, errors.New("private networking should be enabled to use vpc_uuid"))
}
}
// Check if the PrivateNetworking is enabled by user before use ConnectWithPrivateIP
if c.ConnectWithPrivateIP == true {
if c.PrivateNetworking != true {
errs = packersdk.MultiErrorAppend(errs, errors.New("private networking should be enabled to use connect_with_private_ip"))
}
}
if errs != nil && len(errs.Errors) > 0 {
return nil, errs
}
packersdk.LogSecretFilter.Set(c.APIToken)
return nil, nil
}

View File

@ -1,179 +0,0 @@
// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT.
package digitalocean
import (
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
)
// FlatConfig is an auto-generated flat version of Config.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatConfig struct {
PackerBuildName *string `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"`
PackerBuilderType *string `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"`
PackerCoreVersion *string `mapstructure:"packer_core_version" cty:"packer_core_version" hcl:"packer_core_version"`
PackerDebug *bool `mapstructure:"packer_debug" cty:"packer_debug" hcl:"packer_debug"`
PackerForce *bool `mapstructure:"packer_force" cty:"packer_force" hcl:"packer_force"`
PackerOnError *string `mapstructure:"packer_on_error" cty:"packer_on_error" hcl:"packer_on_error"`
PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"`
PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"`
Type *string `mapstructure:"communicator" cty:"communicator" hcl:"communicator"`
PauseBeforeConnect *string `mapstructure:"pause_before_connecting" cty:"pause_before_connecting" hcl:"pause_before_connecting"`
SSHHost *string `mapstructure:"ssh_host" cty:"ssh_host" hcl:"ssh_host"`
SSHPort *int `mapstructure:"ssh_port" cty:"ssh_port" hcl:"ssh_port"`
SSHUsername *string `mapstructure:"ssh_username" cty:"ssh_username" hcl:"ssh_username"`
SSHPassword *string `mapstructure:"ssh_password" cty:"ssh_password" hcl:"ssh_password"`
SSHKeyPairName *string `mapstructure:"ssh_keypair_name" undocumented:"true" cty:"ssh_keypair_name" hcl:"ssh_keypair_name"`
SSHTemporaryKeyPairName *string `mapstructure:"temporary_key_pair_name" undocumented:"true" cty:"temporary_key_pair_name" hcl:"temporary_key_pair_name"`
SSHTemporaryKeyPairType *string `mapstructure:"temporary_key_pair_type" cty:"temporary_key_pair_type" hcl:"temporary_key_pair_type"`
SSHTemporaryKeyPairBits *int `mapstructure:"temporary_key_pair_bits" cty:"temporary_key_pair_bits" hcl:"temporary_key_pair_bits"`
SSHCiphers []string `mapstructure:"ssh_ciphers" cty:"ssh_ciphers" hcl:"ssh_ciphers"`
SSHClearAuthorizedKeys *bool `mapstructure:"ssh_clear_authorized_keys" cty:"ssh_clear_authorized_keys" hcl:"ssh_clear_authorized_keys"`
SSHKEXAlgos []string `mapstructure:"ssh_key_exchange_algorithms" cty:"ssh_key_exchange_algorithms" hcl:"ssh_key_exchange_algorithms"`
SSHPrivateKeyFile *string `mapstructure:"ssh_private_key_file" undocumented:"true" cty:"ssh_private_key_file" hcl:"ssh_private_key_file"`
SSHCertificateFile *string `mapstructure:"ssh_certificate_file" cty:"ssh_certificate_file" hcl:"ssh_certificate_file"`
SSHPty *bool `mapstructure:"ssh_pty" cty:"ssh_pty" hcl:"ssh_pty"`
SSHTimeout *string `mapstructure:"ssh_timeout" cty:"ssh_timeout" hcl:"ssh_timeout"`
SSHWaitTimeout *string `mapstructure:"ssh_wait_timeout" undocumented:"true" cty:"ssh_wait_timeout" hcl:"ssh_wait_timeout"`
SSHAgentAuth *bool `mapstructure:"ssh_agent_auth" undocumented:"true" cty:"ssh_agent_auth" hcl:"ssh_agent_auth"`
SSHDisableAgentForwarding *bool `mapstructure:"ssh_disable_agent_forwarding" cty:"ssh_disable_agent_forwarding" hcl:"ssh_disable_agent_forwarding"`
SSHHandshakeAttempts *int `mapstructure:"ssh_handshake_attempts" cty:"ssh_handshake_attempts" hcl:"ssh_handshake_attempts"`
SSHBastionHost *string `mapstructure:"ssh_bastion_host" cty:"ssh_bastion_host" hcl:"ssh_bastion_host"`
SSHBastionPort *int `mapstructure:"ssh_bastion_port" cty:"ssh_bastion_port" hcl:"ssh_bastion_port"`
SSHBastionAgentAuth *bool `mapstructure:"ssh_bastion_agent_auth" cty:"ssh_bastion_agent_auth" hcl:"ssh_bastion_agent_auth"`
SSHBastionUsername *string `mapstructure:"ssh_bastion_username" cty:"ssh_bastion_username" hcl:"ssh_bastion_username"`
SSHBastionPassword *string `mapstructure:"ssh_bastion_password" cty:"ssh_bastion_password" hcl:"ssh_bastion_password"`
SSHBastionInteractive *bool `mapstructure:"ssh_bastion_interactive" cty:"ssh_bastion_interactive" hcl:"ssh_bastion_interactive"`
SSHBastionPrivateKeyFile *string `mapstructure:"ssh_bastion_private_key_file" cty:"ssh_bastion_private_key_file" hcl:"ssh_bastion_private_key_file"`
SSHBastionCertificateFile *string `mapstructure:"ssh_bastion_certificate_file" cty:"ssh_bastion_certificate_file" hcl:"ssh_bastion_certificate_file"`
SSHFileTransferMethod *string `mapstructure:"ssh_file_transfer_method" cty:"ssh_file_transfer_method" hcl:"ssh_file_transfer_method"`
SSHProxyHost *string `mapstructure:"ssh_proxy_host" cty:"ssh_proxy_host" hcl:"ssh_proxy_host"`
SSHProxyPort *int `mapstructure:"ssh_proxy_port" cty:"ssh_proxy_port" hcl:"ssh_proxy_port"`
SSHProxyUsername *string `mapstructure:"ssh_proxy_username" cty:"ssh_proxy_username" hcl:"ssh_proxy_username"`
SSHProxyPassword *string `mapstructure:"ssh_proxy_password" cty:"ssh_proxy_password" hcl:"ssh_proxy_password"`
SSHKeepAliveInterval *string `mapstructure:"ssh_keep_alive_interval" cty:"ssh_keep_alive_interval" hcl:"ssh_keep_alive_interval"`
SSHReadWriteTimeout *string `mapstructure:"ssh_read_write_timeout" cty:"ssh_read_write_timeout" hcl:"ssh_read_write_timeout"`
SSHRemoteTunnels []string `mapstructure:"ssh_remote_tunnels" cty:"ssh_remote_tunnels" hcl:"ssh_remote_tunnels"`
SSHLocalTunnels []string `mapstructure:"ssh_local_tunnels" cty:"ssh_local_tunnels" hcl:"ssh_local_tunnels"`
SSHPublicKey []byte `mapstructure:"ssh_public_key" undocumented:"true" cty:"ssh_public_key" hcl:"ssh_public_key"`
SSHPrivateKey []byte `mapstructure:"ssh_private_key" undocumented:"true" cty:"ssh_private_key" hcl:"ssh_private_key"`
WinRMUser *string `mapstructure:"winrm_username" cty:"winrm_username" hcl:"winrm_username"`
WinRMPassword *string `mapstructure:"winrm_password" cty:"winrm_password" hcl:"winrm_password"`
WinRMHost *string `mapstructure:"winrm_host" cty:"winrm_host" hcl:"winrm_host"`
WinRMNoProxy *bool `mapstructure:"winrm_no_proxy" cty:"winrm_no_proxy" hcl:"winrm_no_proxy"`
WinRMPort *int `mapstructure:"winrm_port" cty:"winrm_port" hcl:"winrm_port"`
WinRMTimeout *string `mapstructure:"winrm_timeout" cty:"winrm_timeout" hcl:"winrm_timeout"`
WinRMUseSSL *bool `mapstructure:"winrm_use_ssl" cty:"winrm_use_ssl" hcl:"winrm_use_ssl"`
WinRMInsecure *bool `mapstructure:"winrm_insecure" cty:"winrm_insecure" hcl:"winrm_insecure"`
WinRMUseNTLM *bool `mapstructure:"winrm_use_ntlm" cty:"winrm_use_ntlm" hcl:"winrm_use_ntlm"`
APIToken *string `mapstructure:"api_token" required:"true" cty:"api_token" hcl:"api_token"`
APIURL *string `mapstructure:"api_url" required:"false" cty:"api_url" hcl:"api_url"`
Region *string `mapstructure:"region" required:"true" cty:"region" hcl:"region"`
Size *string `mapstructure:"size" required:"true" cty:"size" hcl:"size"`
Image *string `mapstructure:"image" required:"true" cty:"image" hcl:"image"`
PrivateNetworking *bool `mapstructure:"private_networking" required:"false" cty:"private_networking" hcl:"private_networking"`
Monitoring *bool `mapstructure:"monitoring" required:"false" cty:"monitoring" hcl:"monitoring"`
IPv6 *bool `mapstructure:"ipv6" required:"false" cty:"ipv6" hcl:"ipv6"`
SnapshotName *string `mapstructure:"snapshot_name" required:"false" cty:"snapshot_name" hcl:"snapshot_name"`
SnapshotRegions []string `mapstructure:"snapshot_regions" required:"false" cty:"snapshot_regions" hcl:"snapshot_regions"`
StateTimeout *string `mapstructure:"state_timeout" required:"false" cty:"state_timeout" hcl:"state_timeout"`
SnapshotTimeout *string `mapstructure:"snapshot_timeout" required:"false" cty:"snapshot_timeout" hcl:"snapshot_timeout"`
DropletName *string `mapstructure:"droplet_name" required:"false" cty:"droplet_name" hcl:"droplet_name"`
UserData *string `mapstructure:"user_data" required:"false" cty:"user_data" hcl:"user_data"`
UserDataFile *string `mapstructure:"user_data_file" required:"false" cty:"user_data_file" hcl:"user_data_file"`
Tags []string `mapstructure:"tags" required:"false" cty:"tags" hcl:"tags"`
VPCUUID *string `mapstructure:"vpc_uuid" required:"false" cty:"vpc_uuid" hcl:"vpc_uuid"`
ConnectWithPrivateIP *bool `mapstructure:"connect_with_private_ip" required:"false" cty:"connect_with_private_ip" hcl:"connect_with_private_ip"`
}
// FlatMapstructure returns a new FlatConfig.
// FlatConfig is an auto-generated flat version of Config.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*Config) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatConfig)
}
// HCL2Spec returns the hcl spec of a Config.
// This spec is used by HCL to read the fields of Config.
// The decoded values from this spec will then be applied to a FlatConfig.
func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"packer_build_name": &hcldec.AttrSpec{Name: "packer_build_name", Type: cty.String, Required: false},
"packer_builder_type": &hcldec.AttrSpec{Name: "packer_builder_type", Type: cty.String, Required: false},
"packer_core_version": &hcldec.AttrSpec{Name: "packer_core_version", Type: cty.String, Required: false},
"packer_debug": &hcldec.AttrSpec{Name: "packer_debug", Type: cty.Bool, Required: false},
"packer_force": &hcldec.AttrSpec{Name: "packer_force", Type: cty.Bool, Required: false},
"packer_on_error": &hcldec.AttrSpec{Name: "packer_on_error", Type: cty.String, Required: false},
"packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false},
"packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false},
"communicator": &hcldec.AttrSpec{Name: "communicator", Type: cty.String, Required: false},
"pause_before_connecting": &hcldec.AttrSpec{Name: "pause_before_connecting", Type: cty.String, Required: false},
"ssh_host": &hcldec.AttrSpec{Name: "ssh_host", Type: cty.String, Required: false},
"ssh_port": &hcldec.AttrSpec{Name: "ssh_port", Type: cty.Number, Required: false},
"ssh_username": &hcldec.AttrSpec{Name: "ssh_username", Type: cty.String, Required: false},
"ssh_password": &hcldec.AttrSpec{Name: "ssh_password", Type: cty.String, Required: false},
"ssh_keypair_name": &hcldec.AttrSpec{Name: "ssh_keypair_name", Type: cty.String, Required: false},
"temporary_key_pair_name": &hcldec.AttrSpec{Name: "temporary_key_pair_name", Type: cty.String, Required: false},
"temporary_key_pair_type": &hcldec.AttrSpec{Name: "temporary_key_pair_type", Type: cty.String, Required: false},
"temporary_key_pair_bits": &hcldec.AttrSpec{Name: "temporary_key_pair_bits", Type: cty.Number, Required: false},
"ssh_ciphers": &hcldec.AttrSpec{Name: "ssh_ciphers", Type: cty.List(cty.String), Required: false},
"ssh_clear_authorized_keys": &hcldec.AttrSpec{Name: "ssh_clear_authorized_keys", Type: cty.Bool, Required: false},
"ssh_key_exchange_algorithms": &hcldec.AttrSpec{Name: "ssh_key_exchange_algorithms", Type: cty.List(cty.String), Required: false},
"ssh_private_key_file": &hcldec.AttrSpec{Name: "ssh_private_key_file", Type: cty.String, Required: false},
"ssh_certificate_file": &hcldec.AttrSpec{Name: "ssh_certificate_file", Type: cty.String, Required: false},
"ssh_pty": &hcldec.AttrSpec{Name: "ssh_pty", Type: cty.Bool, Required: false},
"ssh_timeout": &hcldec.AttrSpec{Name: "ssh_timeout", Type: cty.String, Required: false},
"ssh_wait_timeout": &hcldec.AttrSpec{Name: "ssh_wait_timeout", Type: cty.String, Required: false},
"ssh_agent_auth": &hcldec.AttrSpec{Name: "ssh_agent_auth", Type: cty.Bool, Required: false},
"ssh_disable_agent_forwarding": &hcldec.AttrSpec{Name: "ssh_disable_agent_forwarding", Type: cty.Bool, Required: false},
"ssh_handshake_attempts": &hcldec.AttrSpec{Name: "ssh_handshake_attempts", Type: cty.Number, Required: false},
"ssh_bastion_host": &hcldec.AttrSpec{Name: "ssh_bastion_host", Type: cty.String, Required: false},
"ssh_bastion_port": &hcldec.AttrSpec{Name: "ssh_bastion_port", Type: cty.Number, Required: false},
"ssh_bastion_agent_auth": &hcldec.AttrSpec{Name: "ssh_bastion_agent_auth", Type: cty.Bool, Required: false},
"ssh_bastion_username": &hcldec.AttrSpec{Name: "ssh_bastion_username", Type: cty.String, Required: false},
"ssh_bastion_password": &hcldec.AttrSpec{Name: "ssh_bastion_password", Type: cty.String, Required: false},
"ssh_bastion_interactive": &hcldec.AttrSpec{Name: "ssh_bastion_interactive", Type: cty.Bool, Required: false},
"ssh_bastion_private_key_file": &hcldec.AttrSpec{Name: "ssh_bastion_private_key_file", Type: cty.String, Required: false},
"ssh_bastion_certificate_file": &hcldec.AttrSpec{Name: "ssh_bastion_certificate_file", Type: cty.String, Required: false},
"ssh_file_transfer_method": &hcldec.AttrSpec{Name: "ssh_file_transfer_method", Type: cty.String, Required: false},
"ssh_proxy_host": &hcldec.AttrSpec{Name: "ssh_proxy_host", Type: cty.String, Required: false},
"ssh_proxy_port": &hcldec.AttrSpec{Name: "ssh_proxy_port", Type: cty.Number, Required: false},
"ssh_proxy_username": &hcldec.AttrSpec{Name: "ssh_proxy_username", Type: cty.String, Required: false},
"ssh_proxy_password": &hcldec.AttrSpec{Name: "ssh_proxy_password", Type: cty.String, Required: false},
"ssh_keep_alive_interval": &hcldec.AttrSpec{Name: "ssh_keep_alive_interval", Type: cty.String, Required: false},
"ssh_read_write_timeout": &hcldec.AttrSpec{Name: "ssh_read_write_timeout", Type: cty.String, Required: false},
"ssh_remote_tunnels": &hcldec.AttrSpec{Name: "ssh_remote_tunnels", Type: cty.List(cty.String), Required: false},
"ssh_local_tunnels": &hcldec.AttrSpec{Name: "ssh_local_tunnels", Type: cty.List(cty.String), Required: false},
"ssh_public_key": &hcldec.AttrSpec{Name: "ssh_public_key", Type: cty.List(cty.Number), Required: false},
"ssh_private_key": &hcldec.AttrSpec{Name: "ssh_private_key", Type: cty.List(cty.Number), Required: false},
"winrm_username": &hcldec.AttrSpec{Name: "winrm_username", Type: cty.String, Required: false},
"winrm_password": &hcldec.AttrSpec{Name: "winrm_password", Type: cty.String, Required: false},
"winrm_host": &hcldec.AttrSpec{Name: "winrm_host", Type: cty.String, Required: false},
"winrm_no_proxy": &hcldec.AttrSpec{Name: "winrm_no_proxy", Type: cty.Bool, Required: false},
"winrm_port": &hcldec.AttrSpec{Name: "winrm_port", Type: cty.Number, Required: false},
"winrm_timeout": &hcldec.AttrSpec{Name: "winrm_timeout", Type: cty.String, Required: false},
"winrm_use_ssl": &hcldec.AttrSpec{Name: "winrm_use_ssl", Type: cty.Bool, Required: false},
"winrm_insecure": &hcldec.AttrSpec{Name: "winrm_insecure", Type: cty.Bool, Required: false},
"winrm_use_ntlm": &hcldec.AttrSpec{Name: "winrm_use_ntlm", Type: cty.Bool, Required: false},
"api_token": &hcldec.AttrSpec{Name: "api_token", Type: cty.String, Required: false},
"api_url": &hcldec.AttrSpec{Name: "api_url", Type: cty.String, Required: false},
"region": &hcldec.AttrSpec{Name: "region", Type: cty.String, Required: false},
"size": &hcldec.AttrSpec{Name: "size", Type: cty.String, Required: false},
"image": &hcldec.AttrSpec{Name: "image", Type: cty.String, Required: false},
"private_networking": &hcldec.AttrSpec{Name: "private_networking", Type: cty.Bool, Required: false},
"monitoring": &hcldec.AttrSpec{Name: "monitoring", Type: cty.Bool, Required: false},
"ipv6": &hcldec.AttrSpec{Name: "ipv6", Type: cty.Bool, Required: false},
"snapshot_name": &hcldec.AttrSpec{Name: "snapshot_name", Type: cty.String, Required: false},
"snapshot_regions": &hcldec.AttrSpec{Name: "snapshot_regions", Type: cty.List(cty.String), Required: false},
"state_timeout": &hcldec.AttrSpec{Name: "state_timeout", Type: cty.String, Required: false},
"snapshot_timeout": &hcldec.AttrSpec{Name: "snapshot_timeout", Type: cty.String, Required: false},
"droplet_name": &hcldec.AttrSpec{Name: "droplet_name", Type: cty.String, Required: false},
"user_data": &hcldec.AttrSpec{Name: "user_data", Type: cty.String, Required: false},
"user_data_file": &hcldec.AttrSpec{Name: "user_data_file", Type: cty.String, Required: false},
"tags": &hcldec.AttrSpec{Name: "tags", Type: cty.List(cty.String), Required: false},
"vpc_uuid": &hcldec.AttrSpec{Name: "vpc_uuid", Type: cty.String, Required: false},
"connect_with_private_ip": &hcldec.AttrSpec{Name: "connect_with_private_ip", Type: cty.Bool, Required: false},
}
return s
}

View File

@ -1,107 +0,0 @@
package digitalocean
import (
"context"
"fmt"
"log"
"strconv"
"io/ioutil"
"github.com/digitalocean/godo"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
type stepCreateDroplet struct {
dropletId int
}
func (s *stepCreateDroplet) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packersdk.Ui)
c := state.Get("config").(*Config)
sshKeyId := state.Get("ssh_key_id").(int)
// Create the droplet based on configuration
ui.Say("Creating droplet...")
userData := c.UserData
if c.UserDataFile != "" {
contents, err := ioutil.ReadFile(c.UserDataFile)
if err != nil {
state.Put("error", fmt.Errorf("Problem reading user data file: %s", err))
return multistep.ActionHalt
}
userData = string(contents)
}
createImage := getImageType(c.Image)
dropletCreateReq := &godo.DropletCreateRequest{
Name: c.DropletName,
Region: c.Region,
Size: c.Size,
Image: createImage,
SSHKeys: []godo.DropletCreateSSHKey{
{ID: sshKeyId},
},
PrivateNetworking: c.PrivateNetworking,
Monitoring: c.Monitoring,
IPv6: c.IPv6,
UserData: userData,
Tags: c.Tags,
VPCUUID: c.VPCUUID,
}
log.Printf("[DEBUG] Droplet create paramaters: %s", godo.Stringify(dropletCreateReq))
droplet, _, err := client.Droplets.Create(context.TODO(), dropletCreateReq)
if err != nil {
err := fmt.Errorf("Error creating droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// We use this in cleanup
s.dropletId = droplet.ID
// Store the droplet id for later
state.Put("droplet_id", droplet.ID)
// instance_id is the generic term used so that users can have access to the
// instance id inside of the provisioners, used in step_provision.
state.Put("instance_id", droplet.ID)
return multistep.ActionContinue
}
func (s *stepCreateDroplet) Cleanup(state multistep.StateBag) {
// If the dropletid isn't there, we probably never created it
if s.dropletId == 0 {
return
}
client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packersdk.Ui)
// Destroy the droplet we just created
ui.Say("Destroying droplet...")
_, err := client.Droplets.Delete(context.TODO(), s.dropletId)
if err != nil {
ui.Error(fmt.Sprintf(
"Error destroying droplet. Please destroy it manually: %s", err))
}
}
func getImageType(image string) godo.DropletCreateImage {
createImage := godo.DropletCreateImage{Slug: image}
imageId, err := strconv.Atoi(image)
if err == nil {
createImage = godo.DropletCreateImage{ID: imageId}
}
return createImage
}

View File

@ -1,26 +0,0 @@
package digitalocean
import (
"testing"
"github.com/digitalocean/godo"
)
func TestBuilder_GetImageType(t *testing.T) {
imageTypeTests := []struct {
in string
out godo.DropletCreateImage
}{
{"ubuntu-20-04-x64", godo.DropletCreateImage{Slug: "ubuntu-20-04-x64"}},
{"123456", godo.DropletCreateImage{ID: 123456}},
}
for _, tt := range imageTypeTests {
t.Run(tt.in, func(t *testing.T) {
i := getImageType(tt.in)
if i != tt.out {
t.Errorf("got %q, want %q", godo.Stringify(i), godo.Stringify(tt.out))
}
})
}
}

View File

@ -1,72 +0,0 @@
package digitalocean
import (
"context"
"fmt"
"log"
"github.com/digitalocean/godo"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/uuid"
)
type stepCreateSSHKey struct {
keyId int
}
func (s *stepCreateSSHKey) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packersdk.Ui)
c := state.Get("config").(*Config)
if c.Comm.SSHPublicKey == nil {
ui.Say("No public SSH key found; skipping SSH public key import...")
return multistep.ActionContinue
}
ui.Say("Importing SSH public key...")
// The name of the public key on DO
name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
// Create the key!
key, _, err := client.Keys.Create(context.TODO(), &godo.KeyCreateRequest{
Name: name,
PublicKey: string(c.Comm.SSHPublicKey),
})
if err != nil {
err := fmt.Errorf("Error creating temporary SSH key: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// We use this to check cleanup
s.keyId = key.ID
log.Printf("temporary ssh key name: %s", name)
// Remember some state for the future
state.Put("ssh_key_id", key.ID)
return multistep.ActionContinue
}
func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {
// If no key name is set, then we never created it, so just return
if s.keyId == 0 {
return
}
client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packersdk.Ui)
ui.Say("Deleting temporary ssh key...")
_, err := client.Keys.DeleteByID(context.TODO(), s.keyId)
if err != nil {
log.Printf("Error cleaning up ssh key: %s", err)
ui.Error(fmt.Sprintf(
"Error cleaning up ssh key. Please delete the key manually: %s", err))
}
}

View File

@ -1,71 +0,0 @@
package digitalocean
import (
"context"
"fmt"
"github.com/digitalocean/godo"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
type stepDropletInfo struct{}
func (s *stepDropletInfo) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packersdk.Ui)
c := state.Get("config").(*Config)
dropletID := state.Get("droplet_id").(int)
ui.Say("Waiting for droplet to become active...")
err := waitForDropletState("active", dropletID, client, c.StateTimeout)
if err != nil {
err := fmt.Errorf("Error waiting for droplet to become active: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Set the IP on the state for later
droplet, _, err := client.Droplets.Get(context.TODO(), dropletID)
if err != nil {
err := fmt.Errorf("Error retrieving droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Verify we have an IPv4 address
invalid := droplet.Networks == nil ||
len(droplet.Networks.V4) == 0
if invalid {
err := fmt.Errorf("IPv4 address not found for droplet")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Find the ip address which will be used by communicator
foundNetwork := false
for _, network := range droplet.Networks.V4 {
if (c.ConnectWithPrivateIP && network.Type == "private") ||
(!(c.ConnectWithPrivateIP) && network.Type == "public") {
state.Put("droplet_ip", network.IPAddress)
foundNetwork = true
break
}
}
if !foundNetwork {
err := fmt.Errorf("Count not find a public IPv4 address for this droplet")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepDropletInfo) Cleanup(state multistep.StateBag) {
// no cleanup
}

View File

@ -1,66 +0,0 @@
package digitalocean
import (
"context"
"fmt"
"log"
"github.com/digitalocean/godo"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
type stepPowerOff struct{}
func (s *stepPowerOff) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*godo.Client)
c := state.Get("config").(*Config)
ui := state.Get("ui").(packersdk.Ui)
dropletId := state.Get("droplet_id").(int)
droplet, _, err := client.Droplets.Get(context.TODO(), dropletId)
if err != nil {
err := fmt.Errorf("Error checking droplet state: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if droplet.Status == "off" {
// Droplet is already off, don't do anything
return multistep.ActionContinue
}
// Pull the plug on the Droplet
ui.Say("Forcefully shutting down Droplet...")
_, _, err = client.DropletActions.PowerOff(context.TODO(), dropletId)
if err != nil {
err := fmt.Errorf("Error powering off droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
log.Println("Waiting for poweroff event to complete...")
err = waitForDropletState("off", dropletId, client, c.StateTimeout)
if err != nil {
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Wait for the droplet to become unlocked for future steps
if err := waitForDropletUnlocked(client, dropletId, c.StateTimeout); err != nil {
// If we get an error the first time, actually report it
err := fmt.Errorf("Error powering off droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepPowerOff) Cleanup(state multistep.StateBag) {
// no cleanup
}

View File

@ -1,90 +0,0 @@
package digitalocean
import (
"context"
"fmt"
"log"
"time"
"github.com/digitalocean/godo"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
type stepShutdown struct{}
func (s *stepShutdown) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*godo.Client)
c := state.Get("config").(*Config)
ui := state.Get("ui").(packersdk.Ui)
dropletId := state.Get("droplet_id").(int)
// Gracefully power off the droplet. We have to retry this a number
// of times because sometimes it says it completed when it actually
// did absolutely nothing (*ALAKAZAM!* magic!). We give up after
// a pretty arbitrary amount of time.
ui.Say("Gracefully shutting down droplet...")
_, _, err := client.DropletActions.Shutdown(context.TODO(), dropletId)
if err != nil {
// If we get an error the first time, actually report it
err := fmt.Errorf("Error shutting down droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// A channel we use as a flag to end our goroutines
done := make(chan struct{})
shutdownRetryDone := make(chan struct{})
// Make sure we wait for the shutdown retry goroutine to end
// before moving on.
defer func() {
close(done)
<-shutdownRetryDone
}()
// Start a goroutine that just keeps trying to shut down the
// droplet.
go func() {
defer close(shutdownRetryDone)
for attempts := 2; attempts > 0; attempts++ {
log.Printf("ShutdownDroplet attempt #%d...", attempts)
_, _, err := client.DropletActions.Shutdown(context.TODO(), dropletId)
if err != nil {
log.Printf("Shutdown retry error: %s", err)
}
select {
case <-done:
return
case <-time.After(20 * time.Second):
// Retry!
}
}
}()
err = waitForDropletState("off", dropletId, client, c.StateTimeout)
if err != nil {
// If we get an error the first time, actually report it
err := fmt.Errorf("Error shutting down droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if err := waitForDropletUnlocked(client, dropletId, c.StateTimeout); err != nil {
// If we get an error the first time, actually report it
err := fmt.Errorf("Error shutting down droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
return multistep.ActionContinue
}
func (s *stepShutdown) Cleanup(state multistep.StateBag) {
// no cleanup
}

View File

@ -1,130 +0,0 @@
package digitalocean
import (
"context"
"errors"
"fmt"
"log"
"time"
"github.com/digitalocean/godo"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
type stepSnapshot struct {
snapshotTimeout time.Duration
}
func (s *stepSnapshot) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
client := state.Get("client").(*godo.Client)
ui := state.Get("ui").(packersdk.Ui)
c := state.Get("config").(*Config)
dropletId := state.Get("droplet_id").(int)
var snapshotRegions []string
ui.Say(fmt.Sprintf("Creating snapshot: %v", c.SnapshotName))
action, _, err := client.DropletActions.Snapshot(context.TODO(), dropletId, c.SnapshotName)
if err != nil {
err := fmt.Errorf("Error creating snapshot: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// With the pending state over, verify that we're in the active state
// because action can take a long time and may depend on the size of the final snapshot,
// the timeout is parameterized
ui.Say("Waiting for snapshot to complete...")
if err := waitForActionState(godo.ActionCompleted, dropletId, action.ID,
client, s.snapshotTimeout); err != nil {
// If we get an error the first time, actually report it
err := fmt.Errorf("Error waiting for snapshot: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
// Wait for the droplet to become unlocked first. For snapshots
// this can end up taking quite a long time, so we hardcode this to
// 20 minutes.
if err := waitForDropletUnlocked(client, dropletId, 20*time.Minute); err != nil {
// If we get an error the first time, actually report it
err := fmt.Errorf("Error shutting down droplet: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
log.Printf("Looking up snapshot ID for snapshot: %s", c.SnapshotName)
images, _, err := client.Droplets.Snapshots(context.TODO(), dropletId, nil)
if err != nil {
err := fmt.Errorf("Error looking up snapshot ID: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
if len(c.SnapshotRegions) > 0 {
regionSet := make(map[string]struct{})
regions := make([]string, 0, len(c.SnapshotRegions))
regionSet[c.Region] = struct{}{}
for _, region := range c.SnapshotRegions {
// If we already saw the region, then don't look again
if _, ok := regionSet[region]; ok {
continue
}
// Mark that we saw the region
regionSet[region] = struct{}{}
regions = append(regions, region)
}
snapshotRegions = regions
for transfer := range snapshotRegions {
transferRequest := &godo.ActionRequest{
"type": "transfer",
"region": snapshotRegions[transfer],
}
imageTransfer, _, err := client.ImageActions.Transfer(context.TODO(), images[0].ID, transferRequest)
if err != nil {
err := fmt.Errorf("Error transferring snapshot: %s", err)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
ui.Say(fmt.Sprintf("transferring Snapshot ID: %d", imageTransfer.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)
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
}
}
var imageId int
if len(images) == 1 {
imageId = images[0].ID
} else {
err := errors.New("Couldn't find snapshot to get the image ID. Bug?")
state.Put("error", err)
ui.Error(err.Error())
return multistep.ActionHalt
}
snapshotRegions = append(snapshotRegions, c.Region)
log.Printf("Snapshot image ID: %d", imageId)
state.Put("snapshot_image_id", imageId)
state.Put("snapshot_name", c.SnapshotName)
state.Put("regions", snapshotRegions)
return multistep.ActionContinue
}
func (s *stepSnapshot) Cleanup(state multistep.StateBag) {
// no cleanup
}

View File

@ -1,15 +0,0 @@
package digitalocean
import (
"golang.org/x/oauth2"
)
type apiTokenSource struct {
AccessToken string
}
func (t *apiTokenSource) Token() (*oauth2.Token, error) {
return &oauth2.Token{
AccessToken: t.AccessToken,
}, nil
}

View File

@ -1,13 +0,0 @@
package version
import (
"github.com/hashicorp/packer-plugin-sdk/version"
packerVersion "github.com/hashicorp/packer/version"
)
var DockerPluginVersion *version.PluginVersion
func init() {
DockerPluginVersion = version.InitializePluginVersion(
packerVersion.Version, packerVersion.VersionPrerelease)
}

View File

@ -1,209 +0,0 @@
package digitalocean
import (
"context"
"fmt"
"log"
"time"
"github.com/digitalocean/godo"
)
// waitForDropletUnlocked waits for the Droplet to be unlocked to
// avoid "pending" errors when making state changes.
func waitForDropletUnlocked(
client *godo.Client, dropletId int, timeout time.Duration) error {
done := make(chan struct{})
defer close(done)
result := make(chan error, 1)
go func() {
attempts := 0
for {
attempts += 1
log.Printf("[DEBUG] Checking droplet lock state... (attempt: %d)", attempts)
droplet, _, err := client.Droplets.Get(context.TODO(), dropletId)
if err != nil {
result <- err
return
}
if !droplet.Locked {
result <- nil
return
}
// Wait 3 seconds in between
time.Sleep(3 * time.Second)
// Verify we shouldn't exit
select {
case <-done:
// We finished, so just exit the goroutine
return
default:
// Keep going
}
}
}()
log.Printf("[DEBUG] Waiting for up to %d seconds for droplet to unlock", timeout/time.Second)
select {
case err := <-result:
return err
case <-time.After(timeout):
return fmt.Errorf(
"Timeout while waiting to for droplet to unlock")
}
}
// waitForDropletState simply blocks until the droplet is in
// a state we expect, while eventually timing out.
func waitForDropletState(
desiredState string, dropletId int,
client *godo.Client, timeout time.Duration) error {
done := make(chan struct{})
defer close(done)
result := make(chan error, 1)
go func() {
attempts := 0
for {
attempts += 1
log.Printf("Checking droplet status... (attempt: %d)", attempts)
droplet, _, err := client.Droplets.Get(context.TODO(), dropletId)
if err != nil {
result <- err
return
}
if droplet.Status == desiredState {
result <- nil
return
}
// Wait 3 seconds in between
time.Sleep(3 * time.Second)
// Verify we shouldn't exit
select {
case <-done:
// We finished, so just exit the goroutine
return
default:
// Keep going
}
}
}()
log.Printf("Waiting for up to %d seconds for droplet to become %s", timeout/time.Second, desiredState)
select {
case err := <-result:
return err
case <-time.After(timeout):
err := fmt.Errorf("Timeout while waiting to for droplet to become '%s'", desiredState)
return err
}
}
// waitForActionState simply blocks until the droplet action is in
// a state we expect, while eventually timing out.
func waitForActionState(
desiredState string, dropletId, actionId int,
client *godo.Client, timeout time.Duration) error {
done := make(chan struct{})
defer close(done)
result := make(chan error, 1)
go func() {
attempts := 0
for {
attempts += 1
log.Printf("Checking action status... (attempt: %d)", attempts)
action, _, err := client.DropletActions.Get(context.TODO(), dropletId, actionId)
if err != nil {
result <- err
return
}
if action.Status == desiredState {
result <- nil
return
}
// Wait 3 seconds in between
time.Sleep(3 * time.Second)
// Verify we shouldn't exit
select {
case <-done:
// We finished, so just exit the goroutine
return
default:
// Keep going
}
}
}()
log.Printf("Waiting for up to %d seconds for action to become %s", timeout/time.Second, desiredState)
select {
case err := <-result:
return err
case <-time.After(timeout):
err := fmt.Errorf("Timeout while waiting to for action to become '%s'", desiredState)
return err
}
}
// WaitForImageState simply blocks until the image action is in
// a state we expect, while eventually timing out.
func WaitForImageState(
desiredState string, imageId, actionId int,
client *godo.Client, timeout time.Duration) error {
done := make(chan struct{})
defer close(done)
result := make(chan error, 1)
go func() {
attempts := 0
for {
attempts += 1
log.Printf("Checking action status... (attempt: %d)", attempts)
action, _, err := client.ImageActions.Get(context.TODO(), imageId, actionId)
if err != nil {
result <- err
return
}
if action.Status == desiredState {
result <- nil
return
}
// Wait 3 seconds in between
time.Sleep(3 * time.Second)
// Verify we shouldn't exit
select {
case <-done:
// We finished, so just exit the goroutine
return
default:
// Keep going
}
}
}()
log.Printf("Waiting for up to %d seconds for image transfer to become %s", timeout/time.Second, desiredState)
select {
case err := <-result:
return err
case <-time.After(timeout):
err := fmt.Errorf("Timeout while waiting to for image transfer to become '%s'", desiredState)
return err
}
}

View File

@ -16,7 +16,6 @@ import (
azurearmbuilder "github.com/hashicorp/packer/builder/azure/arm"
azurechrootbuilder "github.com/hashicorp/packer/builder/azure/chroot"
azuredtlbuilder "github.com/hashicorp/packer/builder/azure/dtl"
digitaloceanbuilder "github.com/hashicorp/packer/builder/digitalocean"
filebuilder "github.com/hashicorp/packer/builder/file"
hcloudbuilder "github.com/hashicorp/packer/builder/hcloud"
lxcbuilder "github.com/hashicorp/packer/builder/lxc"
@ -32,7 +31,6 @@ 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"
manifestpostprocessor "github.com/hashicorp/packer/post-processor/manifest"
shelllocalpostprocessor "github.com/hashicorp/packer/post-processor/shell-local"
yandexexportpostprocessor "github.com/hashicorp/packer/post-processor/yandex-export"
@ -58,7 +56,6 @@ var Builders = map[string]packersdk.Builder{
"azure-arm": new(azurearmbuilder.Builder),
"azure-chroot": new(azurechrootbuilder.Builder),
"azure-dtl": new(azuredtlbuilder.Builder),
"digitalocean": new(digitaloceanbuilder.Builder),
"file": new(filebuilder.Builder),
"hcloud": new(hcloudbuilder.Builder),
"lxc": new(lxcbuilder.Builder),
@ -88,14 +85,13 @@ var Provisioners = map[string]packersdk.Provisioner{
}
var PostProcessors = map[string]packersdk.PostProcessor{
"artifice": new(artificepostprocessor.PostProcessor),
"checksum": new(checksumpostprocessor.PostProcessor),
"compress": new(compresspostprocessor.PostProcessor),
"digitalocean-import": new(digitaloceanimportpostprocessor.PostProcessor),
"manifest": new(manifestpostprocessor.PostProcessor),
"shell-local": new(shelllocalpostprocessor.PostProcessor),
"yandex-export": new(yandexexportpostprocessor.PostProcessor),
"yandex-import": new(yandeximportpostprocessor.PostProcessor),
"artifice": new(artificepostprocessor.PostProcessor),
"checksum": new(checksumpostprocessor.PostProcessor),
"compress": new(compresspostprocessor.PostProcessor),
"manifest": new(manifestpostprocessor.PostProcessor),
"shell-local": new(shelllocalpostprocessor.PostProcessor),
"yandex-export": new(yandexexportpostprocessor.PostProcessor),
"yandex-import": new(yandeximportpostprocessor.PostProcessor),
}
var Datasources = map[string]packersdk.Datasource{}

View File

@ -23,6 +23,8 @@ import (
chefclientprovisioner "github.com/hashicorp/packer-plugin-chef/provisioner/chef-client"
chefsoloprovisioner "github.com/hashicorp/packer-plugin-chef/provisioner/chef-solo"
cloudstackbuilder "github.com/hashicorp/packer-plugin-cloudstack/builder/cloudstack"
digitaloceanbuilder "github.com/hashicorp/packer-plugin-digitalocean/builder/digitalocean"
digitaloceanimportpostprocessor "github.com/hashicorp/packer-plugin-digitalocean/post-processor/digitalocean-import"
dockerbuilder "github.com/hashicorp/packer-plugin-docker/builder/docker"
dockerimportpostprocessor "github.com/hashicorp/packer-plugin-docker/post-processor/docker-import"
dockerpushpostprocessor "github.com/hashicorp/packer-plugin-docker/post-processor/docker-push"
@ -83,6 +85,7 @@ var VendoredBuilders = map[string]packersdk.Builder{
"amazon-ebsvolume": new(amazonebsvolumebuilder.Builder),
"amazon-instance": new(amazoninstancebuilder.Builder),
"cloudstack": new(cloudstackbuilder.Builder),
"digitalocean": new(digitaloceanbuilder.Builder),
"docker": new(dockerbuilder.Builder),
"googlecompute": new(googlecomputebuilder.Builder),
"hyperv-iso": new(hypervisobuilder.Builder),
@ -131,6 +134,7 @@ var VendoredProvisioners = map[string]packersdk.Provisioner{
var VendoredPostProcessors = map[string]packersdk.PostProcessor{
"alicloud-import": new(alicloudimportpostprocessor.PostProcessor),
"amazon-import": new(anazibimportpostprocessor.PostProcessor),
"digitalocean-import": new(digitaloceanimportpostprocessor.PostProcessor),
"docker-import": new(dockerimportpostprocessor.PostProcessor),
"docker-push": new(dockerpushpostprocessor.PostProcessor),
"docker-save": new(dockersavepostprocessor.PostProcessor),

3
go.mod
View File

@ -16,7 +16,7 @@ require (
github.com/cheggaaa/pb v1.0.27
github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e
github.com/dgrijalva/jwt-go v3.2.0+incompatible
github.com/digitalocean/godo v1.11.1
github.com/digitalocean/godo v1.60.0
github.com/dsnet/compress v0.0.1
github.com/exoscale/packer-plugin-exoscale v0.1.1
github.com/go-ini/ini v1.25.4
@ -43,6 +43,7 @@ require (
github.com/hashicorp/packer-plugin-chef v0.0.1
github.com/hashicorp/packer-plugin-cloudstack v0.0.1
github.com/hashicorp/packer-plugin-converge v0.0.1
github.com/hashicorp/packer-plugin-digitalocean v0.0.1
github.com/hashicorp/packer-plugin-docker v0.0.7
github.com/hashicorp/packer-plugin-googlecompute v0.0.1
github.com/hashicorp/packer-plugin-hyperone v0.0.1

5
go.sum
View File

@ -212,8 +212,9 @@ github.com/digitalocean/go-qemu v0.0.0-20181112162955-dd7bb9c771b8/go.mod h1:/Yn
github.com/digitalocean/go-qemu v0.0.0-20201211181942-d361e7b4965f/go.mod h1:y4Eq3ZfZQFWQwVyW0qvgo5seXUIq2C7BlHsdE+xtXL4=
github.com/digitalocean/go-qemu v0.0.0-20210326154740-ac9e0b687001 h1:WAg57gnaAWWjMAELcwHjc2xy0PoXQ5G+vn3+XS6s1jI=
github.com/digitalocean/go-qemu v0.0.0-20210326154740-ac9e0b687001/go.mod h1:IetBE52JfFxK46p2n2Rqm+p5Gx1gpu2hRHsrbnPOWZQ=
github.com/digitalocean/godo v1.11.1 h1:OsTh37YFKk+g6DnAOrkXJ9oDArTkRx5UTkBJ2EWAO38=
github.com/digitalocean/godo v1.11.1/go.mod h1:h6faOIcZ8lWIwNQ+DN7b3CgX4Kwby5T+nbpNqkUIozU=
github.com/digitalocean/godo v1.60.0 h1:o/vimtn/HKtYSakFAAZ59Zc5ASORd41S4z1X7pAXPn8=
github.com/digitalocean/godo v1.60.0/go.mod h1:p7dOjjtSBqCTUksqtA5Fd3uaKs9kyTq2xcz76ulEJRU=
github.com/dimchansky/utfbom v1.1.0 h1:FcM3g+nofKgUteL8dm/UpdRXNC9KmADgTpLKsu0TRo4=
github.com/dimchansky/utfbom v1.1.0/go.mod h1:rO41eb7gLfo8SF1jd9F8HplJm1Fewwi4mQvIirEdv+8=
github.com/dnaeon/go-vcr v1.0.1 h1:r8L/HqC0Hje5AXMu1ooW8oyQyOFv4GxqpL0nRP7SLLY=
@ -501,6 +502,8 @@ github.com/hashicorp/packer-plugin-cloudstack v0.0.1 h1:BF9nXRlA0xQV5W/+CoLjWn0a
github.com/hashicorp/packer-plugin-cloudstack v0.0.1/go.mod h1:fx13TY2szz6cm2e99xzU3gQzKdGVwysxY2TyKr0r8MQ=
github.com/hashicorp/packer-plugin-converge v0.0.1 h1:cjrNt2Q/BuSH2o2bpNV91DhWYSTN7vb4LwxwFXULcok=
github.com/hashicorp/packer-plugin-converge v0.0.1/go.mod h1:3Rm0fAiVwFriSRrwt3dsuKInYYzuOa6tqPFGMOW7noI=
github.com/hashicorp/packer-plugin-digitalocean v0.0.1 h1:dkRePO1ojRgoTCtexg6FtaH2kO5xOzEwdI+yoJdToh8=
github.com/hashicorp/packer-plugin-digitalocean v0.0.1/go.mod h1:CgUqpMLQfVEntLDSH4cDh/wu6X8A/ARs1eUjRGaJMD4=
github.com/hashicorp/packer-plugin-docker v0.0.7 h1:hMTrH7vrkFIjphtbbtpuzffTzSjMNgxayo2DPLz9y+c=
github.com/hashicorp/packer-plugin-docker v0.0.7/go.mod h1:IpeKlwOSy2kdgQcysqd3gCsoqjME9jtmpFoKxn7RRNI=
github.com/hashicorp/packer-plugin-googlecompute v0.0.1 h1:Shjio88MraB+ocj0VI5+M65r4UBKbYI4eCqLNyPXKEo=

View File

@ -1,392 +0,0 @@
//go:generate packer-sdc mapstructure-to-hcl2 -type Config
package digitaloceanimport
import (
"context"
"fmt"
"log"
"os"
"strings"
"time"
"golang.org/x/oauth2"
"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/hcl/v2/hcldec"
"github.com/hashicorp/packer-plugin-sdk/common"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template/config"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
"github.com/hashicorp/packer/builder/digitalocean"
)
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) ConfigSpec() hcldec.ObjectSpec { return p.config.FlatMapstructure().HCL2Spec() }
func (p *PostProcessor) Configure(raws ...interface{}) error {
err := config.Decode(&p.config, &config.DecodeOpts{
PluginType: BuilderId,
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(packersdk.MultiError)
if err = interpolate.Validate(p.config.ObjectName, &p.config.ctx); err != nil {
errs = packersdk.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,
}
for key, ptr := range requiredArgs {
if *ptr == "" {
errs = packersdk.MultiErrorAppend(
errs, fmt.Errorf("%s must be set", key))
}
}
if len(p.config.ImageRegions) == 0 {
errs = packersdk.MultiErrorAppend(
errs, fmt.Errorf("image_regions must be set"))
}
if len(errs.Errors) > 0 {
return errs
}
packersdk.LogSecretFilter.Set(p.config.SpacesKey, p.config.SpacesSecret, p.config.APIToken)
log.Println(p.config)
return nil
}
func (p *PostProcessor) PostProcess(ctx context.Context, ui packersdk.Ui, artifact packersdk.Artifact) (packersdk.Artifact, bool, bool, error) {
var err error
generatedData := artifact.State("generated_data")
if generatedData == nil {
// Make sure it's not a nil map so we can assign to it later.
generatedData = make(map[string]interface{})
}
p.config.ctx.Data = generatedData
p.config.ObjectName, err = interpolate.Render(p.config.ObjectName, &p.config.ctx)
if err != nil {
return nil, false, false, fmt.Errorf("Error rendering space_object_name template: %s", err)
}
log.Printf("Rendered space_object_name as %s", p.config.ObjectName)
log.Println("Looking for image in artifact")
source, err := extractImageArtifact(artifact.Files())
if err != nil {
return nil, false, 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, err := session.NewSession(spacesConfig)
if err != nil {
return nil, false, false, err
}
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, 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(context.Background(), &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, 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, 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, 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, false, err
}
}
return artifact, false, false, nil
}
func extractImageArtifact(artifacts []string) (string, error) {
artifactCount := len(artifacts)
if artifactCount == 0 {
return "", fmt.Errorf("no artifacts were provided")
}
if artifactCount == 1 {
return artifacts[0], nil
}
validSuffix := []string{"raw", "img", "qcow2", "vhdx", "vdi", "vmdk", "tar.bz2", "tar.xz", "tar.gz"}
for _, path := range artifacts {
for _, suffix := range validSuffix {
if strings.HasSuffix(path, suffix) {
return path, nil
}
}
}
return "", fmt.Errorf("no valid image file found")
}
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
}

View File

@ -1,71 +0,0 @@
// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT.
package digitaloceanimport
import (
"github.com/hashicorp/hcl/v2/hcldec"
"github.com/zclconf/go-cty/cty"
)
// FlatConfig is an auto-generated flat version of Config.
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
type FlatConfig struct {
PackerBuildName *string `mapstructure:"packer_build_name" cty:"packer_build_name" hcl:"packer_build_name"`
PackerBuilderType *string `mapstructure:"packer_builder_type" cty:"packer_builder_type" hcl:"packer_builder_type"`
PackerCoreVersion *string `mapstructure:"packer_core_version" cty:"packer_core_version" hcl:"packer_core_version"`
PackerDebug *bool `mapstructure:"packer_debug" cty:"packer_debug" hcl:"packer_debug"`
PackerForce *bool `mapstructure:"packer_force" cty:"packer_force" hcl:"packer_force"`
PackerOnError *string `mapstructure:"packer_on_error" cty:"packer_on_error" hcl:"packer_on_error"`
PackerUserVars map[string]string `mapstructure:"packer_user_variables" cty:"packer_user_variables" hcl:"packer_user_variables"`
PackerSensitiveVars []string `mapstructure:"packer_sensitive_variables" cty:"packer_sensitive_variables" hcl:"packer_sensitive_variables"`
APIToken *string `mapstructure:"api_token" cty:"api_token" hcl:"api_token"`
SpacesKey *string `mapstructure:"spaces_key" cty:"spaces_key" hcl:"spaces_key"`
SpacesSecret *string `mapstructure:"spaces_secret" cty:"spaces_secret" hcl:"spaces_secret"`
SpacesRegion *string `mapstructure:"spaces_region" cty:"spaces_region" hcl:"spaces_region"`
SpaceName *string `mapstructure:"space_name" cty:"space_name" hcl:"space_name"`
ObjectName *string `mapstructure:"space_object_name" cty:"space_object_name" hcl:"space_object_name"`
SkipClean *bool `mapstructure:"skip_clean" cty:"skip_clean" hcl:"skip_clean"`
Tags []string `mapstructure:"image_tags" cty:"image_tags" hcl:"image_tags"`
Name *string `mapstructure:"image_name" cty:"image_name" hcl:"image_name"`
Description *string `mapstructure:"image_description" cty:"image_description" hcl:"image_description"`
Distribution *string `mapstructure:"image_distribution" cty:"image_distribution" hcl:"image_distribution"`
ImageRegions []string `mapstructure:"image_regions" cty:"image_regions" hcl:"image_regions"`
Timeout *string `mapstructure:"timeout" cty:"timeout" hcl:"timeout"`
}
// FlatMapstructure returns a new FlatConfig.
// FlatConfig is an auto-generated flat version of Config.
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
func (*Config) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
return new(FlatConfig)
}
// HCL2Spec returns the hcl spec of a Config.
// This spec is used by HCL to read the fields of Config.
// The decoded values from this spec will then be applied to a FlatConfig.
func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
s := map[string]hcldec.Spec{
"packer_build_name": &hcldec.AttrSpec{Name: "packer_build_name", Type: cty.String, Required: false},
"packer_builder_type": &hcldec.AttrSpec{Name: "packer_builder_type", Type: cty.String, Required: false},
"packer_core_version": &hcldec.AttrSpec{Name: "packer_core_version", Type: cty.String, Required: false},
"packer_debug": &hcldec.AttrSpec{Name: "packer_debug", Type: cty.Bool, Required: false},
"packer_force": &hcldec.AttrSpec{Name: "packer_force", Type: cty.Bool, Required: false},
"packer_on_error": &hcldec.AttrSpec{Name: "packer_on_error", Type: cty.String, Required: false},
"packer_user_variables": &hcldec.AttrSpec{Name: "packer_user_variables", Type: cty.Map(cty.String), Required: false},
"packer_sensitive_variables": &hcldec.AttrSpec{Name: "packer_sensitive_variables", Type: cty.List(cty.String), Required: false},
"api_token": &hcldec.AttrSpec{Name: "api_token", Type: cty.String, Required: false},
"spaces_key": &hcldec.AttrSpec{Name: "spaces_key", Type: cty.String, Required: false},
"spaces_secret": &hcldec.AttrSpec{Name: "spaces_secret", Type: cty.String, Required: false},
"spaces_region": &hcldec.AttrSpec{Name: "spaces_region", Type: cty.String, Required: false},
"space_name": &hcldec.AttrSpec{Name: "space_name", Type: cty.String, Required: false},
"space_object_name": &hcldec.AttrSpec{Name: "space_object_name", Type: cty.String, Required: false},
"skip_clean": &hcldec.AttrSpec{Name: "skip_clean", Type: cty.Bool, Required: false},
"image_tags": &hcldec.AttrSpec{Name: "image_tags", Type: cty.List(cty.String), Required: false},
"image_name": &hcldec.AttrSpec{Name: "image_name", Type: cty.String, Required: false},
"image_description": &hcldec.AttrSpec{Name: "image_description", Type: cty.String, Required: false},
"image_distribution": &hcldec.AttrSpec{Name: "image_distribution", Type: cty.String, Required: false},
"image_regions": &hcldec.AttrSpec{Name: "image_regions", Type: cty.List(cty.String), Required: false},
"timeout": &hcldec.AttrSpec{Name: "timeout", Type: cty.String, Required: false},
}
return s
}

View File

@ -1,39 +0,0 @@
package digitaloceanimport
import (
"testing"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
func TestPostProcessor_ImplementsPostProcessor(t *testing.T) {
var _ packersdk.PostProcessor = new(PostProcessor)
}
func TestPostProcessor_ImageArtifactExtraction(t *testing.T) {
tt := []struct {
Name string
Source string
Artifacts []string
ExpectedError string
}{
{Name: "EmptyArtifacts", ExpectedError: "no artifacts were provided"},
{Name: "SingleArtifact", Source: "Sample.img", Artifacts: []string{"Sample.img"}},
{Name: "SupportedArtifact", Source: "Example.tar.xz", Artifacts: []string{"Sample", "SomeZip.zip", "Example.tar.xz"}},
{Name: "FirstSupportedArtifact", Source: "SomeVMDK.vmdk", Artifacts: []string{"Sample", "SomeVMDK.vmdk", "Example.xz"}},
{Name: "NonSupportedArtifact", Artifacts: []string{"Sample", "SomeZip.zip", "Example.xz"}, ExpectedError: "no valid image file found"},
}
for _, tc := range tt {
tc := tc
source, err := extractImageArtifact(tc.Artifacts)
if tc.Source != source {
t.Errorf("expected the source to be %q, but got %q", tc.Source, source)
}
if err != nil && (tc.ExpectedError != err.Error()) {
t.Errorf("unexpected error received; expected %q, but got %q", tc.ExpectedError, err.Error())
}
}
}

View File

@ -1,13 +0,0 @@
package version
import (
"github.com/hashicorp/packer-plugin-sdk/version"
packerVersion "github.com/hashicorp/packer/version"
)
var DigitalOceanImportPluginVersion *version.PluginVersion
func init() {
DigitalOceanImportPluginVersion = version.InitializePluginVersion(
packerVersion.Version, packerVersion.VersionPrerelease)
}

View File

@ -1,69 +0,0 @@
---
description: >
The digitalocean Packer builder is able to create new images for use with
DigitalOcean. The builder takes a source image, runs any provisioning
necessary
on the image after launching it, then snapshots it into a reusable image. This
reusable image can then be used as the foundation of new servers that are
launched within DigitalOcean.
page_title: DigitalOcean - Builders
---
# DigitalOcean Builder
Type: `digitalocean`
Artifact BuilderId: `pearkes.digitalocean`
The `digitalocean` Packer builder is able to create new images for use with
[DigitalOcean](https://www.digitalocean.com). The builder takes a source image,
runs any provisioning necessary on the image after launching it, then snapshots
it into a reusable image. This reusable image can then be used as the
foundation of new servers that are launched within DigitalOcean.
The builder does _not_ manage images. Once it creates an image, it is up to you
to use it or delete it.
## Configuration Reference
There are many configuration options available for the builder. They are
segmented below into two categories: required and optional parameters. Within
each category, the available configuration keys are alphabetized.
### Required:
@include 'builder/digitalocean/Config-required.mdx'
### Optional:
@include 'builder/digitalocean/Config-not-required.mdx'
## Basic Example
Here is a basic example. It is completely valid as soon as you enter your own
access tokens:
```json
{
"type": "digitalocean",
"api_token": "YOUR API KEY",
"image": "ubuntu-16-04-x64",
"region": "nyc3",
"size": "512mb",
"ssh_username": "root"
}
```
### Communicator Config
In addition to the builder options, a
[communicator](/docs/templates/legacy_json_templates/communicator) can be configured for this builder.
@include 'packer-plugin-sdk/communicator/Config-not-required.mdx'
@include 'packer-plugin-sdk/communicator/SSH-not-required.mdx'
@include 'packer-plugin-sdk/communicator/SSH-Private-Key-File-not-required.mdx'

View File

@ -1,130 +0,0 @@
---
description: |
The Packer DigitalOcean Import post-processor takes an image artifact
from various builders and imports it to DigitalOcean.
page_title: DigitalOcean Import - Post-Processors
---
# DigitalOcean Import Post-Processor
Type: `digitalocean-import`
Artifact BuilderId: `packer.post-processor.digitalocean-import`
The Packer DigitalOcean Import post-processor is used to import images created by other Packer builders to DigitalOcean.
~> Note: Users looking to create custom images, and reusable snapshots, directly on DigitalOcean can use
the [DigitalOcean builder](/docs/builders/digitalocean) without this post-processor.
## 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).
## 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.
- `keep_input_artifact` (boolean) - if true, do not delete the source virtual
machine image after importing it to the cloud. Defaults to false.
- `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. This is treated as a
[template engine](/docs/templates/legacy_json_templates/engine). Therefore, you
may use user variables and template functions in this field.
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:
<Tabs>
<Tab heading="JSON">
```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"]
}
```
</Tab>
<Tab heading="HCL2">
```hcl
post-processor "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"]
}
```
</Tab>
</Tabs>

View File

@ -696,10 +696,6 @@
}
]
},
{
"title": "DigitalOcean",
"path": "builders/digitalocean"
},
{
"title": "File",
"path": "builders/file"
@ -848,10 +844,6 @@
"title": "Checksum",
"path": "post-processors/checksum"
},
{
"title": "DigitalOcean Import",
"path": "post-processors/digitalocean-import"
},
{
"title": "Manifest",
"path": "post-processors/manifest"

View File

@ -39,6 +39,13 @@
"pluginTier": "community",
"version": "latest"
},
{
"title": "DigitalOcean",
"path": "digitalocean",
"repo": "hashicorp/packer-plugin-digitalocean",
"pluginTier": "community",
"version": "latest"
},
{
"title": "Docker",
"path": "docker",