remove digitalocean directories, revendor, add to vendored_plugins, regenerate code, and update website paths (#10961)
This commit is contained in:
parent
d0a15f9a15
commit
6b59525408
|
@ -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
|
||||
}
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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": ""
|
||||
}]
|
||||
}
|
||||
`
|
|
@ -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")
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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{}
|
||||
|
|
|
@ -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
3
go.mod
|
@ -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
5
go.sum
|
@ -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=
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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'
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
|
@ -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",
|
||||
|
|
Loading…
Reference in New Issue