Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 247380f586 |
@@ -0,0 +1,18 @@
|
|||||||
|
comment:
|
||||||
|
layout: "flags, files"
|
||||||
|
behavior: default
|
||||||
|
require_changes: true # only comment on changes in coverage
|
||||||
|
require_base: yes # [yes :: must have a base report to post]
|
||||||
|
require_head: yes # [yes :: must have a head report to post]
|
||||||
|
after_n_builds: 3 # wait for all OS test coverage builds to post comment
|
||||||
|
branches: # branch names that can post comment
|
||||||
|
- "master"
|
||||||
|
|
||||||
|
coverage:
|
||||||
|
status:
|
||||||
|
project: off
|
||||||
|
patch: off
|
||||||
|
|
||||||
|
ignore: # ignore hcl2spec generated code for coverage and mocks
|
||||||
|
- "**/*.hcl2spec.go"
|
||||||
|
- "**/*_mock.go"
|
||||||
-25
@@ -2,10 +2,6 @@
|
|||||||
|
|
||||||
# builders
|
# builders
|
||||||
|
|
||||||
/examples/alicloud/ @chhaj5236 @alexyueer
|
|
||||||
/builder/alicloud/ @chhaj5236 @alexyueer
|
|
||||||
/website/pages/docs/builders/alicloud* @chhaj5236 @alexyueer
|
|
||||||
|
|
||||||
/examples/azure/ @paulmey
|
/examples/azure/ @paulmey
|
||||||
/builder/azure/ @paulmey
|
/builder/azure/ @paulmey
|
||||||
/website/pages/docs/builders/azure* @paulmey
|
/website/pages/docs/builders/azure* @paulmey
|
||||||
@@ -13,13 +9,6 @@
|
|||||||
/builder/digitalocean/ @andrewsomething
|
/builder/digitalocean/ @andrewsomething
|
||||||
/website/pages/docs/builders/digitalocean* @andrewsomething
|
/website/pages/docs/builders/digitalocean* @andrewsomething
|
||||||
|
|
||||||
/examples/jdcloud/ @XiaohanLiang @remrain
|
|
||||||
/builder/jdcloud/ @XiaohanLiang @remrain
|
|
||||||
/website/pages/docs/builders/jdcloud* @XiaohanLiang @remrain
|
|
||||||
|
|
||||||
/builder/linode/ @stvnjacobs @charliekenney23 @phillc
|
|
||||||
/website/pages/docs/builders/linode* @stvnjacobs @charliekenney23 @phillc
|
|
||||||
|
|
||||||
/builder/lxc/ @ChrisLundquist
|
/builder/lxc/ @ChrisLundquist
|
||||||
/website/pages/docs/builders/lxc* @ChrisLundquist
|
/website/pages/docs/builders/lxc* @ChrisLundquist
|
||||||
/test/fixtures/builder-lxc/ @ChrisLundquist
|
/test/fixtures/builder-lxc/ @ChrisLundquist
|
||||||
@@ -40,15 +29,6 @@
|
|||||||
/builder/triton/ @sean-
|
/builder/triton/ @sean-
|
||||||
/website/pages/docs/builders/triton* @sean-
|
/website/pages/docs/builders/triton* @sean-
|
||||||
|
|
||||||
/builder/proxmox/ @carlpett
|
|
||||||
/website/pages/docs/builders/proxmox* @carlpett
|
|
||||||
|
|
||||||
/builder/scaleway/ @scaleway/devtools
|
|
||||||
/website/pages/docs/builders/scaleway* @scaleway/devtools
|
|
||||||
|
|
||||||
/builder/hcloud/ @LKaemmerling
|
|
||||||
/website/pages/docs/builders/hcloud* @LKaemmerling
|
|
||||||
|
|
||||||
/builder/yandex/ @GennadySpb @alexanderKhaustov @seukyaso
|
/builder/yandex/ @GennadySpb @alexanderKhaustov @seukyaso
|
||||||
/website/pages/docs/builders/yandex* @GennadySpb @alexanderKhaustov @seukyaso
|
/website/pages/docs/builders/yandex* @GennadySpb @alexanderKhaustov @seukyaso
|
||||||
|
|
||||||
@@ -59,14 +39,9 @@
|
|||||||
# provisioners
|
# provisioners
|
||||||
|
|
||||||
/examples/ansible/ @bhcleek
|
/examples/ansible/ @bhcleek
|
||||||
/provisioner/ansible/ @bhcleek
|
|
||||||
/provisioner/converge/ @stevendborrelli
|
|
||||||
|
|
||||||
# post-processors
|
# post-processors
|
||||||
|
|
||||||
/post-processor/alicloud-import/ dongxiao.zzh@alibaba-inc.com
|
|
||||||
/post-processor/checksum/ v.tolstov@selfip.ru
|
/post-processor/checksum/ v.tolstov@selfip.ru
|
||||||
/post-processor/googlecompute-export/ crunkleton@google.com
|
|
||||||
/post-processor/yandex-export/ @GennadySpb
|
/post-processor/yandex-export/ @GennadySpb
|
||||||
/post-processor/yandex-import/ @GennadySpb
|
/post-processor/yandex-import/ @GennadySpb
|
||||||
/post-processor/vsphere-template/ nelson@bennu.cl
|
|
||||||
|
|||||||
@@ -302,10 +302,6 @@ type Config struct {
|
|||||||
// group and VM name allows one to execute commands to update the VM during a
|
// group and VM name allows one to execute commands to update the VM during a
|
||||||
// Packer build, e.g. attach a resource disk to the VM.
|
// Packer build, e.g. attach a resource disk to the VM.
|
||||||
TempComputeName string `mapstructure:"temp_compute_name" required:"false"`
|
TempComputeName string `mapstructure:"temp_compute_name" required:"false"`
|
||||||
// temporary name assigned to the Nic. If this
|
|
||||||
// value is not set, a random value will be assigned. Being able to assign a custom
|
|
||||||
// nicname could ease deployment if naming conventions are used.
|
|
||||||
TempNicName string `mapstructure:"temp_nic_name" required:"false"`
|
|
||||||
// name assigned to the temporary resource group created during the build.
|
// name assigned to the temporary resource group created during the build.
|
||||||
// If this value is not set, a random value will be assigned. This resource
|
// If this value is not set, a random value will be assigned. This resource
|
||||||
// group is deleted at the end of the build.
|
// group is deleted at the end of the build.
|
||||||
@@ -399,10 +395,6 @@ type Config struct {
|
|||||||
// machine. For Linux this configures an SSH authorized key. For Windows
|
// machine. For Linux this configures an SSH authorized key. For Windows
|
||||||
// this configures a WinRM certificate.
|
// this configures a WinRM certificate.
|
||||||
OSType string `mapstructure:"os_type" required:"false"`
|
OSType string `mapstructure:"os_type" required:"false"`
|
||||||
// temporary name assigned to the OSDisk. If this
|
|
||||||
// value is not set, a random value will be assigned. Being able to assign a custom
|
|
||||||
// osDiskName could ease deployment if naming conventions are used.
|
|
||||||
TempOSDiskName string `mapstructure:"temp_os_disk_name" required:"false"`
|
|
||||||
// Specify the size of the OS disk in GB
|
// Specify the size of the OS disk in GB
|
||||||
// (gigabytes). Values of zero or less than zero are ignored.
|
// (gigabytes). Values of zero or less than zero are ignored.
|
||||||
OSDiskSizeGB int32 `mapstructure:"os_disk_size_gb" required:"false"`
|
OSDiskSizeGB int32 `mapstructure:"os_disk_size_gb" required:"false"`
|
||||||
@@ -712,18 +704,9 @@ func setRuntimeValues(c *Config) {
|
|||||||
} else if c.TempResourceGroupName != "" && c.BuildResourceGroupName == "" {
|
} else if c.TempResourceGroupName != "" && c.BuildResourceGroupName == "" {
|
||||||
c.tmpResourceGroupName = c.TempResourceGroupName
|
c.tmpResourceGroupName = c.TempResourceGroupName
|
||||||
}
|
}
|
||||||
if c.TempNicName == "" {
|
|
||||||
c.tmpNicName = tempName.NicName
|
|
||||||
} else {
|
|
||||||
c.tmpNicName = c.TempNicName
|
|
||||||
}
|
|
||||||
c.tmpNicName = tempName.NicName
|
c.tmpNicName = tempName.NicName
|
||||||
c.tmpPublicIPAddressName = tempName.PublicIPAddressName
|
c.tmpPublicIPAddressName = tempName.PublicIPAddressName
|
||||||
if c.TempOSDiskName == "" {
|
c.tmpOSDiskName = tempName.OSDiskName
|
||||||
c.tmpOSDiskName = tempName.OSDiskName
|
|
||||||
} else {
|
|
||||||
c.tmpOSDiskName = c.TempOSDiskName
|
|
||||||
}
|
|
||||||
c.tmpDataDiskName = tempName.DataDiskName
|
c.tmpDataDiskName = tempName.DataDiskName
|
||||||
c.tmpSubnetName = tempName.SubnetName
|
c.tmpSubnetName = tempName.SubnetName
|
||||||
c.tmpVirtualNetworkName = tempName.VirtualNetworkName
|
c.tmpVirtualNetworkName = tempName.VirtualNetworkName
|
||||||
|
|||||||
@@ -58,7 +58,6 @@ type FlatConfig struct {
|
|||||||
ResourceGroupName *string `mapstructure:"resource_group_name" cty:"resource_group_name" hcl:"resource_group_name"`
|
ResourceGroupName *string `mapstructure:"resource_group_name" cty:"resource_group_name" hcl:"resource_group_name"`
|
||||||
StorageAccount *string `mapstructure:"storage_account" cty:"storage_account" hcl:"storage_account"`
|
StorageAccount *string `mapstructure:"storage_account" cty:"storage_account" hcl:"storage_account"`
|
||||||
TempComputeName *string `mapstructure:"temp_compute_name" required:"false" cty:"temp_compute_name" hcl:"temp_compute_name"`
|
TempComputeName *string `mapstructure:"temp_compute_name" required:"false" cty:"temp_compute_name" hcl:"temp_compute_name"`
|
||||||
TempNicName *string `mapstructure:"temp_nic_name" required:"false" cty:"temp_nic_name" hcl:"temp_nic_name"`
|
|
||||||
TempResourceGroupName *string `mapstructure:"temp_resource_group_name" cty:"temp_resource_group_name" hcl:"temp_resource_group_name"`
|
TempResourceGroupName *string `mapstructure:"temp_resource_group_name" cty:"temp_resource_group_name" hcl:"temp_resource_group_name"`
|
||||||
BuildResourceGroupName *string `mapstructure:"build_resource_group_name" cty:"build_resource_group_name" hcl:"build_resource_group_name"`
|
BuildResourceGroupName *string `mapstructure:"build_resource_group_name" cty:"build_resource_group_name" hcl:"build_resource_group_name"`
|
||||||
BuildKeyVaultName *string `mapstructure:"build_key_vault_name" cty:"build_key_vault_name" hcl:"build_key_vault_name"`
|
BuildKeyVaultName *string `mapstructure:"build_key_vault_name" cty:"build_key_vault_name" hcl:"build_key_vault_name"`
|
||||||
@@ -71,7 +70,6 @@ type FlatConfig struct {
|
|||||||
PlanInfo *FlatPlanInformation `mapstructure:"plan_info" required:"false" cty:"plan_info" hcl:"plan_info"`
|
PlanInfo *FlatPlanInformation `mapstructure:"plan_info" required:"false" cty:"plan_info" hcl:"plan_info"`
|
||||||
PollingDurationTimeout *string `mapstructure:"polling_duration_timeout" required:"false" cty:"polling_duration_timeout" hcl:"polling_duration_timeout"`
|
PollingDurationTimeout *string `mapstructure:"polling_duration_timeout" required:"false" cty:"polling_duration_timeout" hcl:"polling_duration_timeout"`
|
||||||
OSType *string `mapstructure:"os_type" required:"false" cty:"os_type" hcl:"os_type"`
|
OSType *string `mapstructure:"os_type" required:"false" cty:"os_type" hcl:"os_type"`
|
||||||
TempOSDiskName *string `mapstructure:"temp_os_disk_name" required:"false" cty:"temp_os_disk_name" hcl:"temp_os_disk_name"`
|
|
||||||
OSDiskSizeGB *int32 `mapstructure:"os_disk_size_gb" required:"false" cty:"os_disk_size_gb" hcl:"os_disk_size_gb"`
|
OSDiskSizeGB *int32 `mapstructure:"os_disk_size_gb" required:"false" cty:"os_disk_size_gb" hcl:"os_disk_size_gb"`
|
||||||
AdditionalDiskSize []int32 `mapstructure:"disk_additional_size" required:"false" cty:"disk_additional_size" hcl:"disk_additional_size"`
|
AdditionalDiskSize []int32 `mapstructure:"disk_additional_size" required:"false" cty:"disk_additional_size" hcl:"disk_additional_size"`
|
||||||
DiskCachingType *string `mapstructure:"disk_caching_type" required:"false" cty:"disk_caching_type" hcl:"disk_caching_type"`
|
DiskCachingType *string `mapstructure:"disk_caching_type" required:"false" cty:"disk_caching_type" hcl:"disk_caching_type"`
|
||||||
@@ -189,7 +187,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||||||
"resource_group_name": &hcldec.AttrSpec{Name: "resource_group_name", Type: cty.String, Required: false},
|
"resource_group_name": &hcldec.AttrSpec{Name: "resource_group_name", Type: cty.String, Required: false},
|
||||||
"storage_account": &hcldec.AttrSpec{Name: "storage_account", Type: cty.String, Required: false},
|
"storage_account": &hcldec.AttrSpec{Name: "storage_account", Type: cty.String, Required: false},
|
||||||
"temp_compute_name": &hcldec.AttrSpec{Name: "temp_compute_name", Type: cty.String, Required: false},
|
"temp_compute_name": &hcldec.AttrSpec{Name: "temp_compute_name", Type: cty.String, Required: false},
|
||||||
"temp_nic_name": &hcldec.AttrSpec{Name: "temp_nic_name", Type: cty.String, Required: false},
|
|
||||||
"temp_resource_group_name": &hcldec.AttrSpec{Name: "temp_resource_group_name", Type: cty.String, Required: false},
|
"temp_resource_group_name": &hcldec.AttrSpec{Name: "temp_resource_group_name", Type: cty.String, Required: false},
|
||||||
"build_resource_group_name": &hcldec.AttrSpec{Name: "build_resource_group_name", Type: cty.String, Required: false},
|
"build_resource_group_name": &hcldec.AttrSpec{Name: "build_resource_group_name", Type: cty.String, Required: false},
|
||||||
"build_key_vault_name": &hcldec.AttrSpec{Name: "build_key_vault_name", Type: cty.String, Required: false},
|
"build_key_vault_name": &hcldec.AttrSpec{Name: "build_key_vault_name", Type: cty.String, Required: false},
|
||||||
@@ -202,7 +199,6 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec {
|
|||||||
"plan_info": &hcldec.BlockSpec{TypeName: "plan_info", Nested: hcldec.ObjectSpec((*FlatPlanInformation)(nil).HCL2Spec())},
|
"plan_info": &hcldec.BlockSpec{TypeName: "plan_info", Nested: hcldec.ObjectSpec((*FlatPlanInformation)(nil).HCL2Spec())},
|
||||||
"polling_duration_timeout": &hcldec.AttrSpec{Name: "polling_duration_timeout", Type: cty.String, Required: false},
|
"polling_duration_timeout": &hcldec.AttrSpec{Name: "polling_duration_timeout", Type: cty.String, Required: false},
|
||||||
"os_type": &hcldec.AttrSpec{Name: "os_type", Type: cty.String, Required: false},
|
"os_type": &hcldec.AttrSpec{Name: "os_type", Type: cty.String, Required: false},
|
||||||
"temp_os_disk_name": &hcldec.AttrSpec{Name: "temp_os_disk_name", Type: cty.String, Required: false},
|
|
||||||
"os_disk_size_gb": &hcldec.AttrSpec{Name: "os_disk_size_gb", Type: cty.Number, Required: false},
|
"os_disk_size_gb": &hcldec.AttrSpec{Name: "os_disk_size_gb", Type: cty.Number, Required: false},
|
||||||
"disk_additional_size": &hcldec.AttrSpec{Name: "disk_additional_size", Type: cty.List(cty.Number), Required: false},
|
"disk_additional_size": &hcldec.AttrSpec{Name: "disk_additional_size", Type: cty.List(cty.Number), Required: false},
|
||||||
"disk_caching_type": &hcldec.AttrSpec{Name: "disk_caching_type", Type: cty.String, Required: false},
|
"disk_caching_type": &hcldec.AttrSpec{Name: "disk_caching_type", Type: cty.String, Required: false},
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,135 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
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": ""
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`
|
||||||
@@ -0,0 +1,416 @@
|
|||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
//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
|
||||||
|
}
|
||||||
@@ -0,0 +1,179 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,90 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,130 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,209 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package hcloud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/hetznercloud/hcloud-go/hcloud"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Artifact struct {
|
||||||
|
// The name of the snapshot
|
||||||
|
snapshotName string
|
||||||
|
|
||||||
|
// The ID of the image
|
||||||
|
snapshotId int
|
||||||
|
|
||||||
|
// The hcloudClient for making API calls
|
||||||
|
hcloudClient *hcloud.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 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) Id() string {
|
||||||
|
return strconv.Itoa(a.snapshotId)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) String() string {
|
||||||
|
return fmt.Sprintf("A snapshot was created: '%v' (ID: %v)", a.snapshotName, a.snapshotId)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.hcloudClient.Image.Delete(context.TODO(), &hcloud.Image{ID: a.snapshotId})
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package hcloud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestArtifact_Impl(t *testing.T) {
|
||||||
|
var _ packersdk.Artifact = (*Artifact)(nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArtifactId(t *testing.T) {
|
||||||
|
generatedData := make(map[string]interface{})
|
||||||
|
a := &Artifact{"packer-foobar", 42, nil, generatedData}
|
||||||
|
expected := "42"
|
||||||
|
|
||||||
|
if a.Id() != expected {
|
||||||
|
t.Fatalf("artifact ID should match: %v", expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestArtifactString(t *testing.T) {
|
||||||
|
generatedData := make(map[string]interface{})
|
||||||
|
a := &Artifact{"packer-foobar", 42, nil, generatedData}
|
||||||
|
expected := "A snapshot was created: 'packer-foobar' (ID: 42)"
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package hcloud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
"github.com/hetznercloud/hcloud-go/hcloud"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The unique id for the builder
|
||||||
|
const BuilderId = "hcloud.builder"
|
||||||
|
|
||||||
|
type Builder struct {
|
||||||
|
config Config
|
||||||
|
runner multistep.Runner
|
||||||
|
hcloudClient *hcloud.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
var pluginVersion = "1.0.0"
|
||||||
|
|
||||||
|
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) {
|
||||||
|
opts := []hcloud.ClientOption{
|
||||||
|
hcloud.WithToken(b.config.HCloudToken),
|
||||||
|
hcloud.WithEndpoint(b.config.Endpoint),
|
||||||
|
hcloud.WithPollInterval(b.config.PollInterval),
|
||||||
|
hcloud.WithApplication("hcloud-packer", pluginVersion),
|
||||||
|
}
|
||||||
|
b.hcloudClient = hcloud.NewClient(opts...)
|
||||||
|
// Set up the state
|
||||||
|
state := new(multistep.BasicStateBag)
|
||||||
|
state.Put("config", &b.config)
|
||||||
|
state.Put("hcloudClient", b.hcloudClient)
|
||||||
|
state.Put("hook", hook)
|
||||||
|
state.Put("ui", ui)
|
||||||
|
|
||||||
|
// Build the steps
|
||||||
|
steps := []multistep.Step{
|
||||||
|
&stepCreateSSHKey{
|
||||||
|
Debug: b.config.PackerDebug,
|
||||||
|
DebugKeyPath: fmt.Sprintf("ssh_key_%s.pem", b.config.PackerBuildName),
|
||||||
|
},
|
||||||
|
&stepCreateServer{},
|
||||||
|
&communicator.StepConnect{
|
||||||
|
Config: &b.config.Comm,
|
||||||
|
Host: getServerIP,
|
||||||
|
SSHConfig: b.config.Comm.SSHConfigFunc(),
|
||||||
|
},
|
||||||
|
&commonsteps.StepProvision{},
|
||||||
|
&commonsteps.StepCleanupTempKeys{
|
||||||
|
Comm: &b.config.Comm,
|
||||||
|
},
|
||||||
|
&stepShutdownServer{},
|
||||||
|
&stepCreateSnapshot{},
|
||||||
|
}
|
||||||
|
// 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 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
artifact := &Artifact{
|
||||||
|
snapshotName: state.Get("snapshot_name").(string),
|
||||||
|
snapshotId: state.Get("snapshot_id").(int),
|
||||||
|
hcloudClient: b.hcloudClient,
|
||||||
|
StateData: map[string]interface{}{"generated_data": state.Get("generated_data")},
|
||||||
|
}
|
||||||
|
|
||||||
|
return artifact, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
package hcloud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
builderT "github.com/hashicorp/packer/acctest"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuilderAcc_basic(t *testing.T) {
|
||||||
|
builderT.Test(t, builderT.TestCase{
|
||||||
|
PreCheck: func() { testAccPreCheck(t) },
|
||||||
|
Builder: &Builder{},
|
||||||
|
Template: testBuilderAccBasic,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func testAccPreCheck(t *testing.T) {
|
||||||
|
if v := os.Getenv("HCLOUD_TOKEN"); v == "" {
|
||||||
|
t.Fatal("HCLOUD_TOKEN must be set for acceptance tests")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const testBuilderAccBasic = `
|
||||||
|
{
|
||||||
|
"builders": [{
|
||||||
|
"type": "test",
|
||||||
|
"location": "nbg1",
|
||||||
|
"server_type": "cx11",
|
||||||
|
"image": "ubuntu-18.04",
|
||||||
|
"user_data": "",
|
||||||
|
"user_data_file": ""
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
`
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
//go:generate packer-sdc mapstructure-to-hcl2 -type Config,imageFilter
|
||||||
|
|
||||||
|
package hcloud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/common"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/communicator"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
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/hetznercloud/hcloud-go/hcloud"
|
||||||
|
"github.com/mitchellh/mapstructure"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
common.PackerConfig `mapstructure:",squash"`
|
||||||
|
Comm communicator.Config `mapstructure:",squash"`
|
||||||
|
|
||||||
|
HCloudToken string `mapstructure:"token"`
|
||||||
|
Endpoint string `mapstructure:"endpoint"`
|
||||||
|
|
||||||
|
PollInterval time.Duration `mapstructure:"poll_interval"`
|
||||||
|
|
||||||
|
ServerName string `mapstructure:"server_name"`
|
||||||
|
Location string `mapstructure:"location"`
|
||||||
|
ServerType string `mapstructure:"server_type"`
|
||||||
|
Image string `mapstructure:"image"`
|
||||||
|
ImageFilter *imageFilter `mapstructure:"image_filter"`
|
||||||
|
|
||||||
|
SnapshotName string `mapstructure:"snapshot_name"`
|
||||||
|
SnapshotLabels map[string]string `mapstructure:"snapshot_labels"`
|
||||||
|
UserData string `mapstructure:"user_data"`
|
||||||
|
UserDataFile string `mapstructure:"user_data_file"`
|
||||||
|
SSHKeys []string `mapstructure:"ssh_keys"`
|
||||||
|
|
||||||
|
RescueMode string `mapstructure:"rescue"`
|
||||||
|
|
||||||
|
ctx interpolate.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
type imageFilter struct {
|
||||||
|
WithSelector []string `mapstructure:"with_selector"`
|
||||||
|
MostRecent bool `mapstructure:"most_recent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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.HCloudToken == "" {
|
||||||
|
c.HCloudToken = os.Getenv("HCLOUD_TOKEN")
|
||||||
|
}
|
||||||
|
if c.Endpoint == "" {
|
||||||
|
if os.Getenv("HCLOUD_ENDPOINT") != "" {
|
||||||
|
c.Endpoint = os.Getenv("HCLOUD_ENDPOINT")
|
||||||
|
} else {
|
||||||
|
c.Endpoint = hcloud.Endpoint
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if c.PollInterval == 0 {
|
||||||
|
c.PollInterval = 500 * time.Millisecond
|
||||||
|
}
|
||||||
|
|
||||||
|
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.ServerName == "" {
|
||||||
|
// Default to packer-[time-ordered-uuid]
|
||||||
|
c.ServerName = fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
|
||||||
|
}
|
||||||
|
|
||||||
|
var errs *packersdk.MultiError
|
||||||
|
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, es...)
|
||||||
|
}
|
||||||
|
if c.HCloudToken == "" {
|
||||||
|
// Required configurations that will display errors if not set
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("token for auth must be specified"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Location == "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("location is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ServerType == "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("server type is required"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Image == "" && c.ImageFilter == nil {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("image or image_filter is required"))
|
||||||
|
}
|
||||||
|
if c.ImageFilter != nil {
|
||||||
|
if len(c.ImageFilter.WithSelector) == 0 {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("image_filter.with_selector is required when specifying filter"))
|
||||||
|
} else if c.Image != "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("only one of image or image_filter can be specified"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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 errs != nil && len(errs.Errors) > 0 {
|
||||||
|
return nil, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
packersdk.LogSecretFilter.Set(c.HCloudToken)
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServerIP(state multistep.StateBag) (string, error) {
|
||||||
|
return state.Get("server_ip").(string), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,196 @@
|
|||||||
|
// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package hcloud
|
||||||
|
|
||||||
|
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"`
|
||||||
|
HCloudToken *string `mapstructure:"token" cty:"token" hcl:"token"`
|
||||||
|
Endpoint *string `mapstructure:"endpoint" cty:"endpoint" hcl:"endpoint"`
|
||||||
|
PollInterval *string `mapstructure:"poll_interval" cty:"poll_interval" hcl:"poll_interval"`
|
||||||
|
ServerName *string `mapstructure:"server_name" cty:"server_name" hcl:"server_name"`
|
||||||
|
Location *string `mapstructure:"location" cty:"location" hcl:"location"`
|
||||||
|
ServerType *string `mapstructure:"server_type" cty:"server_type" hcl:"server_type"`
|
||||||
|
Image *string `mapstructure:"image" cty:"image" hcl:"image"`
|
||||||
|
ImageFilter *FlatimageFilter `mapstructure:"image_filter" cty:"image_filter" hcl:"image_filter"`
|
||||||
|
SnapshotName *string `mapstructure:"snapshot_name" cty:"snapshot_name" hcl:"snapshot_name"`
|
||||||
|
SnapshotLabels map[string]string `mapstructure:"snapshot_labels" cty:"snapshot_labels" hcl:"snapshot_labels"`
|
||||||
|
UserData *string `mapstructure:"user_data" cty:"user_data" hcl:"user_data"`
|
||||||
|
UserDataFile *string `mapstructure:"user_data_file" cty:"user_data_file" hcl:"user_data_file"`
|
||||||
|
SSHKeys []string `mapstructure:"ssh_keys" cty:"ssh_keys" hcl:"ssh_keys"`
|
||||||
|
RescueMode *string `mapstructure:"rescue" cty:"rescue" hcl:"rescue"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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},
|
||||||
|
"token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false},
|
||||||
|
"endpoint": &hcldec.AttrSpec{Name: "endpoint", Type: cty.String, Required: false},
|
||||||
|
"poll_interval": &hcldec.AttrSpec{Name: "poll_interval", Type: cty.String, Required: false},
|
||||||
|
"server_name": &hcldec.AttrSpec{Name: "server_name", Type: cty.String, Required: false},
|
||||||
|
"location": &hcldec.AttrSpec{Name: "location", Type: cty.String, Required: false},
|
||||||
|
"server_type": &hcldec.AttrSpec{Name: "server_type", Type: cty.String, Required: false},
|
||||||
|
"image": &hcldec.AttrSpec{Name: "image", Type: cty.String, Required: false},
|
||||||
|
"image_filter": &hcldec.BlockSpec{TypeName: "image_filter", Nested: hcldec.ObjectSpec((*FlatimageFilter)(nil).HCL2Spec())},
|
||||||
|
"snapshot_name": &hcldec.AttrSpec{Name: "snapshot_name", Type: cty.String, Required: false},
|
||||||
|
"snapshot_labels": &hcldec.AttrSpec{Name: "snapshot_labels", Type: cty.Map(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},
|
||||||
|
"ssh_keys": &hcldec.AttrSpec{Name: "ssh_keys", Type: cty.List(cty.String), Required: false},
|
||||||
|
"rescue": &hcldec.AttrSpec{Name: "rescue", Type: cty.String, Required: false},
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatimageFilter is an auto-generated flat version of imageFilter.
|
||||||
|
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
type FlatimageFilter struct {
|
||||||
|
WithSelector []string `mapstructure:"with_selector" cty:"with_selector" hcl:"with_selector"`
|
||||||
|
MostRecent *bool `mapstructure:"most_recent" cty:"most_recent" hcl:"most_recent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatMapstructure returns a new FlatimageFilter.
|
||||||
|
// FlatimageFilter is an auto-generated flat version of imageFilter.
|
||||||
|
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
func (*imageFilter) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
|
||||||
|
return new(FlatimageFilter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HCL2Spec returns the hcl spec of a imageFilter.
|
||||||
|
// This spec is used by HCL to read the fields of imageFilter.
|
||||||
|
// The decoded values from this spec will then be applied to a FlatimageFilter.
|
||||||
|
func (*FlatimageFilter) HCL2Spec() map[string]hcldec.Spec {
|
||||||
|
s := map[string]hcldec.Spec{
|
||||||
|
"with_selector": &hcldec.AttrSpec{Name: "with_selector", Type: cty.List(cty.String), Required: false},
|
||||||
|
"most_recent": &hcldec.AttrSpec{Name: "most_recent", Type: cty.Bool, Required: false},
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
package hcloud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"sort"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
"github.com/hetznercloud/hcloud-go/hcloud"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepCreateServer struct {
|
||||||
|
serverId int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreateServer) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
client := state.Get("hcloudClient").(*hcloud.Client)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
c := state.Get("config").(*Config)
|
||||||
|
sshKeyId := state.Get("ssh_key_id").(int)
|
||||||
|
|
||||||
|
// Create the server based on configuration
|
||||||
|
ui.Say("Creating server...")
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
sshKeys := []*hcloud.SSHKey{{ID: sshKeyId}}
|
||||||
|
for _, k := range c.SSHKeys {
|
||||||
|
sshKey, _, err := client.SSHKey.Get(ctx, k)
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", fmt.Errorf("Error fetching SSH key: %s", err))
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
if sshKey == nil {
|
||||||
|
state.Put("error", fmt.Errorf("Could not find key: %s", k))
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
sshKeys = append(sshKeys, sshKey)
|
||||||
|
}
|
||||||
|
|
||||||
|
var image *hcloud.Image
|
||||||
|
if c.Image != "" {
|
||||||
|
image = &hcloud.Image{Name: c.Image}
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
image, err = getImageWithSelectors(ctx, client, c)
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
ui.Message(fmt.Sprintf("Using image %s with ID %d", image.Description, image.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
serverCreateResult, _, err := client.Server.Create(ctx, hcloud.ServerCreateOpts{
|
||||||
|
Name: c.ServerName,
|
||||||
|
ServerType: &hcloud.ServerType{Name: c.ServerType},
|
||||||
|
Image: image,
|
||||||
|
SSHKeys: sshKeys,
|
||||||
|
Location: &hcloud.Location{Name: c.Location},
|
||||||
|
UserData: userData,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error creating server: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
state.Put("server_ip", serverCreateResult.Server.PublicNet.IPv4.IP.String())
|
||||||
|
// We use this in cleanup
|
||||||
|
s.serverId = serverCreateResult.Server.ID
|
||||||
|
|
||||||
|
// Store the server id for later
|
||||||
|
state.Put("server_id", serverCreateResult.Server.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", serverCreateResult.Server.ID)
|
||||||
|
|
||||||
|
if err := waitForAction(ctx, client, serverCreateResult.Action); err != nil {
|
||||||
|
err := fmt.Errorf("Error creating server: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
for _, nextAction := range serverCreateResult.NextActions {
|
||||||
|
if err := waitForAction(ctx, client, nextAction); err != nil {
|
||||||
|
err := fmt.Errorf("Error creating server: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.RescueMode != "" {
|
||||||
|
ui.Say("Enabling Rescue Mode...")
|
||||||
|
rootPassword, err := setRescue(ctx, client, serverCreateResult.Server, c.RescueMode, sshKeys)
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error enabling rescue mode: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
ui.Say("Reboot server...")
|
||||||
|
action, _, err := client.Server.Reset(ctx, serverCreateResult.Server)
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error rebooting server: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
if err := waitForAction(ctx, client, action); err != nil {
|
||||||
|
err := fmt.Errorf("Error rebooting server: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
if c.RescueMode == "freebsd64" {
|
||||||
|
// We will set this only on freebsd
|
||||||
|
ui.Say("Using Root Password instead of SSH Keys...")
|
||||||
|
c.Comm.SSHPassword = rootPassword
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreateServer) Cleanup(state multistep.StateBag) {
|
||||||
|
// If the serverID isn't there, we probably never created it
|
||||||
|
if s.serverId == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := state.Get("hcloudClient").(*hcloud.Client)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
|
||||||
|
// Destroy the server we just created
|
||||||
|
ui.Say("Destroying server...")
|
||||||
|
_, err := client.Server.Delete(context.TODO(), &hcloud.Server{ID: s.serverId})
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(fmt.Sprintf(
|
||||||
|
"Error destroying server. Please destroy it manually: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func setRescue(ctx context.Context, client *hcloud.Client, server *hcloud.Server, rescue string, sshKeys []*hcloud.SSHKey) (string, error) {
|
||||||
|
rescueChanged := false
|
||||||
|
if server.RescueEnabled {
|
||||||
|
rescueChanged = true
|
||||||
|
action, _, err := client.Server.DisableRescue(ctx, server)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := waitForAction(ctx, client, action); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if rescue != "" {
|
||||||
|
rescueChanged = true
|
||||||
|
if rescue == "freebsd64" {
|
||||||
|
sshKeys = nil // freebsd64 doesn't allow ssh keys so we will remove them here
|
||||||
|
}
|
||||||
|
res, _, err := client.Server.EnableRescue(ctx, server, hcloud.ServerEnableRescueOpts{
|
||||||
|
Type: hcloud.ServerRescueType(rescue),
|
||||||
|
SSHKeys: sshKeys,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := waitForAction(ctx, client, res.Action); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return res.RootPassword, nil
|
||||||
|
}
|
||||||
|
if rescueChanged {
|
||||||
|
action, _, err := client.Server.Reset(ctx, server)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if err := waitForAction(ctx, client, action); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func waitForAction(ctx context.Context, client *hcloud.Client, action *hcloud.Action) error {
|
||||||
|
_, errCh := client.Action.WatchProgress(ctx, action)
|
||||||
|
if err := <-errCh; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getImageWithSelectors(ctx context.Context, client *hcloud.Client, c *Config) (*hcloud.Image, error) {
|
||||||
|
var allImages []*hcloud.Image
|
||||||
|
|
||||||
|
var selector = strings.Join(c.ImageFilter.WithSelector, ",")
|
||||||
|
opts := hcloud.ImageListOpts{
|
||||||
|
ListOpts: hcloud.ListOpts{LabelSelector: selector},
|
||||||
|
Status: []hcloud.ImageStatus{hcloud.ImageStatusAvailable},
|
||||||
|
}
|
||||||
|
|
||||||
|
allImages, err := client.Image.AllWithOpts(ctx, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(allImages) == 0 {
|
||||||
|
return nil, fmt.Errorf("no image found for selector %q", selector)
|
||||||
|
}
|
||||||
|
if len(allImages) > 1 {
|
||||||
|
if !c.ImageFilter.MostRecent {
|
||||||
|
return nil, fmt.Errorf("more than one image found for selector %q", selector)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(allImages, func(i, j int) bool {
|
||||||
|
return allImages[i].Created.After(allImages[j].Created)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return allImages[0], nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package hcloud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
"github.com/hetznercloud/hcloud-go/hcloud"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepCreateSnapshot struct{}
|
||||||
|
|
||||||
|
func (s *stepCreateSnapshot) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
client := state.Get("hcloudClient").(*hcloud.Client)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
c := state.Get("config").(*Config)
|
||||||
|
serverID := state.Get("server_id").(int)
|
||||||
|
|
||||||
|
ui.Say("Creating snapshot ...")
|
||||||
|
ui.Say("This can take some time")
|
||||||
|
result, _, err := client.Server.CreateImage(ctx, &hcloud.Server{ID: serverID}, &hcloud.ServerCreateImageOpts{
|
||||||
|
Type: hcloud.ImageTypeSnapshot,
|
||||||
|
Labels: c.SnapshotLabels,
|
||||||
|
Description: hcloud.String(c.SnapshotName),
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error creating snapshot: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
state.Put("snapshot_id", result.Image.ID)
|
||||||
|
state.Put("snapshot_name", c.SnapshotName)
|
||||||
|
_, errCh := client.Action.WatchProgress(ctx, result.Action)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err1 := <-errCh:
|
||||||
|
if err1 == nil {
|
||||||
|
return multistep.ActionContinue
|
||||||
|
} else {
|
||||||
|
err := fmt.Errorf("Error creating snapshot: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreateSnapshot) Cleanup(state multistep.StateBag) {
|
||||||
|
// no cleanup
|
||||||
|
}
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
package hcloud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/uuid"
|
||||||
|
"github.com/hetznercloud/hcloud-go/hcloud"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepCreateSSHKey struct {
|
||||||
|
Debug bool
|
||||||
|
DebugKeyPath string
|
||||||
|
|
||||||
|
keyId int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreateSSHKey) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
client := state.Get("hcloudClient").(*hcloud.Client)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
c := state.Get("config").(*Config)
|
||||||
|
ui.Say("Creating temporary ssh key for server...")
|
||||||
|
|
||||||
|
priv, err := rsa.GenerateKey(rand.Reader, 2014)
|
||||||
|
if err != nil {
|
||||||
|
state.Put("error", fmt.Errorf("Error generating RSA key: %s", err))
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
// ASN.1 DER encoded form
|
||||||
|
privDER := x509.MarshalPKCS1PrivateKey(priv)
|
||||||
|
privBLK := pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Headers: nil,
|
||||||
|
Bytes: privDER,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the private key in the config for later
|
||||||
|
c.Comm.SSHPrivateKey = pem.EncodeToMemory(&privBLK)
|
||||||
|
|
||||||
|
// Marshal the public key into SSH compatible format
|
||||||
|
pub, err := ssh.NewPublicKey(&priv.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
state.Put("error", fmt.Errorf("Error generating public key: %s", err))
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
pubSSHFormat := string(ssh.MarshalAuthorizedKey(pub))
|
||||||
|
|
||||||
|
// The name of the public key on the Hetzner Cloud
|
||||||
|
name := fmt.Sprintf("packer-%s", uuid.TimeOrderedUUID())
|
||||||
|
|
||||||
|
// Create the key!
|
||||||
|
key, _, err := client.SSHKey.Create(ctx, hcloud.SSHKeyCreateOpts{
|
||||||
|
Name: name,
|
||||||
|
PublicKey: pubSSHFormat,
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
|
||||||
|
// If we're in debug mode, output the private key to the working directory.
|
||||||
|
if s.Debug {
|
||||||
|
ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath))
|
||||||
|
f, err := os.Create(s.DebugKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// Write the key out
|
||||||
|
if _, err := f.Write(pem.EncodeToMemory(&privBLK)); err != nil {
|
||||||
|
state.Put("error", fmt.Errorf("Error saving debug key: %s", err))
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chmod it so that it is SSH ready
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
if err := f.Chmod(0600); err != nil {
|
||||||
|
state.Put("error", fmt.Errorf("Error setting permissions of debug key: %s", err))
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreateSSHKey) Cleanup(state multistep.StateBag) {
|
||||||
|
// If no key id is set, then we never created it, so just return
|
||||||
|
if s.keyId == 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := state.Get("hcloudClient").(*hcloud.Client)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
|
||||||
|
ui.Say("Deleting temporary ssh key...")
|
||||||
|
_, err := client.SSHKey.Delete(context.TODO(), &hcloud.SSHKey{ID: 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package hcloud
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
"github.com/hetznercloud/hcloud-go/hcloud"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepShutdownServer struct{}
|
||||||
|
|
||||||
|
func (s *stepShutdownServer) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
client := state.Get("hcloudClient").(*hcloud.Client)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
serverID := state.Get("server_id").(int)
|
||||||
|
|
||||||
|
ui.Say("Shutting down server...")
|
||||||
|
|
||||||
|
action, _, err := client.Server.Shutdown(ctx, &hcloud.Server{ID: serverID})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error stopping server: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
_, errCh := client.Action.WatchProgress(ctx, action)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case err1 := <-errCh:
|
||||||
|
if err1 == nil {
|
||||||
|
return multistep.ActionContinue
|
||||||
|
} else {
|
||||||
|
err := fmt.Errorf("Error stopping server: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepShutdownServer) Cleanup(state multistep.StateBag) {
|
||||||
|
// no cleanup
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/version"
|
||||||
|
packerVersion "github.com/hashicorp/packer/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
var HcloudPluginVersion *version.PluginVersion
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
HcloudPluginVersion = version.InitializePluginVersion(
|
||||||
|
packerVersion.Version, packerVersion.VersionPrerelease)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package lxc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Artifact struct {
|
||||||
|
dir string
|
||||||
|
f []string
|
||||||
|
// StateData should store data such as GeneratedData
|
||||||
|
// to be shared with post-processors
|
||||||
|
StateData map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Artifact) BuilderId() string {
|
||||||
|
return BuilderId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) Files() []string {
|
||||||
|
return a.f
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Artifact) Id() string {
|
||||||
|
return "VM"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) String() string {
|
||||||
|
return fmt.Sprintf("VM files in directory: %s", a.dir)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) State(name string) interface{} {
|
||||||
|
return a.StateData[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) Destroy() error {
|
||||||
|
return os.RemoveAll(a.dir)
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package lxc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/hashicorp/hcl/v2/hcldec"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The unique ID for this builder
|
||||||
|
const BuilderId = "ustream.lxc"
|
||||||
|
|
||||||
|
type wrappedCommandTemplate struct {
|
||||||
|
Command string
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
errs := b.config.Prepare(raws...)
|
||||||
|
if errs != nil {
|
||||||
|
return nil, nil, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) (packersdk.Artifact, error) {
|
||||||
|
wrappedCommand := func(command string) (string, error) {
|
||||||
|
b.config.ctx.Data = &wrappedCommandTemplate{Command: command}
|
||||||
|
return interpolate.Render(b.config.CommandWrapper, &b.config.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
steps := []multistep.Step{
|
||||||
|
new(stepPrepareOutputDir),
|
||||||
|
new(stepLxcCreate),
|
||||||
|
&StepWaitInit{
|
||||||
|
WaitTimeout: b.config.InitTimeout,
|
||||||
|
},
|
||||||
|
new(StepProvision),
|
||||||
|
new(stepExport),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the state bag
|
||||||
|
state := new(multistep.BasicStateBag)
|
||||||
|
state.Put("config", &b.config)
|
||||||
|
state.Put("hook", hook)
|
||||||
|
state.Put("ui", ui)
|
||||||
|
state.Put("wrappedCommand", CommandWrapper(wrappedCommand))
|
||||||
|
|
||||||
|
// Run
|
||||||
|
b.runner = commonsteps.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
|
||||||
|
b.runner.Run(ctx, state)
|
||||||
|
|
||||||
|
// If there was an error, return that
|
||||||
|
if rawErr, ok := state.GetOk("error"); ok {
|
||||||
|
return nil, rawErr.(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compile the artifact list
|
||||||
|
files := make([]string, 0, 5)
|
||||||
|
visit := func(path string, info os.FileInfo, err error) error {
|
||||||
|
if !info.IsDir() {
|
||||||
|
files = append(files, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := filepath.Walk(b.config.OutputDir, visit); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
artifact := &Artifact{
|
||||||
|
dir: b.config.OutputDir,
|
||||||
|
f: files,
|
||||||
|
StateData: map[string]interface{}{"generated_data": state.Get("generated_data")},
|
||||||
|
}
|
||||||
|
|
||||||
|
return artifact, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package lxc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testConfig() map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"config_file": "builder_test.go",
|
||||||
|
"template_name": "debian",
|
||||||
|
"template_environment_vars": "SUITE=jessie",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilder_Foo(t *testing.T) {
|
||||||
|
if os.Getenv("PACKER_ACC") == "" {
|
||||||
|
t.Skip("This test is only run with PACKER_ACC=1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_ConfigFile(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
// Good
|
||||||
|
config := testConfig()
|
||||||
|
_, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad, missing config file
|
||||||
|
config = testConfig()
|
||||||
|
delete(config, "config_file")
|
||||||
|
b = Builder{}
|
||||||
|
_, warnings, err = b.Prepare(config)
|
||||||
|
if len(warnings) > 0 {
|
||||||
|
t.Fatalf("bad: %#v", warnings)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilder_ImplementsBuilder(t *testing.T) {
|
||||||
|
var raw interface{}
|
||||||
|
raw = &Builder{}
|
||||||
|
if _, ok := raw.(packersdk.Builder); !ok {
|
||||||
|
t.Fatalf("Builder should be a builder")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package lxc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommandWrapper is a type that given a command, will possibly modify that
|
||||||
|
// command in-flight. This might return an error.
|
||||||
|
type CommandWrapper func(string) (string, error)
|
||||||
|
|
||||||
|
// ShellCommand takes a command string and returns an *exec.Cmd to execute
|
||||||
|
// it within the context of a shell (/bin/sh).
|
||||||
|
func ShellCommand(command string) *exec.Cmd {
|
||||||
|
return exec.Command("/bin/sh", "-c", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunCommand(args ...string) error {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
|
||||||
|
log.Printf("Executing args: %#v", args)
|
||||||
|
cmd := exec.Command(args[0], args[1:]...)
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
err := cmd.Run()
|
||||||
|
|
||||||
|
stdoutString := strings.TrimSpace(stdout.String())
|
||||||
|
stderrString := strings.TrimSpace(stderr.String())
|
||||||
|
|
||||||
|
if _, ok := err.(*exec.ExitError); ok {
|
||||||
|
err = fmt.Errorf("Command error: %s", stderrString)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("stdout: %s", stdoutString)
|
||||||
|
log.Printf("stderr: %s", stderrString)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package lxc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/tmp"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LxcAttachCommunicator struct {
|
||||||
|
RootFs string
|
||||||
|
ContainerName string
|
||||||
|
AttachOptions []string
|
||||||
|
CmdWrapper CommandWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LxcAttachCommunicator) Start(ctx context.Context, cmd *packersdk.RemoteCmd) error {
|
||||||
|
localCmd, err := c.Execute(cmd.Command)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
localCmd.Stdin = cmd.Stdin
|
||||||
|
localCmd.Stdout = cmd.Stdout
|
||||||
|
localCmd.Stderr = cmd.Stderr
|
||||||
|
if err := localCmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
exitStatus := 0
|
||||||
|
if err := localCmd.Wait(); err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
exitStatus = 1
|
||||||
|
|
||||||
|
// There is no process-independent way to get the REAL
|
||||||
|
// exit status so we just try to go deeper.
|
||||||
|
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
|
||||||
|
exitStatus = status.ExitStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf(
|
||||||
|
"lxc-attach execution exited with '%d': '%s'",
|
||||||
|
exitStatus, cmd.Command)
|
||||||
|
cmd.SetExited(exitStatus)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LxcAttachCommunicator) Upload(dst string, r io.Reader, fi *os.FileInfo) error {
|
||||||
|
log.Printf("Uploading to rootfs: %s", dst)
|
||||||
|
tf, err := tmp.File("packer-lxc-attach")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error uploading file to rootfs: %s", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tf.Name())
|
||||||
|
io.Copy(tf, r)
|
||||||
|
|
||||||
|
attachCommand := []string{"cat", "%s", " | ", "lxc-attach"}
|
||||||
|
attachCommand = append(attachCommand, c.AttachOptions...)
|
||||||
|
attachCommand = append(attachCommand, []string{"--name", "%s", "--", "/bin/sh -c \"/bin/cat > %s\""}...)
|
||||||
|
|
||||||
|
cpCmd, err := c.CmdWrapper(fmt.Sprintf(strings.Join(attachCommand, " "), tf.Name(), c.ContainerName, dst))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if fi != nil {
|
||||||
|
tfDir := filepath.Dir(tf.Name())
|
||||||
|
// rename tempfile to match original file name. This makes sure that if file is being
|
||||||
|
// moved into a directory, the filename is preserved instead of a temp name.
|
||||||
|
adjustedTempName := filepath.Join(tfDir, (*fi).Name())
|
||||||
|
mvCmd, err := c.CmdWrapper(fmt.Sprintf("mv %s %s", tf.Name(), adjustedTempName))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer os.Remove(adjustedTempName)
|
||||||
|
ShellCommand(mvCmd).Run()
|
||||||
|
// change cpCmd to use new file name as source
|
||||||
|
cpCmd, err = c.CmdWrapper(fmt.Sprintf(strings.Join(attachCommand, " "), adjustedTempName, c.ContainerName, dst))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Running copy command: %s", dst)
|
||||||
|
|
||||||
|
return ShellCommand(cpCmd).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LxcAttachCommunicator) UploadDir(dst string, src string, exclude []string) error {
|
||||||
|
// TODO: remove any file copied if it appears in `exclude`
|
||||||
|
dest := filepath.Join(c.RootFs, dst)
|
||||||
|
log.Printf("Uploading directory '%s' to rootfs '%s'", src, dest)
|
||||||
|
cpCmd, err := c.CmdWrapper(fmt.Sprintf("cp -R %s/. %s", src, dest))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ShellCommand(cpCmd).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LxcAttachCommunicator) Download(src string, w io.Writer) error {
|
||||||
|
src = filepath.Join(c.RootFs, src)
|
||||||
|
log.Printf("Downloading from rootfs dir: %s", src)
|
||||||
|
f, err := os.Open(src)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
if _, err := io.Copy(w, f); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LxcAttachCommunicator) DownloadDir(src string, dst string, exclude []string) error {
|
||||||
|
return fmt.Errorf("DownloadDir is not implemented for lxc")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LxcAttachCommunicator) Execute(commandString string) (*exec.Cmd, error) {
|
||||||
|
log.Printf("Executing with lxc-attach in container: %s %s %s", c.ContainerName, c.RootFs, commandString)
|
||||||
|
|
||||||
|
attachCommand := []string{"lxc-attach"}
|
||||||
|
attachCommand = append(attachCommand, c.AttachOptions...)
|
||||||
|
attachCommand = append(attachCommand, []string{"--name", "%s", "--", "/bin/sh -c \"%s\""}...)
|
||||||
|
|
||||||
|
command, err := c.CmdWrapper(
|
||||||
|
fmt.Sprintf(strings.Join(attachCommand, " "), c.ContainerName, commandString))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localCmd := ShellCommand(command)
|
||||||
|
log.Printf("Executing lxc-attach: %s %#v", localCmd.Path, localCmd.Args)
|
||||||
|
|
||||||
|
return localCmd, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LxcAttachCommunicator) CheckInit() (string, error) {
|
||||||
|
log.Printf("Debug runlevel exec")
|
||||||
|
localCmd, err := c.Execute("/sbin/runlevel")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
pr, _ := localCmd.StdoutPipe()
|
||||||
|
if err = localCmd.Start(); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
output, err := ioutil.ReadAll(pr)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = localCmd.Wait()
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return strings.TrimSpace(string(output)), nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package lxc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommunicator_ImplementsCommunicator(t *testing.T) {
|
||||||
|
var raw interface{}
|
||||||
|
raw = &LxcAttachCommunicator{}
|
||||||
|
if _, ok := raw.(packersdk.Communicator); !ok {
|
||||||
|
t.Fatalf("Communicator should be a communicator")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,114 @@
|
|||||||
|
//go:generate packer-sdc struct-markdown
|
||||||
|
//go:generate packer-sdc mapstructure-to-hcl2 -type Config
|
||||||
|
|
||||||
|
package lxc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"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/mitchellh/mapstructure"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
common.PackerConfig `mapstructure:",squash"`
|
||||||
|
// The path to the lxc configuration file.
|
||||||
|
ConfigFile string `mapstructure:"config_file" required:"true"`
|
||||||
|
// The directory in which to save the exported
|
||||||
|
// tar.gz. Defaults to `output-<BuildName>` in the current directory.
|
||||||
|
OutputDir string `mapstructure:"output_directory" required:"false"`
|
||||||
|
// The name of the LXC container. Usually stored
|
||||||
|
// in `/var/lib/lxc/containers/<container_name>`. Defaults to
|
||||||
|
// `packer-<BuildName>`.
|
||||||
|
ContainerName string `mapstructure:"container_name" required:"false"`
|
||||||
|
// Allows you to specify a wrapper command, such
|
||||||
|
// as ssh so you can execute packer builds on a remote host. Defaults to
|
||||||
|
// `{{.Command}}`; i.e. no wrapper.
|
||||||
|
CommandWrapper string `mapstructure:"command_wrapper" required:"false"`
|
||||||
|
// The timeout in seconds to wait for the the
|
||||||
|
// container to start. Defaults to 20 seconds.
|
||||||
|
InitTimeout time.Duration `mapstructure:"init_timeout" required:"false"`
|
||||||
|
|
||||||
|
// Options to pass to lxc-create. For
|
||||||
|
// instance, you can specify a custom LXC container configuration file with
|
||||||
|
// ["-f", "/path/to/lxc.conf"]. Defaults to []. See man 1 lxc-create for
|
||||||
|
// available options.
|
||||||
|
CreateOptions []string `mapstructure:"create_options" required:"false"`
|
||||||
|
// Options to pass to lxc-start. For
|
||||||
|
// instance, you can override parameters from the LXC container configuration
|
||||||
|
// file via ["--define", "KEY=VALUE"]. Defaults to []. See
|
||||||
|
// man 1 lxc-start for available options.
|
||||||
|
StartOptions []string `mapstructure:"start_options" required:"false"`
|
||||||
|
// Options to pass to lxc-attach. For
|
||||||
|
// instance, you can prevent the container from inheriting the host machine's
|
||||||
|
// environment by specifying ["--clear-env"]. Defaults to []. See
|
||||||
|
// man 1 lxc-attach for available options.
|
||||||
|
AttachOptions []string `mapstructure:"attach_options" required:"false"`
|
||||||
|
// The LXC template name to use.
|
||||||
|
Name string `mapstructure:"template_name" required:"true"`
|
||||||
|
// Options to pass to the given
|
||||||
|
// lxc-template command, usually located in
|
||||||
|
// `/usr/share/lxc/templates/lxc-<template_name>`. Note: This gets passed as
|
||||||
|
// ARGV to the template command. Ensure you have an array of strings, as a
|
||||||
|
// single string with spaces probably won't work. Defaults to [].
|
||||||
|
Parameters []string `mapstructure:"template_parameters" required:"false"`
|
||||||
|
// Environmental variables to
|
||||||
|
// use to build the template with.
|
||||||
|
EnvVars []string `mapstructure:"template_environment_vars" required:"true"`
|
||||||
|
// The minimum run level to wait for the
|
||||||
|
// container to reach. Note some distributions (Ubuntu) simulate run levels
|
||||||
|
// and may report 5 rather than 3.
|
||||||
|
TargetRunlevel int `mapstructure:"target_runlevel" required:"false"`
|
||||||
|
|
||||||
|
ctx interpolate.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Prepare(raws ...interface{}) error {
|
||||||
|
|
||||||
|
var md mapstructure.Metadata
|
||||||
|
err := config.Decode(c, &config.DecodeOpts{
|
||||||
|
Metadata: &md,
|
||||||
|
Interpolate: true,
|
||||||
|
}, raws...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accumulate any errors
|
||||||
|
var errs *packersdk.MultiError
|
||||||
|
|
||||||
|
if c.OutputDir == "" {
|
||||||
|
c.OutputDir = fmt.Sprintf("output-%s", c.PackerBuildName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ContainerName == "" {
|
||||||
|
c.ContainerName = fmt.Sprintf("packer-%s", c.PackerBuildName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.TargetRunlevel == 0 {
|
||||||
|
c.TargetRunlevel = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.CommandWrapper == "" {
|
||||||
|
c.CommandWrapper = "{{.Command}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.InitTimeout == 0 {
|
||||||
|
c.InitTimeout = 20 * time.Second
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := os.Stat(c.ConfigFile); os.IsNotExist(err) {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("LXC Config file appears to be missing: %s", c.ConfigFile))
|
||||||
|
}
|
||||||
|
|
||||||
|
if errs != nil && len(errs.Errors) > 0 {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package lxc
|
||||||
|
|
||||||
|
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"`
|
||||||
|
ConfigFile *string `mapstructure:"config_file" required:"true" cty:"config_file" hcl:"config_file"`
|
||||||
|
OutputDir *string `mapstructure:"output_directory" required:"false" cty:"output_directory" hcl:"output_directory"`
|
||||||
|
ContainerName *string `mapstructure:"container_name" required:"false" cty:"container_name" hcl:"container_name"`
|
||||||
|
CommandWrapper *string `mapstructure:"command_wrapper" required:"false" cty:"command_wrapper" hcl:"command_wrapper"`
|
||||||
|
InitTimeout *string `mapstructure:"init_timeout" required:"false" cty:"init_timeout" hcl:"init_timeout"`
|
||||||
|
CreateOptions []string `mapstructure:"create_options" required:"false" cty:"create_options" hcl:"create_options"`
|
||||||
|
StartOptions []string `mapstructure:"start_options" required:"false" cty:"start_options" hcl:"start_options"`
|
||||||
|
AttachOptions []string `mapstructure:"attach_options" required:"false" cty:"attach_options" hcl:"attach_options"`
|
||||||
|
Name *string `mapstructure:"template_name" required:"true" cty:"template_name" hcl:"template_name"`
|
||||||
|
Parameters []string `mapstructure:"template_parameters" required:"false" cty:"template_parameters" hcl:"template_parameters"`
|
||||||
|
EnvVars []string `mapstructure:"template_environment_vars" required:"true" cty:"template_environment_vars" hcl:"template_environment_vars"`
|
||||||
|
TargetRunlevel *int `mapstructure:"target_runlevel" required:"false" cty:"target_runlevel" hcl:"target_runlevel"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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},
|
||||||
|
"config_file": &hcldec.AttrSpec{Name: "config_file", Type: cty.String, Required: false},
|
||||||
|
"output_directory": &hcldec.AttrSpec{Name: "output_directory", Type: cty.String, Required: false},
|
||||||
|
"container_name": &hcldec.AttrSpec{Name: "container_name", Type: cty.String, Required: false},
|
||||||
|
"command_wrapper": &hcldec.AttrSpec{Name: "command_wrapper", Type: cty.String, Required: false},
|
||||||
|
"init_timeout": &hcldec.AttrSpec{Name: "init_timeout", Type: cty.String, Required: false},
|
||||||
|
"create_options": &hcldec.AttrSpec{Name: "create_options", Type: cty.List(cty.String), Required: false},
|
||||||
|
"start_options": &hcldec.AttrSpec{Name: "start_options", Type: cty.List(cty.String), Required: false},
|
||||||
|
"attach_options": &hcldec.AttrSpec{Name: "attach_options", Type: cty.List(cty.String), Required: false},
|
||||||
|
"template_name": &hcldec.AttrSpec{Name: "template_name", Type: cty.String, Required: false},
|
||||||
|
"template_parameters": &hcldec.AttrSpec{Name: "template_parameters", Type: cty.List(cty.String), Required: false},
|
||||||
|
"template_environment_vars": &hcldec.AttrSpec{Name: "template_environment_vars", Type: cty.List(cty.String), Required: false},
|
||||||
|
"target_runlevel": &hcldec.AttrSpec{Name: "target_runlevel", Type: cty.Number, Required: false},
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
package lxc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepExport struct{}
|
||||||
|
|
||||||
|
func (s *stepExport) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
|
||||||
|
name := config.ContainerName
|
||||||
|
|
||||||
|
lxc_dir := "/var/lib/lxc"
|
||||||
|
user, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
log.Print("Cannot find current user. Falling back to /var/lib/lxc...")
|
||||||
|
}
|
||||||
|
if user.Uid != "0" && user.HomeDir != "" {
|
||||||
|
lxc_dir = filepath.Join(user.HomeDir, ".local", "share", "lxc")
|
||||||
|
}
|
||||||
|
|
||||||
|
containerDir := filepath.Join(lxc_dir, name)
|
||||||
|
outputPath := filepath.Join(config.OutputDir, "rootfs.tar.gz")
|
||||||
|
configFilePath := filepath.Join(config.OutputDir, "lxc-config")
|
||||||
|
|
||||||
|
configFile, err := os.Create(configFilePath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error creating config file: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
originalConfigFile, err := os.Open(config.ConfigFile)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error opening config file: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(configFile, originalConfigFile)
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("error copying file %s: %v", config.ConfigFile, err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
commands := make([][]string, 3)
|
||||||
|
commands[0] = []string{
|
||||||
|
"lxc-stop", "--name", name,
|
||||||
|
}
|
||||||
|
commands[1] = []string{
|
||||||
|
"tar", "-C", containerDir, "--numeric-owner", "--anchored", "--exclude=./rootfs/dev/log", "-czf", outputPath, "./rootfs",
|
||||||
|
}
|
||||||
|
commands[2] = []string{
|
||||||
|
"chmod", "+x", configFilePath,
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("Exporting container...")
|
||||||
|
for _, command := range commands {
|
||||||
|
err := RunCommand(command...)
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error exporting container: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepExport) Cleanup(state multistep.StateBag) {}
|
||||||
@@ -0,0 +1,78 @@
|
|||||||
|
package lxc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepLxcCreate struct{}
|
||||||
|
|
||||||
|
func (s *stepLxcCreate) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
|
||||||
|
name := config.ContainerName
|
||||||
|
|
||||||
|
// TODO: read from env
|
||||||
|
lxc_dir := "/var/lib/lxc"
|
||||||
|
user, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
log.Print("Cannot find current user. Falling back to /var/lib/lxc...")
|
||||||
|
}
|
||||||
|
if user.Uid != "0" && user.HomeDir != "" {
|
||||||
|
lxc_dir = filepath.Join(user.HomeDir, ".local", "share", "lxc")
|
||||||
|
}
|
||||||
|
rootfs := filepath.Join(lxc_dir, name, "rootfs")
|
||||||
|
|
||||||
|
if config.PackerForce {
|
||||||
|
s.Cleanup(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
commands := make([][]string, 3)
|
||||||
|
commands[0] = append(commands[0], "env")
|
||||||
|
commands[0] = append(commands[0], config.EnvVars...)
|
||||||
|
commands[0] = append(commands[0], "lxc-create")
|
||||||
|
commands[0] = append(commands[0], config.CreateOptions...)
|
||||||
|
commands[0] = append(commands[0], []string{"-n", name, "-t", config.Name, "--"}...)
|
||||||
|
commands[0] = append(commands[0], config.Parameters...)
|
||||||
|
// prevent tmp from being cleaned on boot, we put provisioning scripts there
|
||||||
|
// todo: wait for init to finish before moving on to provisioning instead of this
|
||||||
|
commands[1] = []string{"touch", filepath.Join(rootfs, "tmp", ".tmpfs")}
|
||||||
|
commands[2] = append([]string{"lxc-start"}, config.StartOptions...)
|
||||||
|
commands[2] = append(commands[2], []string{"-d", "--name", name}...)
|
||||||
|
|
||||||
|
ui.Say("Creating container...")
|
||||||
|
for _, command := range commands {
|
||||||
|
err := RunCommand(command...)
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error creating container: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Put("mount_path", rootfs)
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepLxcCreate) Cleanup(state multistep.StateBag) {
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
|
||||||
|
command := []string{
|
||||||
|
"lxc-destroy", "-f", "-n", config.ContainerName,
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("Unregistering and deleting virtual machine...")
|
||||||
|
if err := RunCommand(command...); err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Error deleting virtual machine: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
package lxc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepPrepareOutputDir struct{}
|
||||||
|
|
||||||
|
func (stepPrepareOutputDir) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
|
||||||
|
if _, err := os.Stat(config.OutputDir); err == nil && config.PackerForce {
|
||||||
|
ui.Say("Deleting previous output directory...")
|
||||||
|
os.RemoveAll(config.OutputDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(config.OutputDir, 0755); err != nil {
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (stepPrepareOutputDir) Cleanup(state multistep.StateBag) {
|
||||||
|
_, cancelled := state.GetOk(multistep.StateCancelled)
|
||||||
|
_, halted := state.GetOk(multistep.StateHalted)
|
||||||
|
|
||||||
|
if cancelled || halted {
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
|
||||||
|
ui.Say("Deleting output directory...")
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
err := os.RemoveAll(config.OutputDir)
|
||||||
|
if err == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Error removing output dir: %s", err)
|
||||||
|
time.Sleep(2 * time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package lxc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StepProvision provisions the instance within a chroot.
|
||||||
|
type StepProvision struct{}
|
||||||
|
|
||||||
|
func (s *StepProvision) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
hook := state.Get("hook").(packersdk.Hook)
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
mountPath := state.Get("mount_path").(string)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||||
|
|
||||||
|
// Create our communicator
|
||||||
|
comm := &LxcAttachCommunicator{
|
||||||
|
ContainerName: config.ContainerName,
|
||||||
|
AttachOptions: config.AttachOptions,
|
||||||
|
RootFs: mountPath,
|
||||||
|
CmdWrapper: wrappedCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads hook data from builder's state, if it has been set.
|
||||||
|
hookData := commonsteps.PopulateProvisionHookData(state)
|
||||||
|
|
||||||
|
// Update state generated_data with complete hookData
|
||||||
|
// to make them accessible by post-processors
|
||||||
|
state.Put("generated_data", hookData)
|
||||||
|
|
||||||
|
// Provision
|
||||||
|
log.Println("Running the provision hook")
|
||||||
|
if err := hook.Run(ctx, packersdk.HookProvision, ui, comm, hookData); err != nil {
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepProvision) Cleanup(state multistep.StateBag) {}
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
package lxc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StepWaitInit struct {
|
||||||
|
WaitTimeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepWaitInit) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
cancel := make(chan struct{})
|
||||||
|
waitDone := make(chan bool, 1)
|
||||||
|
go func() {
|
||||||
|
ui.Say("Waiting for container to finish init...")
|
||||||
|
err = s.waitForInit(state, cancel)
|
||||||
|
waitDone <- true
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Printf("Waiting for container to finish init, up to timeout: %s", s.WaitTimeout)
|
||||||
|
timeout := time.After(s.WaitTimeout)
|
||||||
|
WaitLoop:
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-waitDone:
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Error waiting for container to finish init: %s", err))
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("Container finished init!")
|
||||||
|
break WaitLoop
|
||||||
|
case <-timeout:
|
||||||
|
err := fmt.Errorf("Timeout waiting for container to finish init.")
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
close(cancel)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
if _, ok := state.GetOk(multistep.StateCancelled); ok {
|
||||||
|
close(cancel)
|
||||||
|
log.Println("Interrupt detected, quitting waiting for container to finish init.")
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepWaitInit) Cleanup(multistep.StateBag) {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepWaitInit) waitForInit(state multistep.StateBag, cancel <-chan struct{}) error {
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
mountPath := state.Get("mount_path").(string)
|
||||||
|
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-cancel:
|
||||||
|
log.Println("Cancelled. Exiting loop.")
|
||||||
|
return errors.New("Wait cancelled")
|
||||||
|
case <-time.After(1 * time.Second):
|
||||||
|
}
|
||||||
|
|
||||||
|
comm := &LxcAttachCommunicator{
|
||||||
|
ContainerName: config.ContainerName,
|
||||||
|
AttachOptions: config.AttachOptions,
|
||||||
|
RootFs: mountPath,
|
||||||
|
CmdWrapper: wrappedCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
runlevel, _ := comm.CheckInit()
|
||||||
|
currentRunlevel := "unknown"
|
||||||
|
if arr := strings.Split(runlevel, " "); len(arr) >= 2 {
|
||||||
|
currentRunlevel = arr[1]
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Current runlevel in container: '%s'", runlevel)
|
||||||
|
|
||||||
|
targetRunlevel := fmt.Sprintf("%d", config.TargetRunlevel)
|
||||||
|
if currentRunlevel == targetRunlevel {
|
||||||
|
log.Printf("Container finished init.")
|
||||||
|
break
|
||||||
|
} else if currentRunlevel > targetRunlevel {
|
||||||
|
log.Printf("Expected Runlevel %s, Got Runlevel %s, continuing", targetRunlevel, currentRunlevel)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/version"
|
||||||
|
packerVersion "github.com/hashicorp/packer/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
var LXCPluginVersion *version.PluginVersion
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
LXCPluginVersion = version.InitializePluginVersion(
|
||||||
|
packerVersion.Version, packerVersion.VersionPrerelease)
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package lxd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Artifact struct {
|
||||||
|
id string
|
||||||
|
|
||||||
|
// StateData should store data such as GeneratedData
|
||||||
|
// to be shared with post-processors
|
||||||
|
StateData map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Artifact) BuilderId() string {
|
||||||
|
return BuilderId
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) Files() []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) Id() string {
|
||||||
|
return a.id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) String() string {
|
||||||
|
return fmt.Sprintf("image: %s", a.id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) State(name string) interface{} {
|
||||||
|
return a.StateData[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) Destroy() error {
|
||||||
|
_, err := LXDCommand("image", "delete", a.id)
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
package lxd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/hashicorp/hcl/v2/hcldec"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
|
||||||
|
)
|
||||||
|
|
||||||
|
// The unique ID for this builder
|
||||||
|
const BuilderId = "lxd"
|
||||||
|
|
||||||
|
type wrappedCommandTemplate struct {
|
||||||
|
Command string
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
|
errs := b.config.Prepare(raws...)
|
||||||
|
if errs != nil {
|
||||||
|
return nil, nil, errs
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) (packersdk.Artifact, error) {
|
||||||
|
wrappedCommand := func(command string) (string, error) {
|
||||||
|
b.config.ctx.Data = &wrappedCommandTemplate{Command: command}
|
||||||
|
return interpolate.Render(b.config.CommandWrapper, &b.config.ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
steps := []multistep.Step{
|
||||||
|
&stepLxdLaunch{},
|
||||||
|
&StepProvision{},
|
||||||
|
&stepPublish{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup the state bag
|
||||||
|
state := new(multistep.BasicStateBag)
|
||||||
|
state.Put("config", &b.config)
|
||||||
|
state.Put("hook", hook)
|
||||||
|
state.Put("ui", ui)
|
||||||
|
state.Put("wrappedCommand", CommandWrapper(wrappedCommand))
|
||||||
|
|
||||||
|
// Run
|
||||||
|
b.runner = commonsteps.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
|
||||||
|
b.runner.Run(ctx, state)
|
||||||
|
|
||||||
|
// If there was an error, return that
|
||||||
|
if rawErr, ok := state.GetOk("error"); ok {
|
||||||
|
return nil, rawErr.(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
artifact := &Artifact{
|
||||||
|
id: state.Get("imageFingerprint").(string),
|
||||||
|
StateData: map[string]interface{}{"generated_data": state.Get("generated_data")},
|
||||||
|
}
|
||||||
|
|
||||||
|
return artifact, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package lxd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testConfig() map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"output_image": "foo",
|
||||||
|
"image": "bar",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilder_Foo(t *testing.T) {
|
||||||
|
if os.Getenv("PACKER_ACC") == "" {
|
||||||
|
t.Skip("This test is only run with PACKER_ACC=1")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilderPrepare_ConfigFile(t *testing.T) {
|
||||||
|
var b Builder
|
||||||
|
// Good
|
||||||
|
config := testConfig()
|
||||||
|
_, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good, remote image
|
||||||
|
config = testConfig()
|
||||||
|
config["image"] = "remote:bar"
|
||||||
|
_, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Good, remote output image
|
||||||
|
config = testConfig()
|
||||||
|
config["output_image"] = "remote:foo"
|
||||||
|
_, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bad, missing image name
|
||||||
|
config = testConfig()
|
||||||
|
delete(config, "image")
|
||||||
|
b = Builder{}
|
||||||
|
_, warnings, err = b.Prepare(config)
|
||||||
|
if len(warnings) > 0 {
|
||||||
|
t.Fatalf("bad: %#v", warnings)
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuilder_ImplementsBuilder(t *testing.T) {
|
||||||
|
var raw interface{}
|
||||||
|
raw = &Builder{}
|
||||||
|
if _, ok := raw.(packersdk.Builder); !ok {
|
||||||
|
t.Fatalf("Builder should be a builder")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package lxd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CommandWrapper is a type that given a command, will possibly modify that
|
||||||
|
// command in-flight. This might return an error.
|
||||||
|
type CommandWrapper func(string) (string, error)
|
||||||
|
|
||||||
|
// ShellCommand takes a command string and returns an *exec.Cmd to execute
|
||||||
|
// it within the context of a shell (/bin/sh).
|
||||||
|
func ShellCommand(command string) *exec.Cmd {
|
||||||
|
return exec.Command("/bin/sh", "-c", command)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Yeah...LXD calls `lxc` because the command line is different between the
|
||||||
|
// packages. This should also avoid a naming collision between the LXC builder.
|
||||||
|
func LXDCommand(args ...string) (string, error) {
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
|
||||||
|
log.Printf("Executing lxc command: %#v", args)
|
||||||
|
cmd := exec.Command("lxc", args...)
|
||||||
|
cmd.Stdout = &stdout
|
||||||
|
cmd.Stderr = &stderr
|
||||||
|
err := cmd.Run()
|
||||||
|
|
||||||
|
stdoutString := strings.TrimSpace(stdout.String())
|
||||||
|
stderrString := strings.TrimSpace(stderr.String())
|
||||||
|
|
||||||
|
if _, ok := err.(*exec.ExitError); ok {
|
||||||
|
err = fmt.Errorf("LXD command error: %s", stderrString)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("stdout: %s", stdoutString)
|
||||||
|
log.Printf("stderr: %s", stderrString)
|
||||||
|
|
||||||
|
return stdoutString, err
|
||||||
|
}
|
||||||
@@ -0,0 +1,142 @@
|
|||||||
|
package lxd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Communicator struct {
|
||||||
|
ContainerName string
|
||||||
|
CmdWrapper CommandWrapper
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) Start(ctx context.Context, cmd *packersdk.RemoteCmd) error {
|
||||||
|
localCmd, err := c.Execute(cmd.Command)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
localCmd.Stdin = cmd.Stdin
|
||||||
|
localCmd.Stdout = cmd.Stdout
|
||||||
|
localCmd.Stderr = cmd.Stderr
|
||||||
|
if err := localCmd.Start(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
exitStatus := 0
|
||||||
|
if err := localCmd.Wait(); err != nil {
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
exitStatus = 1
|
||||||
|
|
||||||
|
// There is no process-independent way to get the REAL
|
||||||
|
// exit status so we just try to go deeper.
|
||||||
|
if status, ok := exitErr.Sys().(syscall.WaitStatus); ok {
|
||||||
|
exitStatus = status.ExitStatus()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf(
|
||||||
|
"lxc exec execution exited with '%d': '%s'",
|
||||||
|
exitStatus, cmd.Command)
|
||||||
|
cmd.SetExited(exitStatus)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) Upload(dst string, r io.Reader, fi *os.FileInfo) error {
|
||||||
|
ctx := context.TODO()
|
||||||
|
|
||||||
|
fileDestination := filepath.Join(c.ContainerName, dst)
|
||||||
|
// find out if the place we are pushing to is a directory
|
||||||
|
testDirectoryCommand := fmt.Sprintf(`test -d "%s"`, dst)
|
||||||
|
cmd := &packersdk.RemoteCmd{Command: testDirectoryCommand}
|
||||||
|
err := c.Start(ctx, cmd)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Unable to check whether remote path is a dir: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cmd.Wait()
|
||||||
|
|
||||||
|
if cmd.ExitStatus() == 0 {
|
||||||
|
log.Printf("path is a directory; copying file into directory.")
|
||||||
|
fileDestination = filepath.Join(c.ContainerName, dst, (*fi).Name())
|
||||||
|
}
|
||||||
|
|
||||||
|
cpCmd, err := c.CmdWrapper(fmt.Sprintf("lxc file push - %s", fileDestination))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Running copy command: %s", cpCmd)
|
||||||
|
command := ShellCommand(cpCmd)
|
||||||
|
command.Stdin = r
|
||||||
|
|
||||||
|
return command.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) UploadDir(dst string, src string, exclude []string) error {
|
||||||
|
fileDestination := fmt.Sprintf("%s/%s", c.ContainerName, dst)
|
||||||
|
pushCommand := fmt.Sprintf("lxc file push --debug -pr %s %s", src, fileDestination)
|
||||||
|
log.Printf(pushCommand)
|
||||||
|
cp, err := c.CmdWrapper(pushCommand)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error running cp command: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cpCmd := ShellCommand(cp)
|
||||||
|
|
||||||
|
log.Printf("Running cp command: %s", cp)
|
||||||
|
err = cpCmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error running cp command: %s", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) Download(src string, w io.Writer) error {
|
||||||
|
cpCmd, err := c.CmdWrapper(fmt.Sprintf("lxc file pull %s -", filepath.Join(c.ContainerName, src)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Running copy command: %s", cpCmd)
|
||||||
|
command := ShellCommand(cpCmd)
|
||||||
|
command.Stdout = w
|
||||||
|
|
||||||
|
return command.Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) DownloadDir(src string, dst string, exclude []string) error {
|
||||||
|
// TODO This could probably be "lxc exec <container> -- cd <src> && tar -czf - | tar -xzf - -C <dst>"
|
||||||
|
return fmt.Errorf("DownloadDir is not implemented for lxc")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Communicator) Execute(commandString string) (*exec.Cmd, error) {
|
||||||
|
log.Printf("Executing with lxc exec in container: %s %s", c.ContainerName, commandString)
|
||||||
|
command, err := c.CmdWrapper(
|
||||||
|
fmt.Sprintf("lxc exec %s -- /bin/sh -c \"%s\"", c.ContainerName, commandString))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
localCmd := ShellCommand(command)
|
||||||
|
log.Printf("Executing lxc exec: %s %#v", localCmd.Path, localCmd.Args)
|
||||||
|
|
||||||
|
return localCmd, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package lxd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestCommunicator_ImplementsCommunicator(t *testing.T) {
|
||||||
|
var raw interface{}
|
||||||
|
raw = &Communicator{}
|
||||||
|
if _, ok := raw.(packersdk.Communicator); !ok {
|
||||||
|
t.Fatalf("Communicator should be a communicator")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Acceptance tests
|
||||||
|
// TODO Execute a command
|
||||||
|
// TODO Upload a file
|
||||||
|
// TODO Download a file
|
||||||
|
// TODO Upload a Directory
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
//go:generate packer-sdc struct-markdown
|
||||||
|
//go:generate packer-sdc mapstructure-to-hcl2 -type Config
|
||||||
|
|
||||||
|
package lxd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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/mitchellh/mapstructure"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
common.PackerConfig `mapstructure:",squash"`
|
||||||
|
// The name of the output artifact. Defaults to
|
||||||
|
// name.
|
||||||
|
OutputImage string `mapstructure:"output_image" required:"false"`
|
||||||
|
ContainerName string `mapstructure:"container_name"`
|
||||||
|
// Lets you prefix all builder commands, such as
|
||||||
|
// with ssh for a remote build host. Defaults to `{{.Command}}`; i.e. no
|
||||||
|
// wrapper.
|
||||||
|
CommandWrapper string `mapstructure:"command_wrapper" required:"false"`
|
||||||
|
// The source image to use when creating the build
|
||||||
|
// container. This can be a (local or remote) image (name or fingerprint).
|
||||||
|
// E.G. my-base-image, ubuntu-daily:x, 08fababf6f27, ...
|
||||||
|
Image string `mapstructure:"image" required:"true"`
|
||||||
|
Profile string `mapstructure:"profile"`
|
||||||
|
// The number of seconds to sleep between launching
|
||||||
|
// the LXD instance and provisioning it; defaults to 3 seconds.
|
||||||
|
InitSleep string `mapstructure:"init_sleep" required:"false"`
|
||||||
|
// Pass key values to the publish
|
||||||
|
// step to be set as properties on the output image. This is most helpful to
|
||||||
|
// set the description, but can be used to set anything needed. See
|
||||||
|
// https://stgraber.org/2016/03/30/lxd-2-0-image-management-512/
|
||||||
|
// for more properties.
|
||||||
|
PublishProperties map[string]string `mapstructure:"publish_properties" required:"false"`
|
||||||
|
// List of key/value pairs you wish to
|
||||||
|
// pass to lxc launch via --config. Defaults to empty.
|
||||||
|
LaunchConfig map[string]string `mapstructure:"launch_config" required:"false"`
|
||||||
|
|
||||||
|
ctx interpolate.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Prepare(raws ...interface{}) error {
|
||||||
|
|
||||||
|
var md mapstructure.Metadata
|
||||||
|
err := config.Decode(c, &config.DecodeOpts{
|
||||||
|
Metadata: &md,
|
||||||
|
Interpolate: true,
|
||||||
|
}, raws...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accumulate any errors
|
||||||
|
var errs *packersdk.MultiError
|
||||||
|
|
||||||
|
if c.ContainerName == "" {
|
||||||
|
c.ContainerName = fmt.Sprintf("packer-%s", c.PackerBuildName)
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.OutputImage == "" {
|
||||||
|
c.OutputImage = c.ContainerName
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.CommandWrapper == "" {
|
||||||
|
c.CommandWrapper = "{{.Command}}"
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Image == "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("`image` is a required parameter for LXD. Please specify an image by alias or fingerprint. e.g. `ubuntu-daily:x`"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Profile == "" {
|
||||||
|
c.Profile = "default"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sadly we have to wait a few seconds for /tmp to be intialized and networking
|
||||||
|
// to finish starting. There isn't a great cross platform to check when things are ready.
|
||||||
|
if c.InitSleep == "" {
|
||||||
|
c.InitSleep = "3"
|
||||||
|
}
|
||||||
|
|
||||||
|
if errs != nil && len(errs.Errors) > 0 {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package lxd
|
||||||
|
|
||||||
|
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"`
|
||||||
|
OutputImage *string `mapstructure:"output_image" required:"false" cty:"output_image" hcl:"output_image"`
|
||||||
|
ContainerName *string `mapstructure:"container_name" cty:"container_name" hcl:"container_name"`
|
||||||
|
CommandWrapper *string `mapstructure:"command_wrapper" required:"false" cty:"command_wrapper" hcl:"command_wrapper"`
|
||||||
|
Image *string `mapstructure:"image" required:"true" cty:"image" hcl:"image"`
|
||||||
|
Profile *string `mapstructure:"profile" cty:"profile" hcl:"profile"`
|
||||||
|
InitSleep *string `mapstructure:"init_sleep" required:"false" cty:"init_sleep" hcl:"init_sleep"`
|
||||||
|
PublishProperties map[string]string `mapstructure:"publish_properties" required:"false" cty:"publish_properties" hcl:"publish_properties"`
|
||||||
|
LaunchConfig map[string]string `mapstructure:"launch_config" required:"false" cty:"launch_config" hcl:"launch_config"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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},
|
||||||
|
"output_image": &hcldec.AttrSpec{Name: "output_image", Type: cty.String, Required: false},
|
||||||
|
"container_name": &hcldec.AttrSpec{Name: "container_name", Type: cty.String, Required: false},
|
||||||
|
"command_wrapper": &hcldec.AttrSpec{Name: "command_wrapper", Type: cty.String, Required: false},
|
||||||
|
"image": &hcldec.AttrSpec{Name: "image", Type: cty.String, Required: false},
|
||||||
|
"profile": &hcldec.AttrSpec{Name: "profile", Type: cty.String, Required: false},
|
||||||
|
"init_sleep": &hcldec.AttrSpec{Name: "init_sleep", Type: cty.String, Required: false},
|
||||||
|
"publish_properties": &hcldec.AttrSpec{Name: "publish_properties", Type: cty.Map(cty.String), Required: false},
|
||||||
|
"launch_config": &hcldec.AttrSpec{Name: "launch_config", Type: cty.Map(cty.String), Required: false},
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
package lxd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepLxdLaunch struct{}
|
||||||
|
|
||||||
|
func (s *stepLxdLaunch) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
|
||||||
|
name := config.ContainerName
|
||||||
|
image := config.Image
|
||||||
|
profile := fmt.Sprintf("--profile=%s", config.Profile)
|
||||||
|
|
||||||
|
launch_args := []string{
|
||||||
|
"launch", "--ephemeral=false", profile, image, name,
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range config.LaunchConfig {
|
||||||
|
launch_args = append(launch_args, "--config", fmt.Sprintf("%s=%s", k, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("Creating container...")
|
||||||
|
_, err := LXDCommand(launch_args...)
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error creating container: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
sleep_seconds, err := strconv.Atoi(config.InitSleep)
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error parsing InitSleep into int: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Should we check `lxc info <container>` for "Running"?
|
||||||
|
// We have to do this so /tmp doesn't get cleared and lose our provisioner scripts.
|
||||||
|
|
||||||
|
time.Sleep(time.Duration(sleep_seconds) * time.Second)
|
||||||
|
log.Printf("Sleeping for %d seconds...", sleep_seconds)
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepLxdLaunch) Cleanup(state multistep.StateBag) {
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
|
||||||
|
cleanup_args := []string{
|
||||||
|
"delete", "--force", config.ContainerName,
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("Unregistering and deleting deleting container...")
|
||||||
|
if _, err := LXDCommand(cleanup_args...); err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Error deleting container: %s", err))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
package lxd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep/commonsteps"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// StepProvision provisions the container
|
||||||
|
type StepProvision struct{}
|
||||||
|
|
||||||
|
func (s *StepProvision) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
hook := state.Get("hook").(packersdk.Hook)
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
wrappedCommand := state.Get("wrappedCommand").(CommandWrapper)
|
||||||
|
|
||||||
|
// Create our communicator
|
||||||
|
comm := &Communicator{
|
||||||
|
ContainerName: config.ContainerName,
|
||||||
|
CmdWrapper: wrappedCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads hook data from builder's state, if it has been set.
|
||||||
|
hookData := commonsteps.PopulateProvisionHookData(state)
|
||||||
|
|
||||||
|
// Update state generated_data with complete hookData
|
||||||
|
// to make them accessible by post-processors
|
||||||
|
state.Put("generated_data", hookData)
|
||||||
|
|
||||||
|
// Provision
|
||||||
|
log.Println("Running the provision hook")
|
||||||
|
if err := hook.Run(ctx, packersdk.HookProvision, ui, comm, hookData); err != nil {
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepProvision) Cleanup(state multistep.StateBag) {}
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
package lxd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepPublish struct{}
|
||||||
|
|
||||||
|
func (s *stepPublish) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
|
||||||
|
name := config.ContainerName
|
||||||
|
stop_args := []string{
|
||||||
|
// We created the container with "--ephemeral=false" so we know it is safe to stop.
|
||||||
|
"stop", name,
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("Stopping container...")
|
||||||
|
_, err := LXDCommand(stop_args...)
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error stopping container: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
publish_args := []string{
|
||||||
|
"publish", name, "--alias", config.OutputImage,
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range config.PublishProperties {
|
||||||
|
publish_args = append(publish_args, fmt.Sprintf("%s=%s", k, v))
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("Publishing container...")
|
||||||
|
stdoutString, err := LXDCommand(publish_args...)
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error publishing container: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
r := regexp.MustCompile("([0-9a-fA-F]+)$")
|
||||||
|
fingerprint := r.FindAllStringSubmatch(stdoutString, -1)[0][0]
|
||||||
|
|
||||||
|
ui.Say(fmt.Sprintf("Created image: %s", fingerprint))
|
||||||
|
|
||||||
|
state.Put("imageFingerprint", fingerprint)
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepPublish) Cleanup(state multistep.StateBag) {}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package version
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/version"
|
||||||
|
packerVersion "github.com/hashicorp/packer/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
var LXDPluginVersion *version.PluginVersion
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
LXDPluginVersion = version.InitializePluginVersion(
|
||||||
|
packerVersion.Version, packerVersion.VersionPrerelease)
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Artifact is an artifact implementation that contains Image List
|
||||||
|
// and Machine Image info.
|
||||||
|
type Artifact struct {
|
||||||
|
MachineImageName string
|
||||||
|
MachineImageFile string
|
||||||
|
ImageListVersion int
|
||||||
|
|
||||||
|
// StateData should store data such as GeneratedData
|
||||||
|
// to be shared with post-processors
|
||||||
|
StateData map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuilderId uniquely identifies the builder.
|
||||||
|
func (a *Artifact) BuilderId() string {
|
||||||
|
return BuilderId
|
||||||
|
}
|
||||||
|
|
||||||
|
// Files lists the files associated with an artifact. We don't have any files
|
||||||
|
// as the custom image is stored server side.
|
||||||
|
func (a *Artifact) Files() []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) Id() string {
|
||||||
|
return a.MachineImageName
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) String() string {
|
||||||
|
return fmt.Sprintf("An image list entry was created: \n"+
|
||||||
|
"Name: %s\n"+
|
||||||
|
"File: %s\n"+
|
||||||
|
"Version: %d",
|
||||||
|
a.MachineImageName, a.MachineImageFile, a.ImageListVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) State(name string) interface{} {
|
||||||
|
return a.StateData[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy deletes the custom image associated with the artifact.
|
||||||
|
func (a *Artifact) Destroy() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
//go:generate packer-sdc mapstructure-to-hcl2 -type Config
|
||||||
|
|
||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-cleanhttp"
|
||||||
|
"github.com/hashicorp/go-oracle-terraform/compute"
|
||||||
|
"github.com/hashicorp/go-oracle-terraform/opc"
|
||||||
|
"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"
|
||||||
|
ocommon "github.com/hashicorp/packer/builder/oracle/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BuilderId uniquely identifies the builder
|
||||||
|
const BuilderId = "packer.oracle.classic"
|
||||||
|
|
||||||
|
// Builder is a builder implementation that creates Oracle OCI custom images.
|
||||||
|
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) {
|
||||||
|
err := b.config.Prepare(raws...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var errs *packersdk.MultiError
|
||||||
|
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, b.config.PVConfig.Prepare(&b.config.ctx))
|
||||||
|
|
||||||
|
if errs != nil && len(errs.Errors) > 0 {
|
||||||
|
return nil, nil, errs
|
||||||
|
}
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) (packersdk.Artifact, error) {
|
||||||
|
loggingEnabled := os.Getenv("PACKER_OCI_CLASSIC_LOGGING") != ""
|
||||||
|
httpClient := cleanhttp.DefaultClient()
|
||||||
|
config := &opc.Config{
|
||||||
|
Username: opc.String(b.config.Username),
|
||||||
|
Password: opc.String(b.config.Password),
|
||||||
|
IdentityDomain: opc.String(b.config.IdentityDomain),
|
||||||
|
APIEndpoint: b.config.apiEndpointURL,
|
||||||
|
LogLevel: opc.LogDebug,
|
||||||
|
Logger: &Logger{loggingEnabled},
|
||||||
|
// Logger: # Leave blank to use the default logger, or provide your own
|
||||||
|
HTTPClient: httpClient,
|
||||||
|
}
|
||||||
|
// Create the Compute Client
|
||||||
|
client, err := compute.NewComputeClient(config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("Error creating OPC Compute Client: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
runID := fmt.Sprintf("%s_%s", b.config.ImageName, os.Getenv("PACKER_RUN_UUID"))
|
||||||
|
// Populate the state bag
|
||||||
|
state := new(multistep.BasicStateBag)
|
||||||
|
state.Put("config", &b.config)
|
||||||
|
state.Put("hook", hook)
|
||||||
|
state.Put("ui", ui)
|
||||||
|
state.Put("client", client)
|
||||||
|
state.Put("run_id", runID)
|
||||||
|
|
||||||
|
var steps []multistep.Step
|
||||||
|
if b.config.IsPV() {
|
||||||
|
steps = []multistep.Step{
|
||||||
|
&ocommon.StepKeyPair{
|
||||||
|
Debug: b.config.PackerDebug,
|
||||||
|
Comm: &b.config.Comm,
|
||||||
|
DebugKeyPath: fmt.Sprintf("oci_classic_%s.pem", b.config.PackerBuildName),
|
||||||
|
},
|
||||||
|
&stepCreateIPReservation{},
|
||||||
|
&stepAddKeysToAPI{
|
||||||
|
KeyName: fmt.Sprintf("packer-generated-key_%s", runID),
|
||||||
|
},
|
||||||
|
&stepSecurity{
|
||||||
|
CommType: b.config.Comm.Type,
|
||||||
|
SecurityListKey: "security_list_master",
|
||||||
|
},
|
||||||
|
&stepCreatePersistentVolume{
|
||||||
|
VolumeSize: fmt.Sprintf("%d", b.config.PersistentVolumeSize),
|
||||||
|
VolumeName: fmt.Sprintf("master-storage_%s", runID),
|
||||||
|
ImageList: b.config.SourceImageList,
|
||||||
|
ImageListEntry: b.config.SourceImageListEntry,
|
||||||
|
Bootable: true,
|
||||||
|
},
|
||||||
|
&stepCreatePVMaster{
|
||||||
|
Name: fmt.Sprintf("master-instance_%s", runID),
|
||||||
|
VolumeName: fmt.Sprintf("master-storage_%s", runID),
|
||||||
|
SecurityListKey: "security_list_master",
|
||||||
|
},
|
||||||
|
&communicator.StepConnect{
|
||||||
|
Config: &b.config.Comm,
|
||||||
|
Host: communicator.CommHost(b.config.Comm.Host(), "instance_ip"),
|
||||||
|
SSHConfig: b.config.Comm.SSHConfigFunc(),
|
||||||
|
},
|
||||||
|
&commonsteps.StepProvision{},
|
||||||
|
&stepTerminatePVMaster{},
|
||||||
|
&stepSecurity{
|
||||||
|
SecurityListKey: "security_list_builder",
|
||||||
|
CommType: "ssh",
|
||||||
|
},
|
||||||
|
&stepCreatePersistentVolume{
|
||||||
|
// We double the master volume size because we need room to
|
||||||
|
// tarball the disk image. We also need to chunk the tar ball,
|
||||||
|
// but we can remove the original disk image first.
|
||||||
|
VolumeSize: fmt.Sprintf("%d", b.config.PersistentVolumeSize*2),
|
||||||
|
VolumeName: fmt.Sprintf("builder-storage_%s", runID),
|
||||||
|
},
|
||||||
|
&stepCreatePVBuilder{
|
||||||
|
Name: fmt.Sprintf("builder-instance_%s", runID),
|
||||||
|
BuilderVolumeName: fmt.Sprintf("builder-storage_%s", runID),
|
||||||
|
SecurityListKey: "security_list_builder",
|
||||||
|
},
|
||||||
|
&stepAttachVolume{
|
||||||
|
VolumeName: fmt.Sprintf("master-storage_%s", runID),
|
||||||
|
Index: 2,
|
||||||
|
InstanceInfoKey: "builder_instance_info",
|
||||||
|
},
|
||||||
|
&stepConnectBuilder{
|
||||||
|
KeyName: fmt.Sprintf("packer-generated-key_%s", runID),
|
||||||
|
StepConnectSSH: &communicator.StepConnectSSH{
|
||||||
|
Config: &b.config.BuilderComm,
|
||||||
|
Host: communicator.CommHost(b.config.Comm.Host(), "instance_ip"),
|
||||||
|
SSHConfig: b.config.BuilderComm.SSHConfigFunc(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
&stepUploadImage{
|
||||||
|
UploadImageCommand: b.config.BuilderUploadImageCommand,
|
||||||
|
},
|
||||||
|
&stepCreateImage{},
|
||||||
|
&stepListImages{},
|
||||||
|
&commonsteps.StepCleanupTempKeys{
|
||||||
|
Comm: &b.config.Comm,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Build the steps
|
||||||
|
steps = []multistep.Step{
|
||||||
|
&ocommon.StepKeyPair{
|
||||||
|
Debug: b.config.PackerDebug,
|
||||||
|
Comm: &b.config.Comm,
|
||||||
|
DebugKeyPath: fmt.Sprintf("oci_classic_%s.pem", b.config.PackerBuildName),
|
||||||
|
},
|
||||||
|
&stepCreateIPReservation{},
|
||||||
|
&stepAddKeysToAPI{
|
||||||
|
Skip: b.config.Comm.Type != "ssh",
|
||||||
|
KeyName: fmt.Sprintf("packer-generated-key_%s", runID),
|
||||||
|
},
|
||||||
|
&stepSecurity{
|
||||||
|
SecurityListKey: "security_list",
|
||||||
|
CommType: b.config.Comm.Type,
|
||||||
|
},
|
||||||
|
&stepCreateInstance{},
|
||||||
|
&communicator.StepConnect{
|
||||||
|
Config: &b.config.Comm,
|
||||||
|
Host: communicator.CommHost(b.config.Comm.Host(), "instance_ip"),
|
||||||
|
SSHConfig: b.config.Comm.SSHConfigFunc(),
|
||||||
|
},
|
||||||
|
&commonsteps.StepProvision{},
|
||||||
|
&commonsteps.StepCleanupTempKeys{
|
||||||
|
Comm: &b.config.Comm,
|
||||||
|
},
|
||||||
|
&stepSnapshot{},
|
||||||
|
&stepListImages{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 there is no snapshot, then just return
|
||||||
|
if _, ok := state.GetOk("machine_image_name"); !ok {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the artifact and return it
|
||||||
|
artifact := &Artifact{
|
||||||
|
ImageListVersion: state.Get("image_list_version").(int),
|
||||||
|
MachineImageName: state.Get("machine_image_name").(string),
|
||||||
|
MachineImageFile: state.Get("machine_image_file").(string),
|
||||||
|
StateData: map[string]interface{}{"generated_data": state.Get("generated_data")},
|
||||||
|
}
|
||||||
|
|
||||||
|
return artifact, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel terminates a running build.
|
||||||
@@ -0,0 +1,184 @@
|
|||||||
|
// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/hashicorp/hcl/v2/hcldec"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/communicator"
|
||||||
|
"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"`
|
||||||
|
PersistentVolumeSize *int `mapstructure:"persistent_volume_size" cty:"persistent_volume_size" hcl:"persistent_volume_size"`
|
||||||
|
BuilderUploadImageCommand *string `mapstructure:"builder_upload_image_command" cty:"builder_upload_image_command" hcl:"builder_upload_image_command"`
|
||||||
|
BuilderShape *string `mapstructure:"builder_shape" cty:"builder_shape" hcl:"builder_shape"`
|
||||||
|
BuilderImageList *string `mapstructure:"builder_image_list" cty:"builder_image_list" hcl:"builder_image_list"`
|
||||||
|
BuilderImageListEntry *int `mapstructure:"builder_image_list_entry" cty:"builder_image_list_entry" hcl:"builder_image_list_entry"`
|
||||||
|
BuilderComm *communicator.FlatConfig `mapstructure:"builder_communicator" cty:"builder_communicator" hcl:"builder_communicator"`
|
||||||
|
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"`
|
||||||
|
Username *string `mapstructure:"username" cty:"username" hcl:"username"`
|
||||||
|
Password *string `mapstructure:"password" cty:"password" hcl:"password"`
|
||||||
|
IdentityDomain *string `mapstructure:"identity_domain" cty:"identity_domain" hcl:"identity_domain"`
|
||||||
|
APIEndpoint *string `mapstructure:"api_endpoint" cty:"api_endpoint" hcl:"api_endpoint"`
|
||||||
|
ImageName *string `mapstructure:"image_name" cty:"image_name" hcl:"image_name"`
|
||||||
|
Shape *string `mapstructure:"shape" cty:"shape" hcl:"shape"`
|
||||||
|
SourceImageList *string `mapstructure:"source_image_list" cty:"source_image_list" hcl:"source_image_list"`
|
||||||
|
SourceImageListEntry *int `mapstructure:"source_image_list_entry" cty:"source_image_list_entry" hcl:"source_image_list_entry"`
|
||||||
|
SnapshotTimeout *string `mapstructure:"snapshot_timeout" cty:"snapshot_timeout" hcl:"snapshot_timeout"`
|
||||||
|
DestImageList *string `mapstructure:"dest_image_list" cty:"dest_image_list" hcl:"dest_image_list"`
|
||||||
|
Attributes *string `mapstructure:"attributes" cty:"attributes" hcl:"attributes"`
|
||||||
|
AttributesFile *string `mapstructure:"attributes_file" cty:"attributes_file" hcl:"attributes_file"`
|
||||||
|
DestImageListDescription *string `mapstructure:"image_description" cty:"image_description" hcl:"image_description"`
|
||||||
|
SSHSourceList *string `mapstructure:"ssh_source_list" cty:"ssh_source_list" hcl:"ssh_source_list"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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},
|
||||||
|
"persistent_volume_size": &hcldec.AttrSpec{Name: "persistent_volume_size", Type: cty.Number, Required: false},
|
||||||
|
"builder_upload_image_command": &hcldec.AttrSpec{Name: "builder_upload_image_command", Type: cty.String, Required: false},
|
||||||
|
"builder_shape": &hcldec.AttrSpec{Name: "builder_shape", Type: cty.String, Required: false},
|
||||||
|
"builder_image_list": &hcldec.AttrSpec{Name: "builder_image_list", Type: cty.String, Required: false},
|
||||||
|
"builder_image_list_entry": &hcldec.AttrSpec{Name: "builder_image_list_entry", Type: cty.Number, Required: false},
|
||||||
|
"builder_communicator": &hcldec.BlockSpec{TypeName: "builder_communicator", Nested: hcldec.ObjectSpec((*communicator.FlatConfig)(nil).HCL2Spec())},
|
||||||
|
"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},
|
||||||
|
"username": &hcldec.AttrSpec{Name: "username", Type: cty.String, Required: false},
|
||||||
|
"password": &hcldec.AttrSpec{Name: "password", Type: cty.String, Required: false},
|
||||||
|
"identity_domain": &hcldec.AttrSpec{Name: "identity_domain", Type: cty.String, Required: false},
|
||||||
|
"api_endpoint": &hcldec.AttrSpec{Name: "api_endpoint", Type: cty.String, Required: false},
|
||||||
|
"image_name": &hcldec.AttrSpec{Name: "image_name", Type: cty.String, Required: false},
|
||||||
|
"shape": &hcldec.AttrSpec{Name: "shape", Type: cty.String, Required: false},
|
||||||
|
"source_image_list": &hcldec.AttrSpec{Name: "source_image_list", Type: cty.String, Required: false},
|
||||||
|
"source_image_list_entry": &hcldec.AttrSpec{Name: "source_image_list_entry", Type: cty.Number, Required: false},
|
||||||
|
"snapshot_timeout": &hcldec.AttrSpec{Name: "snapshot_timeout", Type: cty.String, Required: false},
|
||||||
|
"dest_image_list": &hcldec.AttrSpec{Name: "dest_image_list", Type: cty.String, Required: false},
|
||||||
|
"attributes": &hcldec.AttrSpec{Name: "attributes", Type: cty.String, Required: false},
|
||||||
|
"attributes_file": &hcldec.AttrSpec{Name: "attributes_file", Type: cty.String, Required: false},
|
||||||
|
"image_description": &hcldec.AttrSpec{Name: "image_description", Type: cty.String, Required: false},
|
||||||
|
"ssh_source_list": &hcldec.AttrSpec{Name: "ssh_source_list", Type: cty.String, Required: false},
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"net/url"
|
||||||
|
"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"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
common.PackerConfig `mapstructure:",squash"`
|
||||||
|
PVConfig `mapstructure:",squash"`
|
||||||
|
Comm communicator.Config `mapstructure:",squash"`
|
||||||
|
attribs map[string]interface{}
|
||||||
|
|
||||||
|
// Access config overrides
|
||||||
|
Username string `mapstructure:"username"`
|
||||||
|
Password string `mapstructure:"password"`
|
||||||
|
IdentityDomain string `mapstructure:"identity_domain"`
|
||||||
|
APIEndpoint string `mapstructure:"api_endpoint"`
|
||||||
|
apiEndpointURL *url.URL
|
||||||
|
|
||||||
|
// Image
|
||||||
|
ImageName string `mapstructure:"image_name"`
|
||||||
|
Shape string `mapstructure:"shape"`
|
||||||
|
SourceImageList string `mapstructure:"source_image_list"`
|
||||||
|
SourceImageListEntry int `mapstructure:"source_image_list_entry"`
|
||||||
|
SnapshotTimeout time.Duration `mapstructure:"snapshot_timeout"`
|
||||||
|
DestImageList string `mapstructure:"dest_image_list"`
|
||||||
|
// Attributes and Attributes file are both optional and mutually exclusive.
|
||||||
|
Attributes string `mapstructure:"attributes"`
|
||||||
|
AttributesFile string `mapstructure:"attributes_file"`
|
||||||
|
// Optional; if you don't enter anything, the image list description
|
||||||
|
// will read "Packer-built image list"
|
||||||
|
DestImageListDescription string `mapstructure:"image_description"`
|
||||||
|
// Optional. Describes what computers are allowed to reach your instance
|
||||||
|
// via SSH. This whitelist must contain the computer you're running Packer
|
||||||
|
// from. It defaults to public-internet, meaning that you can SSH into your
|
||||||
|
// instance from anywhere as long as you have the right keys
|
||||||
|
SSHSourceList string `mapstructure:"ssh_source_list"`
|
||||||
|
|
||||||
|
ctx interpolate.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Identifier(s string) string {
|
||||||
|
return fmt.Sprintf("/Compute-%s/%s/%s", c.IdentityDomain, c.Username, s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Prepare(raws ...interface{}) error {
|
||||||
|
|
||||||
|
// Decode from template
|
||||||
|
err := config.Decode(c, &config.DecodeOpts{
|
||||||
|
Interpolate: true,
|
||||||
|
InterpolateContext: &c.ctx,
|
||||||
|
}, raws...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to mapstructure Config: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.apiEndpointURL, err = url.Parse(c.APIEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Error parsing API Endpoint: %s", err)
|
||||||
|
}
|
||||||
|
// Set default source list
|
||||||
|
if c.SSHSourceList == "" {
|
||||||
|
c.SSHSourceList = "seciplist:/oracle/public/public-internet"
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.SnapshotTimeout == 0 {
|
||||||
|
c.SnapshotTimeout = 20 * time.Minute
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate that all required fields are present
|
||||||
|
var errs *packersdk.MultiError
|
||||||
|
required := map[string]string{
|
||||||
|
"username": c.Username,
|
||||||
|
"password": c.Password,
|
||||||
|
"api_endpoint": c.APIEndpoint,
|
||||||
|
"identity_domain": c.IdentityDomain,
|
||||||
|
"source_image_list": c.SourceImageList,
|
||||||
|
"dest_image_list": c.DestImageList,
|
||||||
|
"shape": c.Shape,
|
||||||
|
}
|
||||||
|
for k, v := range required {
|
||||||
|
if v == "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("You must specify a %s.", k))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Object names can contain only alphanumeric characters, hyphens, underscores, and periods
|
||||||
|
reValidObject := regexp.MustCompile("^[a-zA-Z0-9-._/]+$")
|
||||||
|
var objectValidation = []struct {
|
||||||
|
name string
|
||||||
|
value string
|
||||||
|
}{
|
||||||
|
{"dest_image_list", c.DestImageList},
|
||||||
|
{"image_name", c.ImageName},
|
||||||
|
}
|
||||||
|
for _, ov := range objectValidation {
|
||||||
|
if !reValidObject.MatchString(ov.value) {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("%s can contain only alphanumeric characters, hyphens, underscores, and periods.", ov.name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Attributes != "" && c.AttributesFile != "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("Only one of user_data or user_data_file can be specified."))
|
||||||
|
} else if c.AttributesFile != "" {
|
||||||
|
if _, err := os.Stat(c.AttributesFile); err != nil {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("attributes_file not found: %s", c.AttributesFile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, es...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// unpack attributes from json into config
|
||||||
|
var data map[string]interface{}
|
||||||
|
|
||||||
|
if c.Attributes != "" {
|
||||||
|
err := json.Unmarshal([]byte(c.Attributes), &data)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem parsing json from attributes: %s", err)
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, err)
|
||||||
|
}
|
||||||
|
c.attribs = data
|
||||||
|
} else if c.AttributesFile != "" {
|
||||||
|
fidata, err := ioutil.ReadFile(c.AttributesFile)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem reading attributes_file: %s", err)
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, err)
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(fidata, &data)
|
||||||
|
c.attribs = data
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem parsing json from attributes_file: %s", err)
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, err)
|
||||||
|
}
|
||||||
|
c.attribs = data
|
||||||
|
}
|
||||||
|
|
||||||
|
if errs != nil && len(errs.Errors) > 0 {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testConfig() map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"identity_domain": "abc12345",
|
||||||
|
"username": "test@hashicorp.com",
|
||||||
|
"password": "testpassword123",
|
||||||
|
"api_endpoint": "https://api-test.compute.test.oraclecloud.com/",
|
||||||
|
"dest_image_list": "/Config-thing/myuser/myimage",
|
||||||
|
"source_image_list": "/oracle/public/whatever",
|
||||||
|
"shape": "oc3",
|
||||||
|
"image_name": "TestImageName",
|
||||||
|
"ssh_username": "opc",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigAutoFillsSourceList(t *testing.T) {
|
||||||
|
tc := testConfig()
|
||||||
|
var conf Config
|
||||||
|
err := conf.Prepare(tc)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Should not have error: %s", err.Error())
|
||||||
|
}
|
||||||
|
if conf.SSHSourceList != "seciplist:/oracle/public/public-internet" {
|
||||||
|
t.Fatalf("conf.SSHSourceList should have been "+
|
||||||
|
"\"seciplist:/oracle/public/public-internet\" but is \"%s\"",
|
||||||
|
conf.SSHSourceList)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigValidationCatchesMissing(t *testing.T) {
|
||||||
|
required := []string{
|
||||||
|
"username",
|
||||||
|
"password",
|
||||||
|
"api_endpoint",
|
||||||
|
"identity_domain",
|
||||||
|
"dest_image_list",
|
||||||
|
"source_image_list",
|
||||||
|
"shape",
|
||||||
|
"ssh_username",
|
||||||
|
}
|
||||||
|
for _, key := range required {
|
||||||
|
tc := testConfig()
|
||||||
|
delete(tc, key)
|
||||||
|
var c Config
|
||||||
|
err := c.Prepare(tc)
|
||||||
|
if err == nil {
|
||||||
|
t.Fatalf("Test should have failed when config lacked %s!", key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfigValidatesObjects(t *testing.T) {
|
||||||
|
var objectTests = []struct {
|
||||||
|
object string
|
||||||
|
valid bool
|
||||||
|
}{
|
||||||
|
{"foo-BAR.0_9", true},
|
||||||
|
{"%", false},
|
||||||
|
{"Matt...?", false},
|
||||||
|
{"/Config-thing/myuser/myimage", true},
|
||||||
|
}
|
||||||
|
for _, s := range []string{"dest_image_list", "image_name"} {
|
||||||
|
for _, tt := range objectTests {
|
||||||
|
tc := testConfig()
|
||||||
|
tc[s] = tt.object
|
||||||
|
var c Config
|
||||||
|
err := c.Prepare(tc)
|
||||||
|
if tt.valid {
|
||||||
|
assert.NoError(t, err, tt.object)
|
||||||
|
} else {
|
||||||
|
assert.Error(t, err, tt.object)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import "log"
|
||||||
|
|
||||||
|
type Logger struct {
|
||||||
|
Enabled bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *Logger) Log(input ...interface{}) {
|
||||||
|
if !l.Enabled {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println(input...)
|
||||||
|
}
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/communicator"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
|
||||||
|
)
|
||||||
|
|
||||||
|
const imageListDefault = "/oracle/public/OL_7.2_UEKR4_x86_64"
|
||||||
|
const imageListEntryDefault = 5
|
||||||
|
const usernameDefault = "opc"
|
||||||
|
const shapeDefault = "oc3"
|
||||||
|
const uploadImageCommandDefault = `
|
||||||
|
# https://www.oracle.com/webfolder/technetwork/tutorials/obe/cloud/objectstorage/upload_files_gt_5GB_REST_API/upload_files_gt_5GB_REST_API.html
|
||||||
|
|
||||||
|
# Split diskimage in to 100mb chunks
|
||||||
|
split -b 100m diskimage.tar.gz segment_
|
||||||
|
printf "Split diskimage into %s segments\n" $(ls segment_* | wc -l)
|
||||||
|
|
||||||
|
# Download jq tool
|
||||||
|
curl -OLs https://github.com/stedolan/jq/releases/download/jq-1.5/jq-linux64
|
||||||
|
mv jq-linux64 jq
|
||||||
|
chmod u+x jq
|
||||||
|
|
||||||
|
# Create manifest file
|
||||||
|
(
|
||||||
|
for i in segment_*; do
|
||||||
|
./jq -n --arg path "{{.SegmentPath}}/$i" \
|
||||||
|
--arg etag $(md5sum $i | cut -f1 -d' ') \
|
||||||
|
--arg size_bytes $(stat --printf "%s" $i) \
|
||||||
|
'{path: $path, etag: $etag, size_bytes: $size_bytes}'
|
||||||
|
done
|
||||||
|
) | ./jq -s . > manifest.json
|
||||||
|
|
||||||
|
# Authenticate
|
||||||
|
curl -D auth-headers -s -X GET \
|
||||||
|
-H "X-Storage-User: Storage-{{.AccountID}}:{{.Username}}" \
|
||||||
|
-H "X-Storage-Pass: {{.Password}}" \
|
||||||
|
https://{{.AccountID}}.storage.oraclecloud.com/auth/v1.0
|
||||||
|
|
||||||
|
export AUTH_TOKEN=$(awk 'BEGIN {FS=": "; RS="\r\n"}/^X-Auth-Token/{print $2}' auth-headers)
|
||||||
|
export STORAGE_URL=$(awk 'BEGIN {FS=": "; RS="\r\n"}/^X-Storage-Url/{print $2}' auth-headers)
|
||||||
|
|
||||||
|
# Upload segments
|
||||||
|
for i in segment_*; do
|
||||||
|
echo "Uploading segment $i"
|
||||||
|
|
||||||
|
curl -s -X PUT -T $i \
|
||||||
|
-H "X-Auth-Token: $AUTH_TOKEN" ${STORAGE_URL}/{{.SegmentPath}}/$i;
|
||||||
|
done
|
||||||
|
|
||||||
|
# Create machine image from manifest
|
||||||
|
curl -s -X PUT \
|
||||||
|
-H "X-Auth-Token: $AUTH_TOKEN" \
|
||||||
|
"${STORAGE_URL}/compute_images/{{.ImageFile}}?multipart-manifest=put" \
|
||||||
|
-T ./manifest.json
|
||||||
|
|
||||||
|
# Get uploaded image description
|
||||||
|
echo "Details of ${STORAGE_URL}/compute_images/{{.ImageFile}}:"
|
||||||
|
curl -I -X HEAD \
|
||||||
|
-H "X-Auth-Token: $AUTH_TOKEN" \
|
||||||
|
"${STORAGE_URL}/compute_images/{{.ImageFile}}"
|
||||||
|
`
|
||||||
|
|
||||||
|
type PVConfig struct {
|
||||||
|
// PersistentVolumeSize lets us control the volume size by using persistent boot storage
|
||||||
|
PersistentVolumeSize int `mapstructure:"persistent_volume_size"`
|
||||||
|
BuilderUploadImageCommand string `mapstructure:"builder_upload_image_command"`
|
||||||
|
|
||||||
|
// Builder Image
|
||||||
|
BuilderShape string `mapstructure:"builder_shape"`
|
||||||
|
BuilderImageList string `mapstructure:"builder_image_list"`
|
||||||
|
BuilderImageListEntry *int `mapstructure:"builder_image_list_entry"`
|
||||||
|
|
||||||
|
BuilderComm communicator.Config `mapstructure:"builder_communicator"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsPV tells us if we're using a persistent volume for this build
|
||||||
|
func (c *PVConfig) IsPV() bool {
|
||||||
|
return c.PersistentVolumeSize > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PVConfig) Prepare(ctx *interpolate.Context) (errs *packersdk.MultiError) {
|
||||||
|
if !c.IsPV() {
|
||||||
|
if c.BuilderShape != "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs,
|
||||||
|
fmt.Errorf("`builder_shape` has no meaning when `persistent_volume_size` is not set."))
|
||||||
|
}
|
||||||
|
if c.BuilderImageList != "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs,
|
||||||
|
fmt.Errorf("`builder_image_list` has no meaning when `persistent_volume_size` is not set."))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.BuilderImageListEntry != nil {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs,
|
||||||
|
fmt.Errorf("`builder_image_list_entry` has no meaning when `persistent_volume_size` is not set."))
|
||||||
|
}
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
c.BuilderComm.SSHPty = true
|
||||||
|
if c.BuilderComm.Type == "winrm" {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs,
|
||||||
|
fmt.Errorf("`ssh` is the only valid builder communicator type."))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.BuilderShape == "" {
|
||||||
|
c.BuilderShape = shapeDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.BuilderComm.SSHUsername == "" {
|
||||||
|
c.BuilderComm.SSHUsername = usernameDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.BuilderImageList == "" {
|
||||||
|
c.BuilderImageList = imageListDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set to known working default if this is unset and we're using the
|
||||||
|
// default image list
|
||||||
|
if c.BuilderImageListEntry == nil {
|
||||||
|
var entry int
|
||||||
|
if c.BuilderImageList == imageListDefault {
|
||||||
|
entry = imageListEntryDefault
|
||||||
|
}
|
||||||
|
c.BuilderImageListEntry = &entry
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.BuilderUploadImageCommand == "" {
|
||||||
|
c.BuilderUploadImageCommand = uploadImageCommandDefault
|
||||||
|
}
|
||||||
|
|
||||||
|
if es := c.BuilderComm.Prepare(ctx); len(es) > 0 {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, es...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestPVConfigEntry(t *testing.T) {
|
||||||
|
entry := 1
|
||||||
|
var entryTests = []struct {
|
||||||
|
imageList string
|
||||||
|
imageListEntry *int
|
||||||
|
expected int
|
||||||
|
}{
|
||||||
|
{"x", nil, 0},
|
||||||
|
{"x", &entry, 1},
|
||||||
|
{imageListDefault, nil, 5},
|
||||||
|
{imageListDefault, &entry, 1},
|
||||||
|
}
|
||||||
|
for _, tt := range entryTests {
|
||||||
|
tc := &PVConfig{
|
||||||
|
PersistentVolumeSize: 1,
|
||||||
|
BuilderImageList: tt.imageList,
|
||||||
|
BuilderImageListEntry: tt.imageListEntry,
|
||||||
|
}
|
||||||
|
errs := tc.Prepare(nil)
|
||||||
|
assert.Nil(t, errs, "Didn't expect any errors")
|
||||||
|
assert.Equal(t, tt.expected, *tc.BuilderImageListEntry)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-oracle-terraform/compute"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepAddKeysToAPI struct {
|
||||||
|
Skip bool
|
||||||
|
KeyName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepAddKeysToAPI) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
// get variables from state
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
|
||||||
|
if s.Skip {
|
||||||
|
ui.Say("Skipping generating SSH keys...")
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
// grab packer-generated key from statebag context.
|
||||||
|
// Always check configured communicator for keys
|
||||||
|
sshPublicKey := bytes.TrimSpace(config.Comm.SSHPublicKey)
|
||||||
|
|
||||||
|
// form API call to add key to compute cloud
|
||||||
|
|
||||||
|
ui.Say(fmt.Sprintf("Creating temporary key: %s", s.KeyName))
|
||||||
|
|
||||||
|
sshKeysClient := client.SSHKeys()
|
||||||
|
sshKeysInput := compute.CreateSSHKeyInput{
|
||||||
|
Name: s.KeyName,
|
||||||
|
Key: string(sshPublicKey),
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the packer-generated SSH key into the Oracle Compute cloud.
|
||||||
|
keyInfo, err := sshKeysClient.CreateSSHKey(&sshKeysInput)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem adding Public SSH key through Oracle's API: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
config.Comm.SSHKeyPairName = keyInfo.Name
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepAddKeysToAPI) Cleanup(state multistep.StateBag) {
|
||||||
|
if s.Skip {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
// Delete the keys we created during this run
|
||||||
|
if len(config.Comm.SSHKeyPairName) == 0 {
|
||||||
|
// No keys were generated; none need to be cleaned up.
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
ui.Say("Deleting SSH keys...")
|
||||||
|
deleteInput := compute.DeleteSSHKeyInput{Name: config.Comm.SSHKeyPairName}
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
deleteClient := client.SSHKeys()
|
||||||
|
err := deleteClient.DeleteSSHKey(&deleteInput)
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Error deleting SSH keys: %s", err.Error()))
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-oracle-terraform/compute"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepAttachVolume struct {
|
||||||
|
Index int
|
||||||
|
VolumeName string
|
||||||
|
InstanceInfoKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepAttachVolume) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
instanceInfo := state.Get(s.InstanceInfoKey).(*compute.InstanceInfo)
|
||||||
|
|
||||||
|
saClient := client.StorageAttachments()
|
||||||
|
saInput := &compute.CreateStorageAttachmentInput{
|
||||||
|
Index: s.Index,
|
||||||
|
InstanceName: instanceInfo.Name + "/" + instanceInfo.ID,
|
||||||
|
StorageVolumeName: s.VolumeName,
|
||||||
|
}
|
||||||
|
|
||||||
|
sa, err := saClient.CreateStorageAttachment(saInput)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem attaching master volume: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Put(s.InstanceInfoKey+"/attachment", sa)
|
||||||
|
|
||||||
|
ui.Message("Volume attached to instance.")
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepAttachVolume) Cleanup(state multistep.StateBag) {
|
||||||
|
sa, ok := state.GetOk(s.InstanceInfoKey + "/attachment")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
|
||||||
|
saClient := client.StorageAttachments()
|
||||||
|
saI := &compute.DeleteStorageAttachmentInput{
|
||||||
|
Name: sa.(*compute.StorageAttachmentInfo).Name,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := saClient.DeleteStorageAttachment(saI); err != nil {
|
||||||
|
err = fmt.Errorf("Problem detaching storage volume: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/communicator"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepConnectBuilder struct {
|
||||||
|
*communicator.StepConnectSSH
|
||||||
|
KeyName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepConnectBuilder) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
s.Config.SSHKeyPairName = s.KeyName
|
||||||
|
return s.StepConnectSSH.Run(ctx, state)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepConnectBuilder) Cleanup(state multistep.StateBag) {
|
||||||
|
s.StepConnectSSH.Cleanup(state)
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-oracle-terraform/compute"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepCreateImage struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreateImage) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
imageFile := state.Get("image_file").(string)
|
||||||
|
|
||||||
|
// Image uploaded, let's register it
|
||||||
|
machineImageClient := client.MachineImages()
|
||||||
|
createMI := &compute.CreateMachineImageInput{
|
||||||
|
// Two-part name of the account
|
||||||
|
Account: fmt.Sprintf("/Compute-%s/cloud_storage", config.IdentityDomain),
|
||||||
|
Description: "Packer generated Machine Image.",
|
||||||
|
// The three-part name of the object
|
||||||
|
Name: config.ImageName,
|
||||||
|
// image_file.tar.gz, where image_file is the .tar.gz name of the
|
||||||
|
// machine image file that you have uploaded to Oracle Cloud
|
||||||
|
// Infrastructure Object Storage Classic.
|
||||||
|
File: imageFile,
|
||||||
|
}
|
||||||
|
mi, err := machineImageClient.CreateMachineImage(createMI)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error creating machine image: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
log.Println("Registered machine image.")
|
||||||
|
state.Put("machine_image", mi.Name)
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreateImage) Cleanup(state multistep.StateBag) {
|
||||||
|
_, cancelled := state.GetOk(multistep.StateCancelled)
|
||||||
|
_, halted := state.GetOk(multistep.StateHalted)
|
||||||
|
if !cancelled && !halted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
ui.Say("Cleaning up Image...")
|
||||||
|
|
||||||
|
machineImageClient := client.MachineImages()
|
||||||
|
deleteMI := &compute.DeleteMachineImageInput{
|
||||||
|
Name: config.ImageName,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := machineImageClient.DeleteMachineImage(deleteMI); err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Error cleaning up machine image: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Message(fmt.Sprintf("Deleted Image: %s", config.ImageName))
|
||||||
|
}
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-oracle-terraform/compute"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepCreateInstance struct{}
|
||||||
|
|
||||||
|
func (s *stepCreateInstance) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
// get variables from state
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
ui.Say("Creating Instance...")
|
||||||
|
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
ipAddName := state.Get("ipres_name").(string)
|
||||||
|
secListName := state.Get("security_list").(string)
|
||||||
|
|
||||||
|
netInfo := compute.NetworkingInfo{
|
||||||
|
Nat: []string{ipAddName},
|
||||||
|
SecLists: []string{secListName},
|
||||||
|
}
|
||||||
|
|
||||||
|
// get instances client
|
||||||
|
instanceClient := client.Instances()
|
||||||
|
|
||||||
|
// Instances Input
|
||||||
|
input := &compute.CreateInstanceInput{
|
||||||
|
Name: config.ImageName,
|
||||||
|
Shape: config.Shape,
|
||||||
|
ImageList: config.SourceImageList,
|
||||||
|
Entry: config.SourceImageListEntry,
|
||||||
|
Networking: map[string]compute.NetworkingInfo{"eth0": netInfo},
|
||||||
|
Attributes: config.attribs,
|
||||||
|
}
|
||||||
|
if config.Comm.Type == "ssh" {
|
||||||
|
input.SSHKeys = []string{config.Comm.SSHKeyPairName}
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceInfo, err := instanceClient.CreateInstance(input)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem creating instance: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Put("instance_info", instanceInfo)
|
||||||
|
state.Put("instance_id", instanceInfo.ID)
|
||||||
|
ui.Message(fmt.Sprintf("Created instance: %s.", instanceInfo.ID))
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreateInstance) Cleanup(state multistep.StateBag) {
|
||||||
|
instanceID, ok := state.GetOk("instance_id")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// terminate instance
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
|
||||||
|
ui.Say("Terminating source instance...")
|
||||||
|
|
||||||
|
instanceClient := client.Instances()
|
||||||
|
input := &compute.DeleteInstanceInput{
|
||||||
|
Name: config.ImageName,
|
||||||
|
ID: instanceID.(string),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := instanceClient.DeleteInstance(input)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem destroying instance: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ui.Say("Terminated instance.")
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-oracle-terraform/compute"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/uuid"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepCreateIPReservation struct{}
|
||||||
|
|
||||||
|
func (s *stepCreateIPReservation) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
iprClient := client.IPReservations()
|
||||||
|
// TODO: add optional Name and Tags
|
||||||
|
|
||||||
|
ipresName := fmt.Sprintf("ipres_%s_%s", config.ImageName, uuid.TimeOrderedUUID())
|
||||||
|
ui.Message(fmt.Sprintf("Creating temporary IP reservation: %s", ipresName))
|
||||||
|
|
||||||
|
IPInput := &compute.CreateIPReservationInput{
|
||||||
|
ParentPool: compute.PublicReservationPool,
|
||||||
|
Permanent: true,
|
||||||
|
Name: ipresName,
|
||||||
|
}
|
||||||
|
ipRes, err := iprClient.CreateIPReservation(IPInput)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error creating IP Reservation: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
state.Put("instance_ip", ipRes.IP)
|
||||||
|
state.Put("ipres_name", ipresName)
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreateIPReservation) Cleanup(state multistep.StateBag) {
|
||||||
|
ipResName, ok := state.GetOk("ipres_name")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
ui.Say("Cleaning up IP reservations...")
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
|
||||||
|
input := compute.DeleteIPReservationInput{Name: ipResName.(string)}
|
||||||
|
ipClient := client.IPReservations()
|
||||||
|
err := ipClient.DeleteIPReservation(&input)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("error deleting IP reservation: %s", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-oracle-terraform/compute"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepCreatePersistentVolume struct {
|
||||||
|
VolumeSize string
|
||||||
|
VolumeName string
|
||||||
|
Bootable bool
|
||||||
|
ImageList string
|
||||||
|
ImageListEntry int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreatePersistentVolume) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
ui.Say("Creating Volume...")
|
||||||
|
|
||||||
|
c := &compute.CreateStorageVolumeInput{
|
||||||
|
Name: s.VolumeName,
|
||||||
|
Size: s.VolumeSize,
|
||||||
|
ImageList: s.ImageList,
|
||||||
|
ImageListEntry: s.ImageListEntry,
|
||||||
|
Properties: []string{"/oracle/public/storage/default"},
|
||||||
|
Bootable: s.Bootable,
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := client.StorageVolumes()
|
||||||
|
cc, err := sc.CreateStorageVolume(c)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error creating persistent storage volume: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Message(fmt.Sprintf("Created volume: %s", cc.Name))
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreatePersistentVolume) Cleanup(state multistep.StateBag) {
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
ui.Say("Cleaning up Volume...")
|
||||||
|
|
||||||
|
c := &compute.DeleteStorageVolumeInput{
|
||||||
|
Name: s.VolumeName,
|
||||||
|
}
|
||||||
|
|
||||||
|
sc := client.StorageVolumes()
|
||||||
|
|
||||||
|
if err := sc.DeleteStorageVolume(c); err != nil {
|
||||||
|
ui.Error(fmt.Sprintf("Error cleaning up persistent storage volume: %s", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Message(fmt.Sprintf("Deleted volume: %s", s.VolumeName))
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-oracle-terraform/compute"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepCreatePVBuilder struct {
|
||||||
|
Name string
|
||||||
|
BuilderVolumeName string
|
||||||
|
SecurityListKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreatePVBuilder) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
// get variables from state
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
ui.Say("Creating builder instance...")
|
||||||
|
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
ipAddName := state.Get("ipres_name").(string)
|
||||||
|
secListName := state.Get(s.SecurityListKey).(string)
|
||||||
|
|
||||||
|
// get instances client
|
||||||
|
instanceClient := client.Instances()
|
||||||
|
|
||||||
|
// Instances Input
|
||||||
|
input := &compute.CreateInstanceInput{
|
||||||
|
Name: s.Name,
|
||||||
|
Shape: config.BuilderShape,
|
||||||
|
Networking: map[string]compute.NetworkingInfo{
|
||||||
|
"eth0": {
|
||||||
|
Nat: []string{ipAddName},
|
||||||
|
SecLists: []string{secListName},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Storage: []compute.StorageAttachmentInput{
|
||||||
|
{
|
||||||
|
Volume: s.BuilderVolumeName,
|
||||||
|
Index: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
ImageList: config.BuilderImageList,
|
||||||
|
SSHKeys: []string{config.Comm.SSHKeyPairName},
|
||||||
|
Entry: *config.BuilderImageListEntry,
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceInfo, err := instanceClient.CreateInstance(input)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem creating instance: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
log.Printf("Created instance %s", instanceInfo.Name)
|
||||||
|
|
||||||
|
state.Put("builder_instance_info", instanceInfo)
|
||||||
|
state.Put("builder_instance_id", instanceInfo.ID)
|
||||||
|
|
||||||
|
ui.Message(fmt.Sprintf("Created builder instance: %s.", instanceInfo.Name))
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreatePVBuilder) Cleanup(state multistep.StateBag) {
|
||||||
|
instanceID, ok := state.GetOk("builder_instance_id")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// terminate instance
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
|
||||||
|
ui.Say("Terminating builder instance...")
|
||||||
|
|
||||||
|
instanceClient := client.Instances()
|
||||||
|
input := &compute.DeleteInstanceInput{
|
||||||
|
Name: s.Name,
|
||||||
|
ID: instanceID.(string),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := instanceClient.DeleteInstance(input)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem destroying instance: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ui.Say("Terminated builder instance.")
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-oracle-terraform/compute"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepCreatePVMaster struct {
|
||||||
|
Name string
|
||||||
|
VolumeName string
|
||||||
|
SecurityListKey string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreatePVMaster) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
ui.Say("Creating master instance...")
|
||||||
|
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
ipAddName := state.Get("ipres_name").(string)
|
||||||
|
secListName := state.Get(s.SecurityListKey).(string)
|
||||||
|
|
||||||
|
// get instances client
|
||||||
|
instanceClient := client.Instances()
|
||||||
|
|
||||||
|
// Instances Input
|
||||||
|
input := &compute.CreateInstanceInput{
|
||||||
|
Name: s.Name,
|
||||||
|
Shape: config.Shape,
|
||||||
|
Networking: map[string]compute.NetworkingInfo{
|
||||||
|
"eth0": {
|
||||||
|
Nat: []string{ipAddName},
|
||||||
|
SecLists: []string{secListName},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Storage: []compute.StorageAttachmentInput{
|
||||||
|
{
|
||||||
|
Volume: s.VolumeName,
|
||||||
|
Index: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
BootOrder: []int{1},
|
||||||
|
Attributes: config.attribs,
|
||||||
|
}
|
||||||
|
if config.Comm.Type == "ssh" {
|
||||||
|
input.SSHKeys = []string{config.Comm.SSHKeyPairName}
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceInfo, err := instanceClient.CreateInstance(input)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem creating instance: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Put("master_instance_info", instanceInfo)
|
||||||
|
state.Put("master_instance_id", instanceInfo.ID)
|
||||||
|
ui.Message(fmt.Sprintf("Created master instance: %s.", instanceInfo.Name))
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreatePVMaster) Cleanup(state multistep.StateBag) {
|
||||||
|
if _, deleted := state.GetOk("master_instance_deleted"); deleted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceID, ok := state.GetOk("master_instance_id")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// terminate instance
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
|
||||||
|
ui.Say("Terminating builder instance...")
|
||||||
|
|
||||||
|
instanceClient := client.Instances()
|
||||||
|
input := &compute.DeleteInstanceInput{
|
||||||
|
Name: s.Name,
|
||||||
|
ID: instanceID.(string),
|
||||||
|
}
|
||||||
|
|
||||||
|
err := instanceClient.DeleteInstance(input)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem destroying instance: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ui.Say("Terminated master instance.")
|
||||||
|
}
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-oracle-terraform/compute"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepListImages struct{}
|
||||||
|
|
||||||
|
func (s *stepListImages) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
// get variables from state
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
ui.Say("Adding image to image list...")
|
||||||
|
|
||||||
|
imageListClient := client.ImageList()
|
||||||
|
getInput := compute.GetImageListInput{
|
||||||
|
Name: config.DestImageList,
|
||||||
|
}
|
||||||
|
imList, err := imageListClient.GetImageList(&getInput)
|
||||||
|
if err != nil {
|
||||||
|
// If the list didn't exist, create it.
|
||||||
|
ui.Say(fmt.Sprintf(err.Error()))
|
||||||
|
ui.Say(fmt.Sprintf("Destination image list %s does not exist; Creating it...",
|
||||||
|
config.DestImageList))
|
||||||
|
|
||||||
|
ilInput := compute.CreateImageListInput{
|
||||||
|
Name: config.DestImageList,
|
||||||
|
Description: "Packer-built image list",
|
||||||
|
}
|
||||||
|
|
||||||
|
imList, err = imageListClient.CreateImageList(&ilInput)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem creating image list: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
ui.Message(fmt.Sprintf("Image list %s created!", imList.URI))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now create and image list entry for the image into that list.
|
||||||
|
machineImage := state.Get("machine_image").(string)
|
||||||
|
version := len(imList.Entries) + 1
|
||||||
|
entriesClient := client.ImageListEntries()
|
||||||
|
entriesInput := compute.CreateImageListEntryInput{
|
||||||
|
Name: config.DestImageList,
|
||||||
|
MachineImages: []string{config.Identifier(machineImage)},
|
||||||
|
Version: version,
|
||||||
|
}
|
||||||
|
entryInfo, err := entriesClient.CreateImageListEntry(&entriesInput)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem creating an image list entry: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
state.Put("image_list_entry", entryInfo)
|
||||||
|
ui.Message(fmt.Sprintf("created image list entry %s", entryInfo.Name))
|
||||||
|
|
||||||
|
machineImagesClient := client.MachineImages()
|
||||||
|
getImagesInput := compute.GetMachineImageInput{
|
||||||
|
Name: config.ImageName,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update image list default to use latest version
|
||||||
|
updateInput := compute.UpdateImageListInput{
|
||||||
|
Default: version,
|
||||||
|
Description: config.DestImageListDescription,
|
||||||
|
Name: config.DestImageList,
|
||||||
|
}
|
||||||
|
_, err = imageListClient.UpdateImageList(&updateInput)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem updating default image list version: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grab info about the machine image to return with the artifact
|
||||||
|
imInfo, err := machineImagesClient.GetMachineImage(&getImagesInput)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem getting machine image info: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
state.Put("machine_image_file", imInfo.File)
|
||||||
|
state.Put("machine_image_name", imInfo.Name)
|
||||||
|
state.Put("image_list_version", version)
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepListImages) Cleanup(state multistep.StateBag) {
|
||||||
|
// Nothing to do
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-oracle-terraform/compute"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepSecurity struct {
|
||||||
|
CommType string
|
||||||
|
SecurityListKey string
|
||||||
|
secListName string
|
||||||
|
secRuleName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepSecurity) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
runID := state.Get("run_id").(string)
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
|
||||||
|
commType := ""
|
||||||
|
if s.CommType == "ssh" {
|
||||||
|
commType = "SSH"
|
||||||
|
} else if s.CommType == "winrm" {
|
||||||
|
commType = "WINRM"
|
||||||
|
}
|
||||||
|
secListName := fmt.Sprintf("Packer_%s_Allow_%s", commType, runID)
|
||||||
|
|
||||||
|
if _, ok := state.GetOk(secListName); ok {
|
||||||
|
log.Println("SecList created in earlier step, continuing")
|
||||||
|
// copy sec list name to proper key
|
||||||
|
state.Put(s.SecurityListKey, secListName)
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say(fmt.Sprintf("Configuring security lists and rules to enable %s access...", commType))
|
||||||
|
log.Println(secListName)
|
||||||
|
|
||||||
|
secListClient := client.SecurityLists()
|
||||||
|
secListInput := compute.CreateSecurityListInput{
|
||||||
|
Description: fmt.Sprintf("Packer-generated security list to give packer %s access", commType),
|
||||||
|
Name: config.Identifier(secListName),
|
||||||
|
}
|
||||||
|
_, err := secListClient.CreateSecurityList(&secListInput)
|
||||||
|
if err != nil {
|
||||||
|
if !strings.Contains(err.Error(), "already exists") {
|
||||||
|
err = fmt.Errorf("Error creating security List to"+
|
||||||
|
" allow Packer to connect to Oracle instance via %s: %s", commType, err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// DOCS NOTE: user must have Compute_Operations role
|
||||||
|
// Create security rule that allows Packer to connect via SSH or winRM
|
||||||
|
var application string
|
||||||
|
if commType == "SSH" {
|
||||||
|
application = "/oracle/public/ssh"
|
||||||
|
} else if commType == "WINRM" {
|
||||||
|
// Check to see whether a winRM security application is already defined
|
||||||
|
applicationClient := client.SecurityApplications()
|
||||||
|
application = fmt.Sprintf("packer_winRM_%s", runID)
|
||||||
|
applicationInput := compute.CreateSecurityApplicationInput{
|
||||||
|
Description: "Allows Packer to connect to instance via winRM",
|
||||||
|
DPort: "5985-5986",
|
||||||
|
Name: application,
|
||||||
|
Protocol: "TCP",
|
||||||
|
}
|
||||||
|
_, err = applicationClient.CreateSecurityApplication(&applicationInput)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error creating security application to"+
|
||||||
|
" allow Packer to connect to Oracle instance via %s: %s", commType, err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
state.Put("winrm_application", application)
|
||||||
|
}
|
||||||
|
secRulesClient := client.SecRules()
|
||||||
|
secRuleName := fmt.Sprintf("Packer-allow-%s-Rule_%s", commType, runID)
|
||||||
|
log.Println(secRuleName)
|
||||||
|
secRulesInput := compute.CreateSecRuleInput{
|
||||||
|
Action: "PERMIT",
|
||||||
|
Application: application,
|
||||||
|
Description: "Packer-generated security rule to allow ssh/winrm",
|
||||||
|
DestinationList: "seclist:" + config.Identifier(secListName),
|
||||||
|
Name: config.Identifier(secRuleName),
|
||||||
|
SourceList: config.SSHSourceList,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = secRulesClient.CreateSecRule(&secRulesInput)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error creating security rule to"+
|
||||||
|
" allow Packer to connect to Oracle instance: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
state.Put(s.SecurityListKey, secListName)
|
||||||
|
state.Put(secListName, true)
|
||||||
|
s.secListName = secListName
|
||||||
|
s.secRuleName = secRuleName
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepSecurity) Cleanup(state multistep.StateBag) {
|
||||||
|
if s.secListName == "" || s.secRuleName == "" {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
|
||||||
|
ui.Say("Deleting temporary rules and lists...")
|
||||||
|
|
||||||
|
// delete security rules that Packer generated
|
||||||
|
secRulesClient := client.SecRules()
|
||||||
|
ruleInput := compute.DeleteSecRuleInput{
|
||||||
|
Name: config.Identifier(s.secRuleName),
|
||||||
|
}
|
||||||
|
err := secRulesClient.DeleteSecRule(&ruleInput)
|
||||||
|
if err != nil {
|
||||||
|
ui.Say(fmt.Sprintf("Error deleting the packer-generated security rule %s; "+
|
||||||
|
"please delete manually. (error: %s)", s.secRuleName, err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// delete security list that Packer generated
|
||||||
|
secListClient := client.SecurityLists()
|
||||||
|
input := compute.DeleteSecurityListInput{Name: config.Identifier(s.secListName)}
|
||||||
|
err = secListClient.DeleteSecurityList(&input)
|
||||||
|
if err != nil {
|
||||||
|
ui.Say(fmt.Sprintf("Error deleting the packer-generated security list %s; "+
|
||||||
|
"please delete manually. (error : %s)", s.secListName, err.Error()))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some extra cleanup if we used the winRM communicator
|
||||||
|
if s.CommType == "winrm" {
|
||||||
|
// Delete the packer-generated application
|
||||||
|
application, ok := state.GetOk("winrm_application")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
applicationClient := client.SecurityApplications()
|
||||||
|
deleteApplicationInput := compute.DeleteSecurityApplicationInput{
|
||||||
|
Name: config.Identifier(application.(string)),
|
||||||
|
}
|
||||||
|
err = applicationClient.DeleteSecurityApplication(&deleteApplicationInput)
|
||||||
|
if err != nil {
|
||||||
|
ui.Say(fmt.Sprintf("Error deleting the packer-generated winrm security application %s; "+
|
||||||
|
"please delete manually. (error : %s)", application.(string), err.Error()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -0,0 +1,72 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-oracle-terraform/compute"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepSnapshot struct {
|
||||||
|
cleanupSnap bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepSnapshot) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
// get variables from state
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
ui.Say("Creating Snapshot...")
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
instanceID := state.Get("instance_id").(string)
|
||||||
|
|
||||||
|
// get instances client
|
||||||
|
snapshotClient := client.Snapshots()
|
||||||
|
|
||||||
|
// Instances Input
|
||||||
|
snapshotInput := &compute.CreateSnapshotInput{
|
||||||
|
Instance: fmt.Sprintf("%s/%s", config.ImageName, instanceID),
|
||||||
|
MachineImage: config.ImageName,
|
||||||
|
Timeout: config.SnapshotTimeout,
|
||||||
|
}
|
||||||
|
|
||||||
|
snap, err := snapshotClient.CreateSnapshot(snapshotInput)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem creating snapshot: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
state.Put("snapshot", snap)
|
||||||
|
state.Put("machine_image", snap.MachineImage)
|
||||||
|
ui.Message(fmt.Sprintf("Created snapshot: %s.", snap.Name))
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepSnapshot) Cleanup(state multistep.StateBag) {
|
||||||
|
// Delete the snapshot
|
||||||
|
var snap *compute.Snapshot
|
||||||
|
if snapshot, ok := state.GetOk("snapshot"); ok {
|
||||||
|
snap = snapshot.(*compute.Snapshot)
|
||||||
|
} else {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
ui.Say("Deleting Snapshot...")
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
snapClient := client.Snapshots()
|
||||||
|
snapInput := compute.DeleteSnapshotInput{
|
||||||
|
Snapshot: snap.Name,
|
||||||
|
MachineImage: snap.MachineImage,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := snapClient.DeleteSnapshotResourceOnly(&snapInput)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem deleting snapshot: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/go-oracle-terraform/compute"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepTerminatePVMaster struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepTerminatePVMaster) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
// get variables from state
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
ui.Say("Deleting master Instance...")
|
||||||
|
|
||||||
|
client := state.Get("client").(*compute.Client)
|
||||||
|
instanceInfo := state.Get("master_instance_info").(*compute.InstanceInfo)
|
||||||
|
|
||||||
|
// get instances client
|
||||||
|
instanceClient := client.Instances()
|
||||||
|
|
||||||
|
// Instances Input
|
||||||
|
input := &compute.DeleteInstanceInput{
|
||||||
|
Name: instanceInfo.Name,
|
||||||
|
ID: instanceInfo.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := instanceClient.DeleteInstance(input)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem creating instance: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Message(fmt.Sprintf("Deleted master instance: %s.", instanceInfo.ID))
|
||||||
|
state.Put("master_instance_deleted", true)
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepTerminatePVMaster) Cleanup(state multistep.StateBag) {
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package classic
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepUploadImage struct {
|
||||||
|
UploadImageCommand string
|
||||||
|
segmentPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
type uploadCmdData struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
AccountID string
|
||||||
|
ImageFile string
|
||||||
|
SegmentPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepUploadImage) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
comm := state.Get("communicator").(packersdk.Communicator)
|
||||||
|
config := state.Get("config").(*Config)
|
||||||
|
runID := state.Get("run_id").(string)
|
||||||
|
|
||||||
|
imageFile := fmt.Sprintf("%s.tar.gz", config.ImageName)
|
||||||
|
state.Put("image_file", imageFile)
|
||||||
|
s.segmentPath = fmt.Sprintf("compute_images_segments/%s/_segment_/%s", imageFile, runID)
|
||||||
|
|
||||||
|
config.ctx.Data = uploadCmdData{
|
||||||
|
Username: config.Username,
|
||||||
|
Password: config.Password,
|
||||||
|
AccountID: config.IdentityDomain,
|
||||||
|
ImageFile: imageFile,
|
||||||
|
SegmentPath: s.segmentPath,
|
||||||
|
}
|
||||||
|
uploadImageCmd, err := interpolate.Render(s.UploadImageCommand, &config.ctx)
|
||||||
|
if err != nil {
|
||||||
|
err := fmt.Errorf("Error processing image upload command: %s", err)
|
||||||
|
state.Put("error", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
command := fmt.Sprintf(`#!/bin/sh
|
||||||
|
set -e
|
||||||
|
mkdir /builder
|
||||||
|
mkfs -t ext3 /dev/xvdb
|
||||||
|
mount /dev/xvdb /builder
|
||||||
|
chown opc:opc /builder
|
||||||
|
cd /builder
|
||||||
|
dd if=/dev/xvdc bs=8M status=progress | cp --sparse=always /dev/stdin diskimage.raw
|
||||||
|
tar czSf ./diskimage.tar.gz ./diskimage.raw
|
||||||
|
rm diskimage.raw
|
||||||
|
%s`, uploadImageCmd)
|
||||||
|
|
||||||
|
dest := "/tmp/create-packer-diskimage.sh"
|
||||||
|
comm.Upload(dest, strings.NewReader(command), nil)
|
||||||
|
cmd := &packersdk.RemoteCmd{
|
||||||
|
Command: fmt.Sprintf("sudo /bin/sh %s", dest),
|
||||||
|
}
|
||||||
|
if err := cmd.RunWithUi(ctx, comm, ui); err != nil {
|
||||||
|
err = fmt.Errorf("Problem creating image`: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.ExitStatus() != 0 {
|
||||||
|
err = fmt.Errorf("Create Disk Image command failed with exit code %d", cmd.ExitStatus())
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("Uploaded image to object storage.")
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepUploadImage) Cleanup(state multistep.StateBag) {
|
||||||
|
_, cancelled := state.GetOk(multistep.StateCancelled)
|
||||||
|
_, halted := state.GetOk(multistep.StateHalted)
|
||||||
|
if !cancelled && !halted {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("Some segments may need to be manually cleaned at '%s'", s.segmentPath)
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/communicator"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
"golang.org/x/crypto/ssh"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StepKeyPair struct {
|
||||||
|
Debug bool
|
||||||
|
Comm *communicator.Config
|
||||||
|
DebugKeyPath string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepKeyPair) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
|
||||||
|
if s.Comm.SSHPrivateKeyFile != "" {
|
||||||
|
ui.Say("Using existing SSH private key")
|
||||||
|
privateKeyBytes, err := s.Comm.ReadSSHPrivateKeyFile()
|
||||||
|
if err != nil {
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
key, err := ssh.ParsePrivateKey(privateKeyBytes)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error parsing 'ssh_private_key_file': %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Comm.SSHPublicKey = ssh.MarshalAuthorizedKey(key.PublicKey())
|
||||||
|
s.Comm.SSHPrivateKey = privateKeyBytes
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("Creating temporary ssh key for instance...")
|
||||||
|
|
||||||
|
priv, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error creating temporary SSH key: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
// ASN.1 DER encoded form
|
||||||
|
privDer := x509.MarshalPKCS1PrivateKey(priv)
|
||||||
|
privBlk := pem.Block{Type: "RSA PRIVATE KEY", Headers: nil, Bytes: privDer}
|
||||||
|
|
||||||
|
// Set the private key in the statebag for later
|
||||||
|
state.Put("privateKey", string(pem.EncodeToMemory(&privBlk)))
|
||||||
|
|
||||||
|
// Marshal the public key into SSH compatible format
|
||||||
|
pub, err := ssh.NewPublicKey(&priv.PublicKey)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error marshaling temporary SSH public key: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
s.Comm.SSHPublicKey = ssh.MarshalAuthorizedKey(pub)
|
||||||
|
|
||||||
|
// If we're in debug mode, output the private key to the working
|
||||||
|
// directory.
|
||||||
|
if s.Debug {
|
||||||
|
ui.Message(fmt.Sprintf("Saving key for debug purposes: %s", s.DebugKeyPath))
|
||||||
|
f, err := os.Create(s.DebugKeyPath)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error saving debug key: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
// Write the key out
|
||||||
|
if _, err := f.Write(pem.EncodeToMemory(&privBlk)); err != nil {
|
||||||
|
err = fmt.Errorf("Error saving debug key: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
// Chmod it so that it is SSH ready
|
||||||
|
if runtime.GOOS != "windows" {
|
||||||
|
if err := f.Chmod(0600); err != nil {
|
||||||
|
err = fmt.Errorf("Error setting permissions of debug key: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StepKeyPair) Cleanup(state multistep.StateBag) {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
@@ -0,0 +1,375 @@
|
|||||||
|
Copyright (c) 2017 Oracle and/or its affiliates. All rights reserved.
|
||||||
|
|
||||||
|
Mozilla Public License Version 2.0
|
||||||
|
==================================
|
||||||
|
|
||||||
|
1. Definitions
|
||||||
|
--------------
|
||||||
|
|
||||||
|
1.1. "Contributor"
|
||||||
|
means each individual or legal entity that creates, contributes to
|
||||||
|
the creation of, or owns Covered Software.
|
||||||
|
|
||||||
|
1.2. "Contributor Version"
|
||||||
|
means the combination of the Contributions of others (if any) used
|
||||||
|
by a Contributor and that particular Contributor's Contribution.
|
||||||
|
|
||||||
|
1.3. "Contribution"
|
||||||
|
means Covered Software of a particular Contributor.
|
||||||
|
|
||||||
|
1.4. "Covered Software"
|
||||||
|
means Source Code Form to which the initial Contributor has attached
|
||||||
|
the notice in Exhibit A, the Executable Form of such Source Code
|
||||||
|
Form, and Modifications of such Source Code Form, in each case
|
||||||
|
including portions thereof.
|
||||||
|
|
||||||
|
1.5. "Incompatible With Secondary Licenses"
|
||||||
|
means
|
||||||
|
|
||||||
|
(a) that the initial Contributor has attached the notice described
|
||||||
|
in Exhibit B to the Covered Software; or
|
||||||
|
|
||||||
|
(b) that the Covered Software was made available under the terms of
|
||||||
|
version 1.1 or earlier of the License, but not also under the
|
||||||
|
terms of a Secondary License.
|
||||||
|
|
||||||
|
1.6. "Executable Form"
|
||||||
|
means any form of the work other than Source Code Form.
|
||||||
|
|
||||||
|
1.7. "Larger Work"
|
||||||
|
means a work that combines Covered Software with other material, in
|
||||||
|
a separate file or files, that is not Covered Software.
|
||||||
|
|
||||||
|
1.8. "License"
|
||||||
|
means this document.
|
||||||
|
|
||||||
|
1.9. "Licensable"
|
||||||
|
means having the right to grant, to the maximum extent possible,
|
||||||
|
whether at the time of the initial grant or subsequently, any and
|
||||||
|
all of the rights conveyed by this License.
|
||||||
|
|
||||||
|
1.10. "Modifications"
|
||||||
|
means any of the following:
|
||||||
|
|
||||||
|
(a) any file in Source Code Form that results from an addition to,
|
||||||
|
deletion from, or modification of the contents of Covered
|
||||||
|
Software; or
|
||||||
|
|
||||||
|
(b) any new file in Source Code Form that contains any Covered
|
||||||
|
Software.
|
||||||
|
|
||||||
|
1.11. "Patent Claims" of a Contributor
|
||||||
|
means any patent claim(s), including without limitation, method,
|
||||||
|
process, and apparatus claims, in any patent Licensable by such
|
||||||
|
Contributor that would be infringed, but for the grant of the
|
||||||
|
License, by the making, using, selling, offering for sale, having
|
||||||
|
made, import, or transfer of either its Contributions or its
|
||||||
|
Contributor Version.
|
||||||
|
|
||||||
|
1.12. "Secondary License"
|
||||||
|
means either the GNU General Public License, Version 2.0, the GNU
|
||||||
|
Lesser General Public License, Version 2.1, the GNU Affero General
|
||||||
|
Public License, Version 3.0, or any later versions of those
|
||||||
|
licenses.
|
||||||
|
|
||||||
|
1.13. "Source Code Form"
|
||||||
|
means the form of the work preferred for making modifications.
|
||||||
|
|
||||||
|
1.14. "You" (or "Your")
|
||||||
|
means an individual or a legal entity exercising rights under this
|
||||||
|
License. For legal entities, "You" includes any entity that
|
||||||
|
controls, is controlled by, or is under common control with You. For
|
||||||
|
purposes of this definition, "control" means (a) the power, direct
|
||||||
|
or indirect, to cause the direction or management of such entity,
|
||||||
|
whether by contract or otherwise, or (b) ownership of more than
|
||||||
|
fifty percent (50%) of the outstanding shares or beneficial
|
||||||
|
ownership of such entity.
|
||||||
|
|
||||||
|
2. License Grants and Conditions
|
||||||
|
--------------------------------
|
||||||
|
|
||||||
|
2.1. Grants
|
||||||
|
|
||||||
|
Each Contributor hereby grants You a world-wide, royalty-free,
|
||||||
|
non-exclusive license:
|
||||||
|
|
||||||
|
(a) under intellectual property rights (other than patent or trademark)
|
||||||
|
Licensable by such Contributor to use, reproduce, make available,
|
||||||
|
modify, display, perform, distribute, and otherwise exploit its
|
||||||
|
Contributions, either on an unmodified basis, with Modifications, or
|
||||||
|
as part of a Larger Work; and
|
||||||
|
|
||||||
|
(b) under Patent Claims of such Contributor to make, use, sell, offer
|
||||||
|
for sale, have made, import, and otherwise transfer either its
|
||||||
|
Contributions or its Contributor Version.
|
||||||
|
|
||||||
|
2.2. Effective Date
|
||||||
|
|
||||||
|
The licenses granted in Section 2.1 with respect to any Contribution
|
||||||
|
become effective for each Contribution on the date the Contributor first
|
||||||
|
distributes such Contribution.
|
||||||
|
|
||||||
|
2.3. Limitations on Grant Scope
|
||||||
|
|
||||||
|
The licenses granted in this Section 2 are the only rights granted under
|
||||||
|
this License. No additional rights or licenses will be implied from the
|
||||||
|
distribution or licensing of Covered Software under this License.
|
||||||
|
Notwithstanding Section 2.1(b) above, no patent license is granted by a
|
||||||
|
Contributor:
|
||||||
|
|
||||||
|
(a) for any code that a Contributor has removed from Covered Software;
|
||||||
|
or
|
||||||
|
|
||||||
|
(b) for infringements caused by: (i) Your and any other third party's
|
||||||
|
modifications of Covered Software, or (ii) the combination of its
|
||||||
|
Contributions with other software (except as part of its Contributor
|
||||||
|
Version); or
|
||||||
|
|
||||||
|
(c) under Patent Claims infringed by Covered Software in the absence of
|
||||||
|
its Contributions.
|
||||||
|
|
||||||
|
This License does not grant any rights in the trademarks, service marks,
|
||||||
|
or logos of any Contributor (except as may be necessary to comply with
|
||||||
|
the notice requirements in Section 3.4).
|
||||||
|
|
||||||
|
2.4. Subsequent Licenses
|
||||||
|
|
||||||
|
No Contributor makes additional grants as a result of Your choice to
|
||||||
|
distribute the Covered Software under a subsequent version of this
|
||||||
|
License (see Section 10.2) or under the terms of a Secondary License (if
|
||||||
|
permitted under the terms of Section 3.3).
|
||||||
|
|
||||||
|
2.5. Representation
|
||||||
|
|
||||||
|
Each Contributor represents that the Contributor believes its
|
||||||
|
Contributions are its original creation(s) or it has sufficient rights
|
||||||
|
to grant the rights to its Contributions conveyed by this License.
|
||||||
|
|
||||||
|
2.6. Fair Use
|
||||||
|
|
||||||
|
This License is not intended to limit any rights You have under
|
||||||
|
applicable copyright doctrines of fair use, fair dealing, or other
|
||||||
|
equivalents.
|
||||||
|
|
||||||
|
2.7. Conditions
|
||||||
|
|
||||||
|
Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
|
||||||
|
in Section 2.1.
|
||||||
|
|
||||||
|
3. Responsibilities
|
||||||
|
-------------------
|
||||||
|
|
||||||
|
3.1. Distribution of Source Form
|
||||||
|
|
||||||
|
All distribution of Covered Software in Source Code Form, including any
|
||||||
|
Modifications that You create or to which You contribute, must be under
|
||||||
|
the terms of this License. You must inform recipients that the Source
|
||||||
|
Code Form of the Covered Software is governed by the terms of this
|
||||||
|
License, and how they can obtain a copy of this License. You may not
|
||||||
|
attempt to alter or restrict the recipients' rights in the Source Code
|
||||||
|
Form.
|
||||||
|
|
||||||
|
3.2. Distribution of Executable Form
|
||||||
|
|
||||||
|
If You distribute Covered Software in Executable Form then:
|
||||||
|
|
||||||
|
(a) such Covered Software must also be made available in Source Code
|
||||||
|
Form, as described in Section 3.1, and You must inform recipients of
|
||||||
|
the Executable Form how they can obtain a copy of such Source Code
|
||||||
|
Form by reasonable means in a timely manner, at a charge no more
|
||||||
|
than the cost of distribution to the recipient; and
|
||||||
|
|
||||||
|
(b) You may distribute such Executable Form under the terms of this
|
||||||
|
License, or sublicense it under different terms, provided that the
|
||||||
|
license for the Executable Form does not attempt to limit or alter
|
||||||
|
the recipients' rights in the Source Code Form under this License.
|
||||||
|
|
||||||
|
3.3. Distribution of a Larger Work
|
||||||
|
|
||||||
|
You may create and distribute a Larger Work under terms of Your choice,
|
||||||
|
provided that You also comply with the requirements of this License for
|
||||||
|
the Covered Software. If the Larger Work is a combination of Covered
|
||||||
|
Software with a work governed by one or more Secondary Licenses, and the
|
||||||
|
Covered Software is not Incompatible With Secondary Licenses, this
|
||||||
|
License permits You to additionally distribute such Covered Software
|
||||||
|
under the terms of such Secondary License(s), so that the recipient of
|
||||||
|
the Larger Work may, at their option, further distribute the Covered
|
||||||
|
Software under the terms of either this License or such Secondary
|
||||||
|
License(s).
|
||||||
|
|
||||||
|
3.4. Notices
|
||||||
|
|
||||||
|
You may not remove or alter the substance of any license notices
|
||||||
|
(including copyright notices, patent notices, disclaimers of warranty,
|
||||||
|
or limitations of liability) contained within the Source Code Form of
|
||||||
|
the Covered Software, except that You may alter any license notices to
|
||||||
|
the extent required to remedy known factual inaccuracies.
|
||||||
|
|
||||||
|
3.5. Application of Additional Terms
|
||||||
|
|
||||||
|
You may choose to offer, and to charge a fee for, warranty, support,
|
||||||
|
indemnity or liability obligations to one or more recipients of Covered
|
||||||
|
Software. However, You may do so only on Your own behalf, and not on
|
||||||
|
behalf of any Contributor. You must make it absolutely clear that any
|
||||||
|
such warranty, support, indemnity, or liability obligation is offered by
|
||||||
|
You alone, and You hereby agree to indemnify every Contributor for any
|
||||||
|
liability incurred by such Contributor as a result of warranty, support,
|
||||||
|
indemnity or liability terms You offer. You may include additional
|
||||||
|
disclaimers of warranty and limitations of liability specific to any
|
||||||
|
jurisdiction.
|
||||||
|
|
||||||
|
4. Inability to Comply Due to Statute or Regulation
|
||||||
|
---------------------------------------------------
|
||||||
|
|
||||||
|
If it is impossible for You to comply with any of the terms of this
|
||||||
|
License with respect to some or all of the Covered Software due to
|
||||||
|
statute, judicial order, or regulation then You must: (a) comply with
|
||||||
|
the terms of this License to the maximum extent possible; and (b)
|
||||||
|
describe the limitations and the code they affect. Such description must
|
||||||
|
be placed in a text file included with all distributions of the Covered
|
||||||
|
Software under this License. Except to the extent prohibited by statute
|
||||||
|
or regulation, such description must be sufficiently detailed for a
|
||||||
|
recipient of ordinary skill to be able to understand it.
|
||||||
|
|
||||||
|
5. Termination
|
||||||
|
--------------
|
||||||
|
|
||||||
|
5.1. The rights granted under this License will terminate automatically
|
||||||
|
if You fail to comply with any of its terms. However, if You become
|
||||||
|
compliant, then the rights granted under this License from a particular
|
||||||
|
Contributor are reinstated (a) provisionally, unless and until such
|
||||||
|
Contributor explicitly and finally terminates Your grants, and (b) on an
|
||||||
|
ongoing basis, if such Contributor fails to notify You of the
|
||||||
|
non-compliance by some reasonable means prior to 60 days after You have
|
||||||
|
come back into compliance. Moreover, Your grants from a particular
|
||||||
|
Contributor are reinstated on an ongoing basis if such Contributor
|
||||||
|
notifies You of the non-compliance by some reasonable means, this is the
|
||||||
|
first time You have received notice of non-compliance with this License
|
||||||
|
from such Contributor, and You become compliant prior to 30 days after
|
||||||
|
Your receipt of the notice.
|
||||||
|
|
||||||
|
5.2. If You initiate litigation against any entity by asserting a patent
|
||||||
|
infringement claim (excluding declaratory judgment actions,
|
||||||
|
counter-claims, and cross-claims) alleging that a Contributor Version
|
||||||
|
directly or indirectly infringes any patent, then the rights granted to
|
||||||
|
You by any and all Contributors for the Covered Software under Section
|
||||||
|
2.1 of this License shall terminate.
|
||||||
|
|
||||||
|
5.3. In the event of termination under Sections 5.1 or 5.2 above, all
|
||||||
|
end user license agreements (excluding distributors and resellers) which
|
||||||
|
have been validly granted by You or Your distributors under this License
|
||||||
|
prior to termination shall survive termination.
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 6. Disclaimer of Warranty *
|
||||||
|
* ------------------------- *
|
||||||
|
* *
|
||||||
|
* Covered Software is provided under this License on an "as is" *
|
||||||
|
* basis, without warranty of any kind, either expressed, implied, or *
|
||||||
|
* statutory, including, without limitation, warranties that the *
|
||||||
|
* Covered Software is free of defects, merchantable, fit for a *
|
||||||
|
* particular purpose or non-infringing. The entire risk as to the *
|
||||||
|
* quality and performance of the Covered Software is with You. *
|
||||||
|
* Should any Covered Software prove defective in any respect, You *
|
||||||
|
* (not any Contributor) assume the cost of any necessary servicing, *
|
||||||
|
* repair, or correction. This disclaimer of warranty constitutes an *
|
||||||
|
* essential part of this License. No use of any Covered Software is *
|
||||||
|
* authorized under this License except under this disclaimer. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
************************************************************************
|
||||||
|
* *
|
||||||
|
* 7. Limitation of Liability *
|
||||||
|
* -------------------------- *
|
||||||
|
* *
|
||||||
|
* Under no circumstances and under no legal theory, whether tort *
|
||||||
|
* (including negligence), contract, or otherwise, shall any *
|
||||||
|
* Contributor, or anyone who distributes Covered Software as *
|
||||||
|
* permitted above, be liable to You for any direct, indirect, *
|
||||||
|
* special, incidental, or consequential damages of any character *
|
||||||
|
* including, without limitation, damages for lost profits, loss of *
|
||||||
|
* goodwill, work stoppage, computer failure or malfunction, or any *
|
||||||
|
* and all other commercial damages or losses, even if such party *
|
||||||
|
* shall have been informed of the possibility of such damages. This *
|
||||||
|
* limitation of liability shall not apply to liability for death or *
|
||||||
|
* personal injury resulting from such party's negligence to the *
|
||||||
|
* extent applicable law prohibits such limitation. Some *
|
||||||
|
* jurisdictions do not allow the exclusion or limitation of *
|
||||||
|
* incidental or consequential damages, so this exclusion and *
|
||||||
|
* limitation may not apply to You. *
|
||||||
|
* *
|
||||||
|
************************************************************************
|
||||||
|
|
||||||
|
8. Litigation
|
||||||
|
-------------
|
||||||
|
|
||||||
|
Any litigation relating to this License may be brought only in the
|
||||||
|
courts of a jurisdiction where the defendant maintains its principal
|
||||||
|
place of business and such litigation shall be governed by laws of that
|
||||||
|
jurisdiction, without reference to its conflict-of-law provisions.
|
||||||
|
Nothing in this Section shall prevent a party's ability to bring
|
||||||
|
cross-claims or counter-claims.
|
||||||
|
|
||||||
|
9. Miscellaneous
|
||||||
|
----------------
|
||||||
|
|
||||||
|
This License represents the complete agreement concerning the subject
|
||||||
|
matter hereof. If any provision of this License is held to be
|
||||||
|
unenforceable, such provision shall be reformed only to the extent
|
||||||
|
necessary to make it enforceable. Any law or regulation which provides
|
||||||
|
that the language of a contract shall be construed against the drafter
|
||||||
|
shall not be used to construe this License against a Contributor.
|
||||||
|
|
||||||
|
10. Versions of the License
|
||||||
|
---------------------------
|
||||||
|
|
||||||
|
10.1. New Versions
|
||||||
|
|
||||||
|
Mozilla Foundation is the license steward. Except as provided in Section
|
||||||
|
10.3, no one other than the license steward has the right to modify or
|
||||||
|
publish new versions of this License. Each version will be given a
|
||||||
|
distinguishing version number.
|
||||||
|
|
||||||
|
10.2. Effect of New Versions
|
||||||
|
|
||||||
|
You may distribute the Covered Software under the terms of the version
|
||||||
|
of the License under which You originally received the Covered Software,
|
||||||
|
or under the terms of any subsequent version published by the license
|
||||||
|
steward.
|
||||||
|
|
||||||
|
10.3. Modified Versions
|
||||||
|
|
||||||
|
If you create software not governed by this License, and you want to
|
||||||
|
create a new license for such software, you may create and use a
|
||||||
|
modified version of this License if you rename the license and remove
|
||||||
|
any references to the name of the license steward (except to note that
|
||||||
|
such modified license differs from this License).
|
||||||
|
|
||||||
|
10.4. Distributing Source Code Form that is Incompatible With Secondary
|
||||||
|
Licenses
|
||||||
|
|
||||||
|
If You choose to distribute Source Code Form that is Incompatible With
|
||||||
|
Secondary Licenses under the terms of this version of the License, the
|
||||||
|
notice described in Exhibit B of this License must be attached.
|
||||||
|
|
||||||
|
Exhibit A - Source Code Form License Notice
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||||
|
file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||||
|
|
||||||
|
If it is not possible or desirable to put the notice in a particular
|
||||||
|
file, then You may include the notice in a location (such as a LICENSE
|
||||||
|
file in a relevant directory) where a recipient would be likely to look
|
||||||
|
for such a notice.
|
||||||
|
|
||||||
|
You may add additional accurate notices of copyright ownership.
|
||||||
|
|
||||||
|
Exhibit B - "Incompatible With Secondary Licenses" Notice
|
||||||
|
---------------------------------------------------------
|
||||||
|
|
||||||
|
This Source Code Form is "Incompatible With Secondary Licenses", as
|
||||||
|
defined by the Mozilla Public License, v. 2.0.
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/oracle/oci-go-sdk/v36/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Artifact is an artifact implementation that contains a built Custom Image.
|
||||||
|
type Artifact struct {
|
||||||
|
Image core.Image
|
||||||
|
Region string
|
||||||
|
driver Driver
|
||||||
|
|
||||||
|
// StateData should store data such as GeneratedData
|
||||||
|
// to be shared with post-processors
|
||||||
|
StateData map[string]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BuilderId uniquely identifies the builder.
|
||||||
|
func (a *Artifact) BuilderId() string {
|
||||||
|
return BuilderId
|
||||||
|
}
|
||||||
|
|
||||||
|
// Files lists the files associated with an artifact. We don't have any files
|
||||||
|
// as the custom image is stored server side.
|
||||||
|
func (a *Artifact) Files() []string {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Id returns the OCID of the associated Image.
|
||||||
|
func (a *Artifact) Id() string {
|
||||||
|
return *a.Image.Id
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *Artifact) String() string {
|
||||||
|
var displayName string
|
||||||
|
if a.Image.DisplayName != nil {
|
||||||
|
displayName = *a.Image.DisplayName
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"An image was created: '%v' (OCID: %v) in region '%v'",
|
||||||
|
displayName, *a.Image.Id, a.Region,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// State ...
|
||||||
|
func (a *Artifact) State(name string) interface{} {
|
||||||
|
return a.StateData[name]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Destroy deletes the custom image associated with the artifact.
|
||||||
|
func (a *Artifact) Destroy() error {
|
||||||
|
return a.driver.DeleteImage(context.TODO(), *a.Image.Id)
|
||||||
|
}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestArtifactImpl(t *testing.T) {
|
||||||
|
var raw interface{}
|
||||||
|
raw = &Artifact{}
|
||||||
|
if _, ok := raw.(packersdk.Artifact); !ok {
|
||||||
|
t.Fatalf("Artifact should be artifact")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
// Package oci contains a packersdk.Builder implementation that builds Oracle
|
||||||
|
// Bare Metal Cloud Services (OCI) images.
|
||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"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"
|
||||||
|
ocommon "github.com/hashicorp/packer/builder/oracle/common"
|
||||||
|
"github.com/oracle/oci-go-sdk/v36/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// BuilderId uniquely identifies the builder
|
||||||
|
const BuilderId = "packer.oracle.oci"
|
||||||
|
|
||||||
|
// OCI API version
|
||||||
|
const ociAPIVersion = "20160918"
|
||||||
|
|
||||||
|
// Builder is a builder implementation that creates Oracle OCI custom images.
|
||||||
|
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) {
|
||||||
|
err := b.config.Prepare(raws...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *Builder) Run(ctx context.Context, ui packersdk.Ui, hook packersdk.Hook) (packersdk.Artifact, error) {
|
||||||
|
driver, err := NewDriverOCI(&b.config)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Populate the state bag
|
||||||
|
state := new(multistep.BasicStateBag)
|
||||||
|
state.Put("config", &b.config)
|
||||||
|
state.Put("driver", driver)
|
||||||
|
state.Put("hook", hook)
|
||||||
|
state.Put("ui", ui)
|
||||||
|
|
||||||
|
// Build the steps
|
||||||
|
steps := []multistep.Step{
|
||||||
|
&ocommon.StepKeyPair{
|
||||||
|
Debug: b.config.PackerDebug,
|
||||||
|
Comm: &b.config.Comm,
|
||||||
|
DebugKeyPath: fmt.Sprintf("oci_%s.pem", b.config.PackerBuildName),
|
||||||
|
},
|
||||||
|
&stepCreateInstance{},
|
||||||
|
&stepInstanceInfo{},
|
||||||
|
&stepGetDefaultCredentials{
|
||||||
|
Debug: b.config.PackerDebug,
|
||||||
|
Comm: &b.config.Comm,
|
||||||
|
BuildName: b.config.PackerBuildName,
|
||||||
|
},
|
||||||
|
&communicator.StepConnect{
|
||||||
|
Config: &b.config.Comm,
|
||||||
|
Host: communicator.CommHost(b.config.Comm.Host(), "instance_ip"),
|
||||||
|
SSHConfig: b.config.Comm.SSHConfigFunc(),
|
||||||
|
},
|
||||||
|
&commonsteps.StepProvision{},
|
||||||
|
&commonsteps.StepCleanupTempKeys{
|
||||||
|
Comm: &b.config.Comm,
|
||||||
|
},
|
||||||
|
&stepImage{},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run the steps
|
||||||
|
b.runner = commonsteps.NewRunnerWithPauseFn(steps, b.config.PackerConfig, ui, state)
|
||||||
|
b.runner.Run(ctx, state)
|
||||||
|
|
||||||
|
// If there was an error, return that
|
||||||
|
if rawErr, ok := state.GetOk("error"); ok {
|
||||||
|
return nil, rawErr.(error)
|
||||||
|
}
|
||||||
|
|
||||||
|
region, err := b.config.configProvider.Region()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
image, ok := state.GetOk("image")
|
||||||
|
if !ok {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build the artifact and return it
|
||||||
|
artifact := &Artifact{
|
||||||
|
Image: image.(core.Image),
|
||||||
|
Region: region,
|
||||||
|
driver: driver,
|
||||||
|
StateData: map[string]interface{}{"generated_data": state.Get("generated_data")},
|
||||||
|
}
|
||||||
|
|
||||||
|
return artifact, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cancel terminates a running build.
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestBuilder_ImplementsBuilder(t *testing.T) {
|
||||||
|
var raw interface{}
|
||||||
|
raw = &Builder{}
|
||||||
|
if _, ok := raw.(packersdk.Builder); !ok {
|
||||||
|
t.Fatalf("Builder should be a builder")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,415 @@
|
|||||||
|
//go:generate packer-sdc mapstructure-to-hcl2 -type Config,CreateVNICDetails,ListImagesRequest,FlexShapeConfig
|
||||||
|
|
||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/user"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"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/pathing"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/template/config"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
|
||||||
|
ocicommon "github.com/oracle/oci-go-sdk/v36/common"
|
||||||
|
ociauth "github.com/oracle/oci-go-sdk/v36/common/auth"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CreateVNICDetails struct {
|
||||||
|
// fields that can be specified under "create_vnic_details"
|
||||||
|
AssignPublicIp *bool `mapstructure:"assign_public_ip" required:"false"`
|
||||||
|
DefinedTags map[string]map[string]interface{} `mapstructure:"defined_tags" required:"false"`
|
||||||
|
DisplayName *string `mapstructure:"display_name" required:"false"`
|
||||||
|
FreeformTags map[string]string `mapstructure:"tags" required:"false"`
|
||||||
|
HostnameLabel *string `mapstructure:"hostname_label" required:"false"`
|
||||||
|
NsgIds []string `mapstructure:"nsg_ids" required:"false"`
|
||||||
|
PrivateIp *string `mapstructure:"private_ip" required:"false"`
|
||||||
|
SkipSourceDestCheck *bool `mapstructure:"skip_source_dest_check" required:"false"`
|
||||||
|
SubnetId *string `mapstructure:"subnet_id" required:"false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListImagesRequest struct {
|
||||||
|
// fields that can be specified under "base_image_filter"
|
||||||
|
CompartmentId *string `mapstructure:"compartment_id"`
|
||||||
|
DisplayName *string `mapstructure:"display_name"`
|
||||||
|
DisplayNameSearch *string `mapstructure:"display_name_search"`
|
||||||
|
OperatingSystem *string `mapstructure:"operating_system"`
|
||||||
|
OperatingSystemVersion *string `mapstructure:"operating_system_version"`
|
||||||
|
Shape *string `mapstructure:"shape"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FlexShapeConfig struct {
|
||||||
|
Ocpus *float32 `mapstructure:"ocpus" required:"false"`
|
||||||
|
MemoryInGBs *float32 `mapstructure:"memory_in_gbs" required:"false"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
common.PackerConfig `mapstructure:",squash"`
|
||||||
|
Comm communicator.Config `mapstructure:",squash"`
|
||||||
|
|
||||||
|
configProvider ocicommon.ConfigurationProvider
|
||||||
|
|
||||||
|
// Instance Principals (OPTIONAL)
|
||||||
|
// If set to true the following can't have non empty values
|
||||||
|
// - AccessCfgFile
|
||||||
|
// - AccessCfgFileAccount
|
||||||
|
// - UserID
|
||||||
|
// - TenancyID
|
||||||
|
// - Region
|
||||||
|
// - Fingerprint
|
||||||
|
// - KeyFile
|
||||||
|
// - PassPhrase
|
||||||
|
InstancePrincipals bool `mapstructure:"use_instance_principals"`
|
||||||
|
|
||||||
|
AccessCfgFile string `mapstructure:"access_cfg_file"`
|
||||||
|
AccessCfgFileAccount string `mapstructure:"access_cfg_file_account"`
|
||||||
|
|
||||||
|
// Access config overrides
|
||||||
|
UserID string `mapstructure:"user_ocid"`
|
||||||
|
TenancyID string `mapstructure:"tenancy_ocid"`
|
||||||
|
Region string `mapstructure:"region"`
|
||||||
|
Fingerprint string `mapstructure:"fingerprint"`
|
||||||
|
KeyFile string `mapstructure:"key_file"`
|
||||||
|
PassPhrase string `mapstructure:"pass_phrase"`
|
||||||
|
UsePrivateIP bool `mapstructure:"use_private_ip"`
|
||||||
|
|
||||||
|
AvailabilityDomain string `mapstructure:"availability_domain"`
|
||||||
|
CompartmentID string `mapstructure:"compartment_ocid"`
|
||||||
|
|
||||||
|
// Image
|
||||||
|
BaseImageID string `mapstructure:"base_image_ocid"`
|
||||||
|
BaseImageFilter ListImagesRequest `mapstructure:"base_image_filter"`
|
||||||
|
ImageName string `mapstructure:"image_name"`
|
||||||
|
ImageCompartmentID string `mapstructure:"image_compartment_ocid"`
|
||||||
|
LaunchMode string `mapstructure:"image_launch_mode"`
|
||||||
|
|
||||||
|
// Instance
|
||||||
|
InstanceName *string `mapstructure:"instance_name"`
|
||||||
|
InstanceTags map[string]string `mapstructure:"instance_tags"`
|
||||||
|
InstanceDefinedTags map[string]map[string]interface{} `mapstructure:"instance_defined_tags"`
|
||||||
|
Shape string `mapstructure:"shape"`
|
||||||
|
ShapeConfig FlexShapeConfig `mapstructure:"shape_config"`
|
||||||
|
BootVolumeSizeInGBs int64 `mapstructure:"disk_size"`
|
||||||
|
|
||||||
|
// Metadata optionally contains custom metadata key/value pairs provided in the
|
||||||
|
// configuration. While this can be used to set metadata["user_data"] the explicit
|
||||||
|
// "user_data" and "user_data_file" values will have precedence.
|
||||||
|
// An instance's metadata can be obtained from at http://169.254.169.254 on the
|
||||||
|
// launched instance.
|
||||||
|
Metadata map[string]string `mapstructure:"metadata"`
|
||||||
|
|
||||||
|
// UserData and UserDataFile file are both optional and mutually exclusive.
|
||||||
|
UserData string `mapstructure:"user_data"`
|
||||||
|
UserDataFile string `mapstructure:"user_data_file"`
|
||||||
|
|
||||||
|
// Networking
|
||||||
|
SubnetID string `mapstructure:"subnet_ocid"`
|
||||||
|
CreateVnicDetails CreateVNICDetails `mapstructure:"create_vnic_details"`
|
||||||
|
|
||||||
|
// Tagging
|
||||||
|
Tags map[string]string `mapstructure:"tags"`
|
||||||
|
DefinedTags map[string]map[string]interface{} `mapstructure:"defined_tags"`
|
||||||
|
|
||||||
|
ctx interpolate.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) ConfigProvider() ocicommon.ConfigurationProvider {
|
||||||
|
return c.configProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Config) Prepare(raws ...interface{}) error {
|
||||||
|
|
||||||
|
// Decode from template
|
||||||
|
err := config.Decode(c, &config.DecodeOpts{
|
||||||
|
Interpolate: true,
|
||||||
|
InterpolateContext: &c.ctx,
|
||||||
|
}, raws...)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("Failed to mapstructure Config: %+v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var errs *packersdk.MultiError
|
||||||
|
if es := c.Comm.Prepare(&c.ctx); len(es) > 0 {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, es...)
|
||||||
|
}
|
||||||
|
|
||||||
|
var tenancyOCID string
|
||||||
|
|
||||||
|
if c.InstancePrincipals {
|
||||||
|
// We could go through all keys in one go and report that the below set
|
||||||
|
// of keys cannot coexist with use_instance_principals but decided to
|
||||||
|
// split them and report them seperately so that the user sees the specific
|
||||||
|
// key involved.
|
||||||
|
var message string = " cannot be present when use_instance_principals is set to true."
|
||||||
|
if c.AccessCfgFile != "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("access_cfg_file"+message))
|
||||||
|
}
|
||||||
|
if c.AccessCfgFileAccount != "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("access_cfg_file_account"+message))
|
||||||
|
}
|
||||||
|
if c.UserID != "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("user_ocid"+message))
|
||||||
|
}
|
||||||
|
if c.TenancyID != "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("tenancy_ocid"+message))
|
||||||
|
}
|
||||||
|
if c.Region != "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("region"+message))
|
||||||
|
}
|
||||||
|
if c.Fingerprint != "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("fingerprint"+message))
|
||||||
|
}
|
||||||
|
if c.KeyFile != "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("key_file"+message))
|
||||||
|
}
|
||||||
|
if c.PassPhrase != "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("pass_phrase"+message))
|
||||||
|
}
|
||||||
|
// This check is used to facilitate testing. During testing a Mock struct
|
||||||
|
// is assigned to c.configProvider otherwise testing fails because Instance
|
||||||
|
// Principals cannot be obtained.
|
||||||
|
if c.configProvider == nil {
|
||||||
|
// Even though the previous configuraion checks might fail we don't want
|
||||||
|
// to skip this step. It seems that the logic behind the checks in this
|
||||||
|
// file is to check everything even getting the configProvider.
|
||||||
|
c.configProvider, err = ociauth.InstancePrincipalConfigurationProvider()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tenancyOCID, err = c.configProvider.TenancyOCID()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Determine where the SDK config is located
|
||||||
|
if c.AccessCfgFile == "" {
|
||||||
|
c.AccessCfgFile, err = getDefaultOCISettingsPath()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Default OCI settings file not found")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.AccessCfgFileAccount == "" {
|
||||||
|
c.AccessCfgFileAccount = "DEFAULT"
|
||||||
|
}
|
||||||
|
|
||||||
|
var keyContent []byte
|
||||||
|
if c.KeyFile != "" {
|
||||||
|
path, err := pathing.ExpandUser(c.KeyFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read API signing key
|
||||||
|
keyContent, err = ioutil.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fileProvider, _ := ocicommon.ConfigurationProviderFromFileWithProfile(c.AccessCfgFile, c.AccessCfgFileAccount, c.PassPhrase)
|
||||||
|
if c.Region == "" {
|
||||||
|
var region string
|
||||||
|
if fileProvider != nil {
|
||||||
|
region, _ = fileProvider.Region()
|
||||||
|
}
|
||||||
|
if region == "" {
|
||||||
|
c.Region = "us-phoenix-1"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
providers := []ocicommon.ConfigurationProvider{
|
||||||
|
ocicommon.NewRawConfigurationProvider(c.TenancyID, c.UserID, c.Region, c.Fingerprint, string(keyContent), &c.PassPhrase),
|
||||||
|
}
|
||||||
|
|
||||||
|
if fileProvider != nil {
|
||||||
|
providers = append(providers, fileProvider)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load API access configuration from SDK
|
||||||
|
configProvider, err := ocicommon.ComposingConfigurationProvider(providers)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if userOCID, _ := configProvider.UserOCID(); userOCID == "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("'user_ocid' must be specified"))
|
||||||
|
}
|
||||||
|
|
||||||
|
tenancyOCID, _ = configProvider.TenancyOCID()
|
||||||
|
if tenancyOCID == "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("'tenancy_ocid' must be specified"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if fingerprint, _ := configProvider.KeyFingerprint(); fingerprint == "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("'fingerprint' must be specified"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := configProvider.PrivateRSAKey(); err != nil {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, fmt.Errorf("'key_file' must be correctly specified. %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
c.configProvider = configProvider
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.AvailabilityDomain == "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("'availability_domain' must be specified"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.CompartmentID == "" && tenancyOCID != "" {
|
||||||
|
c.CompartmentID = tenancyOCID
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ImageCompartmentID == "" {
|
||||||
|
c.ImageCompartmentID = c.CompartmentID
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.Shape == "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("'shape' must be specified"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if strings.HasSuffix(c.Shape, "Flex") {
|
||||||
|
if c.ShapeConfig.Ocpus == nil {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("'Ocpus' must be specified when using flexible shapes"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ShapeConfig.MemoryInGBs != nil && c.ShapeConfig.Ocpus == nil {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("'Ocpus' must be specified if memory_in_gbs is specified"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.SubnetID == "") && (c.CreateVnicDetails.SubnetId == nil) {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("'subnet_ocid' must be specified"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.CreateVnicDetails.SubnetId == nil {
|
||||||
|
c.CreateVnicDetails.SubnetId = &c.SubnetID
|
||||||
|
} else if (*c.CreateVnicDetails.SubnetId != c.SubnetID) && (c.SubnetID != "") {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("'create_vnic_details[subnet]' must match 'subnet_ocid' if both are specified"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if (c.BaseImageID == "") && (c.BaseImageFilter == ListImagesRequest{}) {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("'base_image_ocid' or 'base_image_filter' must be specified"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.BaseImageFilter.CompartmentId == nil {
|
||||||
|
c.BaseImageFilter.CompartmentId = &c.CompartmentID
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.BaseImageFilter.Shape == nil {
|
||||||
|
c.BaseImageFilter.Shape = &c.Shape
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate tag lengths. TODO (hlowndes) maximum number of tags allowed.
|
||||||
|
if c.Tags != nil {
|
||||||
|
for k, v := range c.Tags {
|
||||||
|
k = strings.TrimSpace(k)
|
||||||
|
v = strings.TrimSpace(v)
|
||||||
|
if len(k) > 100 {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, fmt.Errorf("Tag key length too long. Maximum 100 but found %d. Key: %s", len(k), k))
|
||||||
|
}
|
||||||
|
if len(k) == 0 {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("Tag key empty in config"))
|
||||||
|
}
|
||||||
|
if len(v) > 100 {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, fmt.Errorf("Tag value length too long. Maximum 100 but found %d. Key: %s", len(v), k))
|
||||||
|
}
|
||||||
|
if len(v) == 0 {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("Tag value empty in config"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.ImageName == "" {
|
||||||
|
name, err := interpolate.Render("packer-{{timestamp}}", nil)
|
||||||
|
if err != nil {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs,
|
||||||
|
fmt.Errorf("unable to parse image name: %s", err))
|
||||||
|
} else {
|
||||||
|
c.ImageName = name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Optional UserData config
|
||||||
|
if c.UserData != "" && c.UserDataFile != "" {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("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, fmt.Errorf("user_data_file not found: %s", c.UserDataFile))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// read UserDataFile into string.
|
||||||
|
if c.UserDataFile != "" {
|
||||||
|
fiData, err := ioutil.ReadFile(c.UserDataFile)
|
||||||
|
if err != nil {
|
||||||
|
errs = packersdk.MultiErrorAppend(errs, fmt.Errorf("Problem reading user_data_file: %s", err))
|
||||||
|
}
|
||||||
|
c.UserData = string(fiData)
|
||||||
|
}
|
||||||
|
// Test if UserData is encoded already, and if not, encode it
|
||||||
|
if c.UserData != "" {
|
||||||
|
if _, err := base64.StdEncoding.DecodeString(c.UserData); err != nil {
|
||||||
|
log.Printf("[DEBUG] base64 encoding user data...")
|
||||||
|
c.UserData = base64.StdEncoding.EncodeToString([]byte(c.UserData))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set default boot volume size to 50 if not set
|
||||||
|
// Check if size set is allowed by OCI
|
||||||
|
if c.BootVolumeSizeInGBs == 0 {
|
||||||
|
c.BootVolumeSizeInGBs = 50
|
||||||
|
} else if c.BootVolumeSizeInGBs < 50 || c.BootVolumeSizeInGBs > 16384 {
|
||||||
|
errs = packersdk.MultiErrorAppend(
|
||||||
|
errs, errors.New("'disk_size' must be between 50 and 16384 GBs"))
|
||||||
|
}
|
||||||
|
|
||||||
|
if errs != nil && len(errs.Errors) > 0 {
|
||||||
|
return errs
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultOCISettingsPath uses os/user to compute the default
|
||||||
|
// config file location ($HOME/.oci/config).
|
||||||
|
func getDefaultOCISettingsPath() (string, error) {
|
||||||
|
u, err := user.Current()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if u.HomeDir == "" {
|
||||||
|
return "", fmt.Errorf("Unable to determine the home directory for the current user.")
|
||||||
|
}
|
||||||
|
|
||||||
|
path := filepath.Join(u.HomeDir, ".oci", "config")
|
||||||
|
if _, err := os.Stat(path); err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return path, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,300 @@
|
|||||||
|
// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT.
|
||||||
|
|
||||||
|
package oci
|
||||||
|
|
||||||
|
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"`
|
||||||
|
InstancePrincipals *bool `mapstructure:"use_instance_principals" cty:"use_instance_principals" hcl:"use_instance_principals"`
|
||||||
|
AccessCfgFile *string `mapstructure:"access_cfg_file" cty:"access_cfg_file" hcl:"access_cfg_file"`
|
||||||
|
AccessCfgFileAccount *string `mapstructure:"access_cfg_file_account" cty:"access_cfg_file_account" hcl:"access_cfg_file_account"`
|
||||||
|
UserID *string `mapstructure:"user_ocid" cty:"user_ocid" hcl:"user_ocid"`
|
||||||
|
TenancyID *string `mapstructure:"tenancy_ocid" cty:"tenancy_ocid" hcl:"tenancy_ocid"`
|
||||||
|
Region *string `mapstructure:"region" cty:"region" hcl:"region"`
|
||||||
|
Fingerprint *string `mapstructure:"fingerprint" cty:"fingerprint" hcl:"fingerprint"`
|
||||||
|
KeyFile *string `mapstructure:"key_file" cty:"key_file" hcl:"key_file"`
|
||||||
|
PassPhrase *string `mapstructure:"pass_phrase" cty:"pass_phrase" hcl:"pass_phrase"`
|
||||||
|
UsePrivateIP *bool `mapstructure:"use_private_ip" cty:"use_private_ip" hcl:"use_private_ip"`
|
||||||
|
AvailabilityDomain *string `mapstructure:"availability_domain" cty:"availability_domain" hcl:"availability_domain"`
|
||||||
|
CompartmentID *string `mapstructure:"compartment_ocid" cty:"compartment_ocid" hcl:"compartment_ocid"`
|
||||||
|
BaseImageID *string `mapstructure:"base_image_ocid" cty:"base_image_ocid" hcl:"base_image_ocid"`
|
||||||
|
BaseImageFilter *FlatListImagesRequest `mapstructure:"base_image_filter" cty:"base_image_filter" hcl:"base_image_filter"`
|
||||||
|
ImageName *string `mapstructure:"image_name" cty:"image_name" hcl:"image_name"`
|
||||||
|
ImageCompartmentID *string `mapstructure:"image_compartment_ocid" cty:"image_compartment_ocid" hcl:"image_compartment_ocid"`
|
||||||
|
LaunchMode *string `mapstructure:"image_launch_mode" cty:"image_launch_mode" hcl:"image_launch_mode"`
|
||||||
|
InstanceName *string `mapstructure:"instance_name" cty:"instance_name" hcl:"instance_name"`
|
||||||
|
InstanceTags map[string]string `mapstructure:"instance_tags" cty:"instance_tags" hcl:"instance_tags"`
|
||||||
|
InstanceDefinedTags map[string]map[string]interface{} `mapstructure:"instance_defined_tags" cty:"instance_defined_tags" hcl:"instance_defined_tags"`
|
||||||
|
Shape *string `mapstructure:"shape" cty:"shape" hcl:"shape"`
|
||||||
|
ShapeConfig *FlatFlexShapeConfig `mapstructure:"shape_config" cty:"shape_config" hcl:"shape_config"`
|
||||||
|
BootVolumeSizeInGBs *int64 `mapstructure:"disk_size" cty:"disk_size" hcl:"disk_size"`
|
||||||
|
Metadata map[string]string `mapstructure:"metadata" cty:"metadata" hcl:"metadata"`
|
||||||
|
UserData *string `mapstructure:"user_data" cty:"user_data" hcl:"user_data"`
|
||||||
|
UserDataFile *string `mapstructure:"user_data_file" cty:"user_data_file" hcl:"user_data_file"`
|
||||||
|
SubnetID *string `mapstructure:"subnet_ocid" cty:"subnet_ocid" hcl:"subnet_ocid"`
|
||||||
|
CreateVnicDetails *FlatCreateVNICDetails `mapstructure:"create_vnic_details" cty:"create_vnic_details" hcl:"create_vnic_details"`
|
||||||
|
Tags map[string]string `mapstructure:"tags" cty:"tags" hcl:"tags"`
|
||||||
|
DefinedTags map[string]map[string]interface{} `mapstructure:"defined_tags" cty:"defined_tags" hcl:"defined_tags"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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},
|
||||||
|
"use_instance_principals": &hcldec.AttrSpec{Name: "use_instance_principals", Type: cty.Bool, Required: false},
|
||||||
|
"access_cfg_file": &hcldec.AttrSpec{Name: "access_cfg_file", Type: cty.String, Required: false},
|
||||||
|
"access_cfg_file_account": &hcldec.AttrSpec{Name: "access_cfg_file_account", Type: cty.String, Required: false},
|
||||||
|
"user_ocid": &hcldec.AttrSpec{Name: "user_ocid", Type: cty.String, Required: false},
|
||||||
|
"tenancy_ocid": &hcldec.AttrSpec{Name: "tenancy_ocid", Type: cty.String, Required: false},
|
||||||
|
"region": &hcldec.AttrSpec{Name: "region", Type: cty.String, Required: false},
|
||||||
|
"fingerprint": &hcldec.AttrSpec{Name: "fingerprint", Type: cty.String, Required: false},
|
||||||
|
"key_file": &hcldec.AttrSpec{Name: "key_file", Type: cty.String, Required: false},
|
||||||
|
"pass_phrase": &hcldec.AttrSpec{Name: "pass_phrase", Type: cty.String, Required: false},
|
||||||
|
"use_private_ip": &hcldec.AttrSpec{Name: "use_private_ip", Type: cty.Bool, Required: false},
|
||||||
|
"availability_domain": &hcldec.AttrSpec{Name: "availability_domain", Type: cty.String, Required: false},
|
||||||
|
"compartment_ocid": &hcldec.AttrSpec{Name: "compartment_ocid", Type: cty.String, Required: false},
|
||||||
|
"base_image_ocid": &hcldec.AttrSpec{Name: "base_image_ocid", Type: cty.String, Required: false},
|
||||||
|
"base_image_filter": &hcldec.BlockSpec{TypeName: "base_image_filter", Nested: hcldec.ObjectSpec((*FlatListImagesRequest)(nil).HCL2Spec())},
|
||||||
|
"image_name": &hcldec.AttrSpec{Name: "image_name", Type: cty.String, Required: false},
|
||||||
|
"image_compartment_ocid": &hcldec.AttrSpec{Name: "image_compartment_ocid", Type: cty.String, Required: false},
|
||||||
|
"image_launch_mode": &hcldec.AttrSpec{Name: "image_launch_mode", Type: cty.String, Required: false},
|
||||||
|
"instance_name": &hcldec.AttrSpec{Name: "instance_name", Type: cty.String, Required: false},
|
||||||
|
"instance_tags": &hcldec.AttrSpec{Name: "instance_tags", Type: cty.Map(cty.String), Required: false},
|
||||||
|
"instance_defined_tags": &hcldec.AttrSpec{Name: "instance_defined_tags", Type: cty.Map(cty.String), Required: false},
|
||||||
|
"shape": &hcldec.AttrSpec{Name: "shape", Type: cty.String, Required: false},
|
||||||
|
"shape_config": &hcldec.BlockSpec{TypeName: "shape_config", Nested: hcldec.ObjectSpec((*FlatFlexShapeConfig)(nil).HCL2Spec())},
|
||||||
|
"disk_size": &hcldec.AttrSpec{Name: "disk_size", Type: cty.Number, Required: false},
|
||||||
|
"metadata": &hcldec.AttrSpec{Name: "metadata", Type: cty.Map(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},
|
||||||
|
"subnet_ocid": &hcldec.AttrSpec{Name: "subnet_ocid", Type: cty.String, Required: false},
|
||||||
|
"create_vnic_details": &hcldec.BlockSpec{TypeName: "create_vnic_details", Nested: hcldec.ObjectSpec((*FlatCreateVNICDetails)(nil).HCL2Spec())},
|
||||||
|
"tags": &hcldec.AttrSpec{Name: "tags", Type: cty.Map(cty.String), Required: false},
|
||||||
|
"defined_tags": &hcldec.AttrSpec{Name: "defined_tags", Type: cty.Map(cty.String), Required: false},
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatCreateVNICDetails is an auto-generated flat version of CreateVNICDetails.
|
||||||
|
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
type FlatCreateVNICDetails struct {
|
||||||
|
AssignPublicIp *bool `mapstructure:"assign_public_ip" required:"false" cty:"assign_public_ip" hcl:"assign_public_ip"`
|
||||||
|
DefinedTags map[string]map[string]interface{} `mapstructure:"defined_tags" required:"false" cty:"defined_tags" hcl:"defined_tags"`
|
||||||
|
DisplayName *string `mapstructure:"display_name" required:"false" cty:"display_name" hcl:"display_name"`
|
||||||
|
FreeformTags map[string]string `mapstructure:"tags" required:"false" cty:"tags" hcl:"tags"`
|
||||||
|
HostnameLabel *string `mapstructure:"hostname_label" required:"false" cty:"hostname_label" hcl:"hostname_label"`
|
||||||
|
NsgIds []string `mapstructure:"nsg_ids" required:"false" cty:"nsg_ids" hcl:"nsg_ids"`
|
||||||
|
PrivateIp *string `mapstructure:"private_ip" required:"false" cty:"private_ip" hcl:"private_ip"`
|
||||||
|
SkipSourceDestCheck *bool `mapstructure:"skip_source_dest_check" required:"false" cty:"skip_source_dest_check" hcl:"skip_source_dest_check"`
|
||||||
|
SubnetId *string `mapstructure:"subnet_id" required:"false" cty:"subnet_id" hcl:"subnet_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatMapstructure returns a new FlatCreateVNICDetails.
|
||||||
|
// FlatCreateVNICDetails is an auto-generated flat version of CreateVNICDetails.
|
||||||
|
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
func (*CreateVNICDetails) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
|
||||||
|
return new(FlatCreateVNICDetails)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HCL2Spec returns the hcl spec of a CreateVNICDetails.
|
||||||
|
// This spec is used by HCL to read the fields of CreateVNICDetails.
|
||||||
|
// The decoded values from this spec will then be applied to a FlatCreateVNICDetails.
|
||||||
|
func (*FlatCreateVNICDetails) HCL2Spec() map[string]hcldec.Spec {
|
||||||
|
s := map[string]hcldec.Spec{
|
||||||
|
"assign_public_ip": &hcldec.AttrSpec{Name: "assign_public_ip", Type: cty.Bool, Required: false},
|
||||||
|
"defined_tags": &hcldec.AttrSpec{Name: "defined_tags", Type: cty.Map(cty.String), Required: false},
|
||||||
|
"display_name": &hcldec.AttrSpec{Name: "display_name", Type: cty.String, Required: false},
|
||||||
|
"tags": &hcldec.AttrSpec{Name: "tags", Type: cty.Map(cty.String), Required: false},
|
||||||
|
"hostname_label": &hcldec.AttrSpec{Name: "hostname_label", Type: cty.String, Required: false},
|
||||||
|
"nsg_ids": &hcldec.AttrSpec{Name: "nsg_ids", Type: cty.List(cty.String), Required: false},
|
||||||
|
"private_ip": &hcldec.AttrSpec{Name: "private_ip", Type: cty.String, Required: false},
|
||||||
|
"skip_source_dest_check": &hcldec.AttrSpec{Name: "skip_source_dest_check", Type: cty.Bool, Required: false},
|
||||||
|
"subnet_id": &hcldec.AttrSpec{Name: "subnet_id", Type: cty.String, Required: false},
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatFlexShapeConfig is an auto-generated flat version of FlexShapeConfig.
|
||||||
|
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
type FlatFlexShapeConfig struct {
|
||||||
|
Ocpus *float32 `mapstructure:"ocpus" required:"false" cty:"ocpus" hcl:"ocpus"`
|
||||||
|
MemoryInGBs *float32 `mapstructure:"memory_in_gbs" required:"false" cty:"memory_in_gbs" hcl:"memory_in_gbs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatMapstructure returns a new FlatFlexShapeConfig.
|
||||||
|
// FlatFlexShapeConfig is an auto-generated flat version of FlexShapeConfig.
|
||||||
|
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
func (*FlexShapeConfig) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
|
||||||
|
return new(FlatFlexShapeConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HCL2Spec returns the hcl spec of a FlexShapeConfig.
|
||||||
|
// This spec is used by HCL to read the fields of FlexShapeConfig.
|
||||||
|
// The decoded values from this spec will then be applied to a FlatFlexShapeConfig.
|
||||||
|
func (*FlatFlexShapeConfig) HCL2Spec() map[string]hcldec.Spec {
|
||||||
|
s := map[string]hcldec.Spec{
|
||||||
|
"ocpus": &hcldec.AttrSpec{Name: "ocpus", Type: cty.Number, Required: false},
|
||||||
|
"memory_in_gbs": &hcldec.AttrSpec{Name: "memory_in_gbs", Type: cty.Number, Required: false},
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatListImagesRequest is an auto-generated flat version of ListImagesRequest.
|
||||||
|
// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
type FlatListImagesRequest struct {
|
||||||
|
CompartmentId *string `mapstructure:"compartment_id" cty:"compartment_id" hcl:"compartment_id"`
|
||||||
|
DisplayName *string `mapstructure:"display_name" cty:"display_name" hcl:"display_name"`
|
||||||
|
DisplayNameSearch *string `mapstructure:"display_name_search" cty:"display_name_search" hcl:"display_name_search"`
|
||||||
|
OperatingSystem *string `mapstructure:"operating_system" cty:"operating_system" hcl:"operating_system"`
|
||||||
|
OperatingSystemVersion *string `mapstructure:"operating_system_version" cty:"operating_system_version" hcl:"operating_system_version"`
|
||||||
|
Shape *string `mapstructure:"shape" cty:"shape" hcl:"shape"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// FlatMapstructure returns a new FlatListImagesRequest.
|
||||||
|
// FlatListImagesRequest is an auto-generated flat version of ListImagesRequest.
|
||||||
|
// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up.
|
||||||
|
func (*ListImagesRequest) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } {
|
||||||
|
return new(FlatListImagesRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// HCL2Spec returns the hcl spec of a ListImagesRequest.
|
||||||
|
// This spec is used by HCL to read the fields of ListImagesRequest.
|
||||||
|
// The decoded values from this spec will then be applied to a FlatListImagesRequest.
|
||||||
|
func (*FlatListImagesRequest) HCL2Spec() map[string]hcldec.Spec {
|
||||||
|
s := map[string]hcldec.Spec{
|
||||||
|
"compartment_id": &hcldec.AttrSpec{Name: "compartment_id", Type: cty.String, Required: false},
|
||||||
|
"display_name": &hcldec.AttrSpec{Name: "display_name", Type: cty.String, Required: false},
|
||||||
|
"display_name_search": &hcldec.AttrSpec{Name: "display_name_search", Type: cty.String, Required: false},
|
||||||
|
"operating_system": &hcldec.AttrSpec{Name: "operating_system", Type: cty.String, Required: false},
|
||||||
|
"operating_system_version": &hcldec.AttrSpec{Name: "operating_system_version", Type: cty.String, Required: false},
|
||||||
|
"shape": &hcldec.AttrSpec{Name: "shape", Type: cty.String, Required: false},
|
||||||
|
}
|
||||||
|
return s
|
||||||
|
}
|
||||||
@@ -0,0 +1,412 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/pem"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/go-ini/ini"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testConfig(accessConfFile *os.File) map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
|
||||||
|
"availability_domain": "aaaa:PHX-AD-3",
|
||||||
|
"access_cfg_file": accessConfFile.Name(),
|
||||||
|
|
||||||
|
// Image
|
||||||
|
"base_image_ocid": "ocd1...",
|
||||||
|
"image_name": "HelloWorld",
|
||||||
|
|
||||||
|
// Networking
|
||||||
|
"subnet_ocid": "ocd1...",
|
||||||
|
|
||||||
|
// Comm
|
||||||
|
"ssh_username": "opc",
|
||||||
|
"use_private_ip": false,
|
||||||
|
"metadata": map[string]string{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
"defined_tags": map[string]map[string]interface{}{
|
||||||
|
"namespace": {"key": "value"},
|
||||||
|
},
|
||||||
|
|
||||||
|
// Instance Details
|
||||||
|
"instance_name": "hello-world",
|
||||||
|
"instance_tags": map[string]string{
|
||||||
|
"key": "value",
|
||||||
|
},
|
||||||
|
"create_vnic_details": map[string]interface{}{
|
||||||
|
"nsg_ids": []string{"ocd1..."},
|
||||||
|
},
|
||||||
|
"shape": "VM.Standard1.1",
|
||||||
|
"disk_size": 60,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConfig(t *testing.T) {
|
||||||
|
// Shared set-up and deferred deletion
|
||||||
|
|
||||||
|
cfg, keyFile, err := baseTestConfigWithTmpKeyFile()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(keyFile.Name())
|
||||||
|
|
||||||
|
cfgFile, err := writeTestConfig(cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer os.Remove(cfgFile.Name())
|
||||||
|
|
||||||
|
// Temporarily set $HOME to temp directory to bypass default
|
||||||
|
// access config loading.
|
||||||
|
|
||||||
|
tmpHome, err := ioutil.TempDir("", "packer_config_test")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error when creating temporary directory: %+v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(tmpHome)
|
||||||
|
|
||||||
|
home := os.Getenv("HOME")
|
||||||
|
os.Setenv("HOME", tmpHome)
|
||||||
|
defer os.Setenv("HOME", home)
|
||||||
|
|
||||||
|
// Config tests
|
||||||
|
t.Run("BaseConfig", func(t *testing.T) {
|
||||||
|
raw := testConfig(cfgFile)
|
||||||
|
var c Config
|
||||||
|
errs := c.Prepare(raw)
|
||||||
|
|
||||||
|
if errs != nil {
|
||||||
|
t.Fatalf("Unexpected error in configuration %+v", errs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("BaseImageFilterWithoutOCID", func(t *testing.T) {
|
||||||
|
raw := testConfig(cfgFile)
|
||||||
|
raw["base_image_ocid"] = ""
|
||||||
|
raw["base_image_filter"] = map[string]interface{}{
|
||||||
|
"display_name": "hello_world",
|
||||||
|
}
|
||||||
|
|
||||||
|
var c Config
|
||||||
|
errs := c.Prepare(raw)
|
||||||
|
|
||||||
|
if errs != nil {
|
||||||
|
t.Fatalf("Unexpected error in configuration %+v", errs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("BaseImageFilterDefault", func(t *testing.T) {
|
||||||
|
raw := testConfig(cfgFile)
|
||||||
|
|
||||||
|
var c Config
|
||||||
|
errs := c.Prepare(raw)
|
||||||
|
if errs != nil {
|
||||||
|
t.Fatalf("Unexpected error in configuration %+v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if *c.BaseImageFilter.Shape != raw["shape"] {
|
||||||
|
t.Fatalf("Default base_image_filter shape %v does not equal config shape %v",
|
||||||
|
*c.BaseImageFilter.Shape, raw["shape"])
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("LaunchMode", func(t *testing.T) {
|
||||||
|
raw := testConfig(cfgFile)
|
||||||
|
raw["image_launch_mode"] = "NATIVE"
|
||||||
|
|
||||||
|
var c Config
|
||||||
|
errs := c.Prepare(raw)
|
||||||
|
if errs != nil {
|
||||||
|
t.Fatalf("Unexpected error in configuration %+v", errs)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("NoAccessConfig", func(t *testing.T) {
|
||||||
|
raw := testConfig(cfgFile)
|
||||||
|
raw["access_cfg_file"] = "/tmp/random/access/config/file/should/not/exist"
|
||||||
|
|
||||||
|
var c Config
|
||||||
|
errs := c.Prepare(raw)
|
||||||
|
|
||||||
|
expectedErrors := []string{
|
||||||
|
"'user_ocid'", "'tenancy_ocid'", "'fingerprint'", "'key_file'",
|
||||||
|
}
|
||||||
|
|
||||||
|
if errs == nil {
|
||||||
|
t.Fatalf("Expected errors %q but got none", expectedErrors)
|
||||||
|
}
|
||||||
|
|
||||||
|
s := errs.Error()
|
||||||
|
for _, expected := range expectedErrors {
|
||||||
|
if !strings.Contains(s, expected) {
|
||||||
|
t.Errorf("Expected %q to contain '%s'", s, expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("AccessConfigTemplateOnly", func(t *testing.T) {
|
||||||
|
raw := testConfig(cfgFile)
|
||||||
|
delete(raw, "access_cfg_file")
|
||||||
|
raw["user_ocid"] = "ocid1..."
|
||||||
|
raw["tenancy_ocid"] = "ocid1..."
|
||||||
|
raw["fingerprint"] = "00:00..."
|
||||||
|
raw["key_file"] = keyFile.Name()
|
||||||
|
|
||||||
|
var c Config
|
||||||
|
errs := c.Prepare(raw)
|
||||||
|
|
||||||
|
if errs != nil {
|
||||||
|
t.Fatalf("err: %+v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("TenancyReadFromAccessCfgFile", func(t *testing.T) {
|
||||||
|
raw := testConfig(cfgFile)
|
||||||
|
var c Config
|
||||||
|
errs := c.Prepare(raw)
|
||||||
|
if errs != nil {
|
||||||
|
t.Fatalf("Unexpected error in configuration %+v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
tenancy, err := c.configProvider.TenancyOCID()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error getting tenancy ocid: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "ocid1.tenancy.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
|
||||||
|
if tenancy != expected {
|
||||||
|
t.Errorf("Expected tenancy: %s, got %s.", expected, tenancy)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("RegionNotDefaultedToPHXWhenSetInOCISettings", func(t *testing.T) {
|
||||||
|
raw := testConfig(cfgFile)
|
||||||
|
var c Config
|
||||||
|
errs := c.Prepare(raw)
|
||||||
|
if errs != nil {
|
||||||
|
t.Fatalf("Unexpected error in configuration %+v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
region, err := c.configProvider.Region()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Unexpected error getting region: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
expected := "us-ashburn-1"
|
||||||
|
if region != expected {
|
||||||
|
t.Errorf("Expected region: %s, got %s.", expected, region)
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test the correct errors are produced when required template keys are
|
||||||
|
// omitted.
|
||||||
|
requiredKeys := []string{"availability_domain", "base_image_ocid", "shape", "subnet_ocid"}
|
||||||
|
for _, k := range requiredKeys {
|
||||||
|
t.Run(k+"_required", func(t *testing.T) {
|
||||||
|
raw := testConfig(cfgFile)
|
||||||
|
delete(raw, k)
|
||||||
|
|
||||||
|
var c Config
|
||||||
|
errs := c.Prepare(raw)
|
||||||
|
|
||||||
|
if !strings.Contains(errs.Error(), k) {
|
||||||
|
t.Errorf("Expected '%s' to contain '%s'", errs.Error(), k)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("ImageNameDefaultedIfEmpty", func(t *testing.T) {
|
||||||
|
raw := testConfig(cfgFile)
|
||||||
|
delete(raw, "image_name")
|
||||||
|
|
||||||
|
var c Config
|
||||||
|
errs := c.Prepare(raw)
|
||||||
|
if errs != nil {
|
||||||
|
t.Fatalf("Unexpected error in configuration %+v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.Contains(c.ImageName, "packer-") {
|
||||||
|
t.Errorf("got default ImageName %q, want image name 'packer-{{timestamp}}'", c.ImageName)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("user_ocid_overridden", func(t *testing.T) {
|
||||||
|
expected := "override"
|
||||||
|
raw := testConfig(cfgFile)
|
||||||
|
raw["user_ocid"] = expected
|
||||||
|
|
||||||
|
var c Config
|
||||||
|
errs := c.Prepare(raw)
|
||||||
|
if errs != nil {
|
||||||
|
t.Fatalf("Unexpected error in configuration %+v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, _ := c.configProvider.UserOCID()
|
||||||
|
if user != expected {
|
||||||
|
t.Errorf("Expected ConfigProvider.UserOCID: %s, got %s", expected, user)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("tenancy_ocid_overidden", func(t *testing.T) {
|
||||||
|
expected := "override"
|
||||||
|
raw := testConfig(cfgFile)
|
||||||
|
raw["tenancy_ocid"] = expected
|
||||||
|
|
||||||
|
var c Config
|
||||||
|
errs := c.Prepare(raw)
|
||||||
|
if errs != nil {
|
||||||
|
t.Fatalf("Unexpected error in configuration %+v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
tenancy, _ := c.configProvider.TenancyOCID()
|
||||||
|
if tenancy != expected {
|
||||||
|
t.Errorf("Expected ConfigProvider.TenancyOCID: %s, got %s", expected, tenancy)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("region_overidden", func(t *testing.T) {
|
||||||
|
expected := "override"
|
||||||
|
raw := testConfig(cfgFile)
|
||||||
|
raw["region"] = expected
|
||||||
|
|
||||||
|
var c Config
|
||||||
|
errs := c.Prepare(raw)
|
||||||
|
if errs != nil {
|
||||||
|
t.Fatalf("Unexpected error in configuration %+v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
region, _ := c.configProvider.Region()
|
||||||
|
if region != expected {
|
||||||
|
t.Errorf("Expected ConfigProvider.Region: %s, got %s", expected, region)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fingerprint_overidden", func(t *testing.T) {
|
||||||
|
expected := "override"
|
||||||
|
raw := testConfig(cfgFile)
|
||||||
|
raw["fingerprint"] = expected
|
||||||
|
|
||||||
|
var c Config
|
||||||
|
errs := c.Prepare(raw)
|
||||||
|
if errs != nil {
|
||||||
|
t.Fatalf("Unexpected error in configuration: %+v", errs)
|
||||||
|
}
|
||||||
|
|
||||||
|
fingerprint, _ := c.configProvider.KeyFingerprint()
|
||||||
|
if fingerprint != expected {
|
||||||
|
t.Errorf("Expected ConfigProvider.KeyFingerprint: %s, got %s", expected, fingerprint)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Test the correct errors are produced when certain template keys
|
||||||
|
// are present alongside use_instance_principals key.
|
||||||
|
invalidKeys := []string{
|
||||||
|
"access_cfg_file",
|
||||||
|
"access_cfg_file_account",
|
||||||
|
"user_ocid",
|
||||||
|
"tenancy_ocid",
|
||||||
|
"region",
|
||||||
|
"fingerprint",
|
||||||
|
"key_file",
|
||||||
|
"pass_phrase",
|
||||||
|
}
|
||||||
|
for _, k := range invalidKeys {
|
||||||
|
t.Run(k+"_mixed_with_use_instance_principals", func(t *testing.T) {
|
||||||
|
raw := testConfig(cfgFile)
|
||||||
|
raw["use_instance_principals"] = "true"
|
||||||
|
raw[k] = "some_random_value"
|
||||||
|
|
||||||
|
var c Config
|
||||||
|
|
||||||
|
c.configProvider = instancePrincipalConfigurationProviderMock{}
|
||||||
|
|
||||||
|
errs := c.Prepare(raw)
|
||||||
|
|
||||||
|
if !strings.Contains(errs.Error(), k) {
|
||||||
|
t.Errorf("Expected '%s' to contain '%s'", errs.Error(), k)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BaseTestConfig creates the base (DEFAULT) config including a temporary key
|
||||||
|
// file.
|
||||||
|
// NOTE: Caller is responsible for removing temporary key file.
|
||||||
|
func baseTestConfigWithTmpKeyFile() (*ini.File, *os.File, error) {
|
||||||
|
keyFile, err := generateRSAKeyFile()
|
||||||
|
if err != nil {
|
||||||
|
return nil, keyFile, err
|
||||||
|
}
|
||||||
|
// Build ini
|
||||||
|
cfg := ini.Empty()
|
||||||
|
section, _ := cfg.NewSection("DEFAULT")
|
||||||
|
section.NewKey("region", "us-ashburn-1")
|
||||||
|
section.NewKey("tenancy", "ocid1.tenancy.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||||
|
section.NewKey("user", "ocid1.user.oc1..aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
|
||||||
|
section.NewKey("fingerprint", "70:04:5z:b3:19:ab:90:75:a4:1f:50:d4:c7:c3:33:20")
|
||||||
|
section.NewKey("key_file", keyFile.Name())
|
||||||
|
|
||||||
|
return cfg, keyFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WriteTestConfig writes a ini.File to a temporary file for use in unit tests.
|
||||||
|
// NOTE: Caller is responsible for removing temporary file.
|
||||||
|
func writeTestConfig(cfg *ini.File) (*os.File, error) {
|
||||||
|
confFile, err := ioutil.TempFile("", "config_file")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := confFile.Write([]byte("[DEFAULT]\n")); err != nil {
|
||||||
|
os.Remove(confFile.Name())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := cfg.WriteTo(confFile); err != nil {
|
||||||
|
os.Remove(confFile.Name())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return confFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// generateRSAKeyFile generates an RSA key file for use in unit tests.
|
||||||
|
// NOTE: The caller is responsible for deleting the temporary file.
|
||||||
|
func generateRSAKeyFile() (*os.File, error) {
|
||||||
|
// Create temporary file for the key
|
||||||
|
f, err := ioutil.TempFile("", "key")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate key
|
||||||
|
priv, err := rsa.GenerateKey(rand.Reader, 2014)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ASN.1 DER encoded form
|
||||||
|
privDer := x509.MarshalPKCS1PrivateKey(priv)
|
||||||
|
privBlk := pem.Block{
|
||||||
|
Type: "RSA PRIVATE KEY",
|
||||||
|
Headers: nil,
|
||||||
|
Bytes: privDer,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the key out
|
||||||
|
if _, err := f.Write(pem.EncodeToMemory(&privBlk)); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return f, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/oracle/oci-go-sdk/v36/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Driver interfaces between the builder steps and the OCI SDK.
|
||||||
|
type Driver interface {
|
||||||
|
CreateInstance(ctx context.Context, publicKey string) (string, error)
|
||||||
|
CreateImage(ctx context.Context, id string) (core.Image, error)
|
||||||
|
DeleteImage(ctx context.Context, id string) error
|
||||||
|
GetInstanceIP(ctx context.Context, id string) (string, error)
|
||||||
|
TerminateInstance(ctx context.Context, id string) error
|
||||||
|
WaitForImageCreation(ctx context.Context, id string) error
|
||||||
|
WaitForInstanceState(ctx context.Context, id string, waitStates []string, terminalState string) error
|
||||||
|
}
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/oracle/oci-go-sdk/v36/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// driverMock implements the Driver interface and communicates with Oracle
|
||||||
|
// OCI.
|
||||||
|
type driverMock struct {
|
||||||
|
CreateInstanceID string
|
||||||
|
CreateInstanceErr error
|
||||||
|
|
||||||
|
CreateImageID string
|
||||||
|
CreateImageErr error
|
||||||
|
|
||||||
|
DeleteImageID string
|
||||||
|
DeleteImageErr error
|
||||||
|
|
||||||
|
GetInstanceIPErr error
|
||||||
|
|
||||||
|
TerminateInstanceID string
|
||||||
|
TerminateInstanceErr error
|
||||||
|
|
||||||
|
WaitForImageCreationErr error
|
||||||
|
|
||||||
|
WaitForInstanceStateErr error
|
||||||
|
|
||||||
|
cfg *Config
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateInstance creates a new compute instance.
|
||||||
|
func (d *driverMock) CreateInstance(ctx context.Context, publicKey string) (string, error) {
|
||||||
|
if d.CreateInstanceErr != nil {
|
||||||
|
return "", d.CreateInstanceErr
|
||||||
|
}
|
||||||
|
|
||||||
|
d.CreateInstanceID = "ocid1..."
|
||||||
|
|
||||||
|
return d.CreateInstanceID, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateImage creates a new custom image.
|
||||||
|
func (d *driverMock) CreateImage(ctx context.Context, id string) (core.Image, error) {
|
||||||
|
if d.CreateImageErr != nil {
|
||||||
|
return core.Image{}, d.CreateImageErr
|
||||||
|
}
|
||||||
|
d.CreateImageID = id
|
||||||
|
return core.Image{Id: &id}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteImage mocks deleting a custom image.
|
||||||
|
func (d *driverMock) DeleteImage(ctx context.Context, id string) error {
|
||||||
|
if d.DeleteImageErr != nil {
|
||||||
|
return d.DeleteImageErr
|
||||||
|
}
|
||||||
|
|
||||||
|
d.DeleteImageID = id
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInstanceIP returns the public or private IP corresponding to the given instance id.
|
||||||
|
func (d *driverMock) GetInstanceIP(ctx context.Context, id string) (string, error) {
|
||||||
|
if d.GetInstanceIPErr != nil {
|
||||||
|
return "", d.GetInstanceIPErr
|
||||||
|
}
|
||||||
|
if d.cfg.UsePrivateIP {
|
||||||
|
return "private_ip", nil
|
||||||
|
}
|
||||||
|
return "ip", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// TerminateInstance terminates a compute instance.
|
||||||
|
func (d *driverMock) TerminateInstance(ctx context.Context, id string) error {
|
||||||
|
if d.TerminateInstanceErr != nil {
|
||||||
|
return d.TerminateInstanceErr
|
||||||
|
}
|
||||||
|
|
||||||
|
d.TerminateInstanceID = id
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForImageCreation waits for a provisioning custom image to reach the
|
||||||
|
// "AVAILABLE" state.
|
||||||
|
func (d *driverMock) WaitForImageCreation(ctx context.Context, id string) error {
|
||||||
|
return d.WaitForImageCreationErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForInstanceState waits for an instance to reach the a given terminal
|
||||||
|
// state.
|
||||||
|
func (d *driverMock) WaitForInstanceState(ctx context.Context, id string, waitStates []string, terminalState string) error {
|
||||||
|
return d.WaitForInstanceStateErr
|
||||||
|
}
|
||||||
@@ -0,0 +1,339 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"math"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"sync/atomic"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/oracle/oci-go-sdk/v36/common"
|
||||||
|
core "github.com/oracle/oci-go-sdk/v36/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
// driverOCI implements the Driver interface and communicates with Oracle
|
||||||
|
// OCI.
|
||||||
|
type driverOCI struct {
|
||||||
|
computeClient core.ComputeClient
|
||||||
|
vcnClient core.VirtualNetworkClient
|
||||||
|
cfg *Config
|
||||||
|
context context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
var retryPolicy = &common.RetryPolicy{
|
||||||
|
MaximumNumberAttempts: 10,
|
||||||
|
ShouldRetryOperation: func(res common.OCIOperationResponse) bool {
|
||||||
|
var e common.ServiceError
|
||||||
|
if errors.As(res.Error, &e) {
|
||||||
|
switch e.GetHTTPStatusCode() {
|
||||||
|
case http.StatusTooManyRequests, http.StatusInternalServerError, http.StatusServiceUnavailable:
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
},
|
||||||
|
NextDuration: func(res common.OCIOperationResponse) time.Duration {
|
||||||
|
x := uint64(res.AttemptNumber)
|
||||||
|
d := time.Duration(math.Pow(2, float64(atomic.LoadUint64(&x)))) * time.Second
|
||||||
|
j := time.Duration(rand.Float64()*(2000)) * time.Millisecond
|
||||||
|
w := d + j
|
||||||
|
return w
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var requestMetadata = common.RequestMetadata{
|
||||||
|
RetryPolicy: retryPolicy,
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewDriverOCI Creates a new driverOCI with a connected compute client and a connected vcn client.
|
||||||
|
func NewDriverOCI(cfg *Config) (Driver, error) {
|
||||||
|
coreClient, err := core.NewComputeClientWithConfigurationProvider(cfg.configProvider)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
vcnClient, err := core.NewVirtualNetworkClientWithConfigurationProvider(cfg.configProvider)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &driverOCI{
|
||||||
|
computeClient: coreClient,
|
||||||
|
vcnClient: vcnClient,
|
||||||
|
cfg: cfg,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateInstance creates a new compute instance.
|
||||||
|
func (d *driverOCI) CreateInstance(ctx context.Context, publicKey string) (string, error) {
|
||||||
|
metadata := map[string]string{
|
||||||
|
"ssh_authorized_keys": publicKey,
|
||||||
|
}
|
||||||
|
if d.cfg.Metadata != nil {
|
||||||
|
for key, value := range d.cfg.Metadata {
|
||||||
|
metadata[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if d.cfg.UserData != "" {
|
||||||
|
metadata["user_data"] = d.cfg.UserData
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create VNIC details for instance
|
||||||
|
CreateVnicDetails := core.CreateVnicDetails{
|
||||||
|
AssignPublicIp: d.cfg.CreateVnicDetails.AssignPublicIp,
|
||||||
|
DisplayName: d.cfg.CreateVnicDetails.DisplayName,
|
||||||
|
HostnameLabel: d.cfg.CreateVnicDetails.HostnameLabel,
|
||||||
|
NsgIds: d.cfg.CreateVnicDetails.NsgIds,
|
||||||
|
PrivateIp: d.cfg.CreateVnicDetails.PrivateIp,
|
||||||
|
SkipSourceDestCheck: d.cfg.CreateVnicDetails.SkipSourceDestCheck,
|
||||||
|
SubnetId: d.cfg.CreateVnicDetails.SubnetId,
|
||||||
|
DefinedTags: d.cfg.CreateVnicDetails.DefinedTags,
|
||||||
|
FreeformTags: d.cfg.CreateVnicDetails.FreeformTags,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine base image ID
|
||||||
|
var imageId *string
|
||||||
|
if d.cfg.BaseImageID != "" {
|
||||||
|
imageId = &d.cfg.BaseImageID
|
||||||
|
} else {
|
||||||
|
// Pull images and determine which image ID to use, if BaseImageId not specified
|
||||||
|
response, err := d.computeClient.ListImages(ctx, core.ListImagesRequest{
|
||||||
|
CompartmentId: d.cfg.BaseImageFilter.CompartmentId,
|
||||||
|
DisplayName: d.cfg.BaseImageFilter.DisplayName,
|
||||||
|
OperatingSystem: d.cfg.BaseImageFilter.OperatingSystem,
|
||||||
|
OperatingSystemVersion: d.cfg.BaseImageFilter.OperatingSystemVersion,
|
||||||
|
Shape: d.cfg.BaseImageFilter.Shape,
|
||||||
|
LifecycleState: "AVAILABLE",
|
||||||
|
SortBy: "TIMECREATED",
|
||||||
|
SortOrder: "DESC",
|
||||||
|
RequestMetadata: requestMetadata,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if len(response.Items) == 0 {
|
||||||
|
return "", errors.New("base_image_filter returned no images")
|
||||||
|
}
|
||||||
|
if d.cfg.BaseImageFilter.DisplayNameSearch != nil {
|
||||||
|
// Return most recent image that matches regex
|
||||||
|
imageNameRegex, err := regexp.Compile(*d.cfg.BaseImageFilter.DisplayNameSearch)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for _, image := range response.Items {
|
||||||
|
if imageNameRegex.MatchString(*image.DisplayName) {
|
||||||
|
imageId = image.Id
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if imageId == nil {
|
||||||
|
return "", errors.New("No image matched display_name_search criteria")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// If no regex provided, simply return most recent image pulled
|
||||||
|
imageId = response.Items[0].Id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Source details which will be used to Launch Instance
|
||||||
|
InstanceSourceDetails := core.InstanceSourceViaImageDetails{
|
||||||
|
ImageId: imageId,
|
||||||
|
BootVolumeSizeInGBs: &d.cfg.BootVolumeSizeInGBs,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build instance details
|
||||||
|
instanceDetails := core.LaunchInstanceDetails{
|
||||||
|
AvailabilityDomain: &d.cfg.AvailabilityDomain,
|
||||||
|
CompartmentId: &d.cfg.CompartmentID,
|
||||||
|
CreateVnicDetails: &CreateVnicDetails,
|
||||||
|
DefinedTags: d.cfg.InstanceDefinedTags,
|
||||||
|
DisplayName: d.cfg.InstanceName,
|
||||||
|
FreeformTags: d.cfg.InstanceTags,
|
||||||
|
Shape: &d.cfg.Shape,
|
||||||
|
SourceDetails: InstanceSourceDetails,
|
||||||
|
Metadata: metadata,
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.cfg.ShapeConfig.Ocpus != nil {
|
||||||
|
LaunchInstanceShapeConfigDetails := core.LaunchInstanceShapeConfigDetails{
|
||||||
|
Ocpus: d.cfg.ShapeConfig.Ocpus,
|
||||||
|
MemoryInGBs: d.cfg.ShapeConfig.MemoryInGBs,
|
||||||
|
}
|
||||||
|
instanceDetails.ShapeConfig = &LaunchInstanceShapeConfigDetails
|
||||||
|
}
|
||||||
|
|
||||||
|
instance, err := d.computeClient.LaunchInstance(context.TODO(), core.LaunchInstanceRequest{
|
||||||
|
LaunchInstanceDetails: instanceDetails,
|
||||||
|
RequestMetadata: requestMetadata,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return *instance.Id, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateImage creates a new custom image.
|
||||||
|
func (d *driverOCI) CreateImage(ctx context.Context, id string) (core.Image, error) {
|
||||||
|
res, err := d.computeClient.CreateImage(ctx, core.CreateImageRequest{CreateImageDetails: core.CreateImageDetails{
|
||||||
|
CompartmentId: &d.cfg.ImageCompartmentID,
|
||||||
|
InstanceId: &id,
|
||||||
|
DisplayName: &d.cfg.ImageName,
|
||||||
|
FreeformTags: d.cfg.Tags,
|
||||||
|
DefinedTags: d.cfg.DefinedTags,
|
||||||
|
LaunchMode: core.CreateImageDetailsLaunchModeEnum(d.cfg.LaunchMode),
|
||||||
|
},
|
||||||
|
RequestMetadata: requestMetadata,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return core.Image{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.Image, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteImage deletes a custom image.
|
||||||
|
func (d *driverOCI) DeleteImage(ctx context.Context, id string) error {
|
||||||
|
_, err := d.computeClient.DeleteImage(ctx, core.DeleteImageRequest{
|
||||||
|
ImageId: &id,
|
||||||
|
RequestMetadata: requestMetadata,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetInstanceIP returns the public or private IP corresponding to the given instance id.
|
||||||
|
func (d *driverOCI) GetInstanceIP(ctx context.Context, id string) (string, error) {
|
||||||
|
vnics, err := d.computeClient.ListVnicAttachments(ctx, core.ListVnicAttachmentsRequest{
|
||||||
|
InstanceId: &id,
|
||||||
|
CompartmentId: &d.cfg.CompartmentID,
|
||||||
|
RequestMetadata: requestMetadata,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(vnics.Items) == 0 {
|
||||||
|
return "", errors.New("instance has zero VNICs")
|
||||||
|
}
|
||||||
|
|
||||||
|
vnic, err := d.vcnClient.GetVnic(ctx, core.GetVnicRequest{
|
||||||
|
VnicId: vnics.Items[0].VnicId,
|
||||||
|
RequestMetadata: requestMetadata,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("Error getting VNIC details: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d.cfg.UsePrivateIP {
|
||||||
|
return *vnic.PrivateIp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if vnic.PublicIp == nil {
|
||||||
|
return "", fmt.Errorf("Error getting VNIC Public Ip for: %s", id)
|
||||||
|
}
|
||||||
|
|
||||||
|
return *vnic.PublicIp, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *driverOCI) GetInstanceInitialCredentials(ctx context.Context, id string) (string, string, error) {
|
||||||
|
credentials, err := d.computeClient.GetWindowsInstanceInitialCredentials(ctx, core.GetWindowsInstanceInitialCredentialsRequest{
|
||||||
|
InstanceId: &id,
|
||||||
|
RequestMetadata: requestMetadata,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return *credentials.InstanceCredentials.Username, *credentials.InstanceCredentials.Password, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// TerminateInstance terminates a compute instance.
|
||||||
|
func (d *driverOCI) TerminateInstance(ctx context.Context, id string) error {
|
||||||
|
_, err := d.computeClient.TerminateInstance(ctx, core.TerminateInstanceRequest{
|
||||||
|
InstanceId: &id,
|
||||||
|
RequestMetadata: requestMetadata,
|
||||||
|
})
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForImageCreation waits for a provisioning custom image to reach the
|
||||||
|
// "AVAILABLE" state.
|
||||||
|
func (d *driverOCI) WaitForImageCreation(ctx context.Context, id string) error {
|
||||||
|
return waitForResourceToReachState(
|
||||||
|
func(string) (string, error) {
|
||||||
|
image, err := d.computeClient.GetImage(ctx, core.GetImageRequest{
|
||||||
|
ImageId: &id,
|
||||||
|
RequestMetadata: requestMetadata,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(image.LifecycleState), nil
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
[]string{"PROVISIONING"},
|
||||||
|
"AVAILABLE",
|
||||||
|
0, //Unlimited Retries
|
||||||
|
5*time.Second, //5 second wait between retries
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForInstanceState waits for an instance to reach the a given terminal
|
||||||
|
// state.
|
||||||
|
func (d *driverOCI) WaitForInstanceState(ctx context.Context, id string, waitStates []string, terminalState string) error {
|
||||||
|
return waitForResourceToReachState(
|
||||||
|
func(string) (string, error) {
|
||||||
|
instance, err := d.computeClient.GetInstance(ctx, core.GetInstanceRequest{
|
||||||
|
InstanceId: &id,
|
||||||
|
RequestMetadata: requestMetadata,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(instance.LifecycleState), nil
|
||||||
|
},
|
||||||
|
id,
|
||||||
|
waitStates,
|
||||||
|
terminalState,
|
||||||
|
0, //Unlimited Retries
|
||||||
|
5*time.Second, //5 second wait between retries
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// WaitForResourceToReachState checks the response of a request through a
|
||||||
|
// polled get and waits until the desired state or until the max retried has
|
||||||
|
// been reached.
|
||||||
|
func waitForResourceToReachState(getResourceState func(string) (string, error), id string, waitStates []string, terminalState string, maxRetries int, waitDuration time.Duration) error {
|
||||||
|
for i := 0; maxRetries == 0 || i < maxRetries; i++ {
|
||||||
|
state, err := getResourceState(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if stringSliceContains(waitStates, state) {
|
||||||
|
time.Sleep(waitDuration)
|
||||||
|
continue
|
||||||
|
} else if state == terminalState {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Unexpected resource state %q, expecting a waiting state %s or terminal state %q ", state, waitStates, terminalState)
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Maximum number of retries (%d) exceeded; resource did not reach state %q", maxRetries, terminalState)
|
||||||
|
}
|
||||||
|
|
||||||
|
// stringSliceContains loops through a slice of strings returning a boolean
|
||||||
|
// based on whether a given value is contained in the slice.
|
||||||
|
func stringSliceContains(slice []string, value string) bool {
|
||||||
|
for _, elem := range slice {
|
||||||
|
if elem == value {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
@@ -0,0 +1,43 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/rsa"
|
||||||
|
|
||||||
|
"github.com/oracle/oci-go-sdk/v36/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Mock struct to be used during testing to obtain Instance Principals.
|
||||||
|
type instancePrincipalConfigurationProviderMock struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p instancePrincipalConfigurationProviderMock) PrivateRSAKey() (*rsa.PrivateKey, error) {
|
||||||
|
return rsa.GenerateKey(rand.Reader, 1024)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p instancePrincipalConfigurationProviderMock) KeyID() (string, error) {
|
||||||
|
return "some_random_key_id", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p instancePrincipalConfigurationProviderMock) TenancyOCID() (string, error) {
|
||||||
|
return "some_random_tenancy", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p instancePrincipalConfigurationProviderMock) UserOCID() (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p instancePrincipalConfigurationProviderMock) KeyFingerprint() (string, error) {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p instancePrincipalConfigurationProviderMock) Region() (string, error) {
|
||||||
|
return "some_random_region", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p instancePrincipalConfigurationProviderMock) AuthType() (common.AuthConfig, error) {
|
||||||
|
return common.AuthConfig{
|
||||||
|
AuthType: common.InstancePrincipal,
|
||||||
|
IsFromConfigFile: false,
|
||||||
|
OboToken: nil}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepCreateInstance struct{}
|
||||||
|
|
||||||
|
func (s *stepCreateInstance) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
var (
|
||||||
|
driver = state.Get("driver").(Driver)
|
||||||
|
ui = state.Get("ui").(packersdk.Ui)
|
||||||
|
config = state.Get("config").(*Config)
|
||||||
|
)
|
||||||
|
|
||||||
|
ui.Say("Creating instance...")
|
||||||
|
|
||||||
|
instanceID, err := driver.CreateInstance(ctx, string(config.Comm.SSHPublicKey))
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Problem creating instance: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Put("instance_id", instanceID)
|
||||||
|
|
||||||
|
ui.Say(fmt.Sprintf("Created instance (%s).", instanceID))
|
||||||
|
|
||||||
|
ui.Say("Waiting for instance to enter 'RUNNING' state...")
|
||||||
|
|
||||||
|
if err = driver.WaitForInstanceState(ctx, instanceID, []string{"STARTING", "PROVISIONING"}, "RUNNING"); err != nil {
|
||||||
|
err = fmt.Errorf("Error waiting for instance to start: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("Instance 'RUNNING'.")
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepCreateInstance) Cleanup(state multistep.StateBag) {
|
||||||
|
driver := state.Get("driver").(Driver)
|
||||||
|
ui := state.Get("ui").(packersdk.Ui)
|
||||||
|
|
||||||
|
idRaw, ok := state.GetOk("instance_id")
|
||||||
|
if !ok {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
id := idRaw.(string)
|
||||||
|
|
||||||
|
ui.Say(fmt.Sprintf("Terminating instance (%s)...", id))
|
||||||
|
|
||||||
|
if err := driver.TerminateInstance(context.TODO(), id); err != nil {
|
||||||
|
err = fmt.Errorf("Error terminating instance. Please terminate manually: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := driver.WaitForInstanceState(context.TODO(), id, []string{"TERMINATING"}, "TERMINATED")
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error terminating instance. Please terminate manually: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ui.Say("Terminated instance.")
|
||||||
|
}
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStepCreateInstance(t *testing.T) {
|
||||||
|
state := testState()
|
||||||
|
state.Put("publicKey", "key")
|
||||||
|
|
||||||
|
step := new(stepCreateInstance)
|
||||||
|
defer step.Cleanup(state)
|
||||||
|
|
||||||
|
driver := state.Get("driver").(*driverMock)
|
||||||
|
|
||||||
|
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
|
||||||
|
t.Fatalf("bad action: %#v", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceIDRaw, ok := state.GetOk("instance_id")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("should have machine")
|
||||||
|
}
|
||||||
|
|
||||||
|
step.Cleanup(state)
|
||||||
|
|
||||||
|
if driver.TerminateInstanceID != instanceIDRaw.(string) {
|
||||||
|
t.Fatalf(
|
||||||
|
"should've deleted instance (%s != %s)",
|
||||||
|
driver.TerminateInstanceID, instanceIDRaw.(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepCreateInstance_CreateInstanceErr(t *testing.T) {
|
||||||
|
state := testState()
|
||||||
|
state.Put("publicKey", "key")
|
||||||
|
|
||||||
|
step := new(stepCreateInstance)
|
||||||
|
defer step.Cleanup(state)
|
||||||
|
|
||||||
|
driver := state.Get("driver").(*driverMock)
|
||||||
|
driver.CreateInstanceErr = errors.New("error")
|
||||||
|
|
||||||
|
if action := step.Run(context.Background(), state); action != multistep.ActionHalt {
|
||||||
|
t.Fatalf("bad action: %#v", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := state.GetOk("error"); !ok {
|
||||||
|
t.Fatalf("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := state.GetOk("instance_id"); ok {
|
||||||
|
t.Fatalf("should NOT have instance_id")
|
||||||
|
}
|
||||||
|
|
||||||
|
step.Cleanup(state)
|
||||||
|
|
||||||
|
if driver.TerminateInstanceID != "" {
|
||||||
|
t.Fatalf("Should not have tried to terminate an instance")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepCreateInstance_WaitForInstanceStateErr(t *testing.T) {
|
||||||
|
state := testState()
|
||||||
|
state.Put("publicKey", "key")
|
||||||
|
|
||||||
|
step := new(stepCreateInstance)
|
||||||
|
defer step.Cleanup(state)
|
||||||
|
|
||||||
|
driver := state.Get("driver").(*driverMock)
|
||||||
|
driver.WaitForInstanceStateErr = errors.New("error")
|
||||||
|
|
||||||
|
if action := step.Run(context.Background(), state); action != multistep.ActionHalt {
|
||||||
|
t.Fatalf("bad action: %#v", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := state.GetOk("error"); !ok {
|
||||||
|
t.Fatalf("should have error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepCreateInstance_TerminateInstanceErr(t *testing.T) {
|
||||||
|
state := testState()
|
||||||
|
state.Put("publicKey", "key")
|
||||||
|
|
||||||
|
step := new(stepCreateInstance)
|
||||||
|
defer step.Cleanup(state)
|
||||||
|
|
||||||
|
driver := state.Get("driver").(*driverMock)
|
||||||
|
|
||||||
|
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
|
||||||
|
t.Fatalf("bad action: %#v", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, ok := state.GetOk("instance_id")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("should have machine")
|
||||||
|
}
|
||||||
|
|
||||||
|
driver.TerminateInstanceErr = errors.New("error")
|
||||||
|
step.Cleanup(state)
|
||||||
|
|
||||||
|
if _, ok := state.GetOk("error"); !ok {
|
||||||
|
t.Fatalf("should have error")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepCreateInstanceCleanup_WaitForInstanceStateErr(t *testing.T) {
|
||||||
|
state := testState()
|
||||||
|
state.Put("publicKey", "key")
|
||||||
|
|
||||||
|
step := new(stepCreateInstance)
|
||||||
|
defer step.Cleanup(state)
|
||||||
|
|
||||||
|
driver := state.Get("driver").(*driverMock)
|
||||||
|
|
||||||
|
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
|
||||||
|
t.Fatalf("bad action: %#v", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
driver.WaitForInstanceStateErr = errors.New("error")
|
||||||
|
step.Cleanup(state)
|
||||||
|
|
||||||
|
if _, ok := state.GetOk("error"); !ok {
|
||||||
|
t.Fatalf("should have error")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/communicator"
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepGetDefaultCredentials struct {
|
||||||
|
Debug bool
|
||||||
|
Comm *communicator.Config
|
||||||
|
BuildName string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepGetDefaultCredentials) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
var (
|
||||||
|
driver = state.Get("driver").(*driverOCI)
|
||||||
|
ui = state.Get("ui").(packersdk.Ui)
|
||||||
|
id = state.Get("instance_id").(string)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Skip if we're not using winrm
|
||||||
|
if s.Comm.Type != "winrm" {
|
||||||
|
log.Printf("[INFO] Not using winrm communicator, skipping get password...")
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we already have a password, skip it
|
||||||
|
if s.Comm.WinRMPassword != "" {
|
||||||
|
ui.Say("Skipping waiting for password since WinRM password set...")
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
username, password, err := driver.GetInstanceInitialCredentials(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error getting instance's credentials: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
s.Comm.WinRMPassword = password
|
||||||
|
s.Comm.WinRMUser = username
|
||||||
|
|
||||||
|
if s.Debug {
|
||||||
|
ui.Message(fmt.Sprintf(
|
||||||
|
"[DEBUG] (OCI default credentials): Credentials (since debug is enabled): %s", password))
|
||||||
|
}
|
||||||
|
|
||||||
|
// store so that we can access this later during provisioning
|
||||||
|
state.Put("winrm_password", s.Comm.WinRMPassword)
|
||||||
|
packersdk.LogSecretFilter.Set(s.Comm.WinRMPassword)
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepGetDefaultCredentials) Cleanup(state multistep.StateBag) {
|
||||||
|
// no cleanup
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepImage struct{}
|
||||||
|
|
||||||
|
func (s *stepImage) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
var (
|
||||||
|
driver = state.Get("driver").(Driver)
|
||||||
|
ui = state.Get("ui").(packersdk.Ui)
|
||||||
|
instanceID = state.Get("instance_id").(string)
|
||||||
|
)
|
||||||
|
|
||||||
|
ui.Say("Creating image from instance...")
|
||||||
|
|
||||||
|
image, err := driver.CreateImage(ctx, instanceID)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error creating image from instance: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
err = driver.WaitForImageCreation(ctx, *image.Id)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error waiting for image creation to finish: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO(apryde): This is stale as .LifecycleState has changed to
|
||||||
|
// AVAILABLE at this point. Does it matter?
|
||||||
|
state.Put("image", image)
|
||||||
|
|
||||||
|
ui.Say("Image created.")
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepImage) Cleanup(state multistep.StateBag) {
|
||||||
|
// Nothing to do
|
||||||
|
}
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestStepImage(t *testing.T) {
|
||||||
|
state := testState()
|
||||||
|
state.Put("instance_id", "ocid1...")
|
||||||
|
|
||||||
|
step := new(stepImage)
|
||||||
|
defer step.Cleanup(state)
|
||||||
|
|
||||||
|
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
|
||||||
|
t.Fatalf("bad action: %#v", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := state.GetOk("image"); !ok {
|
||||||
|
t.Fatalf("should have image")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepImage_CreateImageErr(t *testing.T) {
|
||||||
|
state := testState()
|
||||||
|
state.Put("instance_id", "ocid1...")
|
||||||
|
|
||||||
|
step := new(stepImage)
|
||||||
|
defer step.Cleanup(state)
|
||||||
|
|
||||||
|
driver := state.Get("driver").(*driverMock)
|
||||||
|
driver.CreateImageErr = errors.New("error")
|
||||||
|
|
||||||
|
if action := step.Run(context.Background(), state); action != multistep.ActionHalt {
|
||||||
|
t.Fatalf("bad action: %#v", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := state.GetOk("error"); !ok {
|
||||||
|
t.Fatalf("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := state.GetOk("image"); ok {
|
||||||
|
t.Fatalf("should NOT have image")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStepImage_WaitForImageCreationErr(t *testing.T) {
|
||||||
|
state := testState()
|
||||||
|
state.Put("instance_id", "ocid1...")
|
||||||
|
|
||||||
|
step := new(stepImage)
|
||||||
|
defer step.Cleanup(state)
|
||||||
|
|
||||||
|
driver := state.Get("driver").(*driverMock)
|
||||||
|
driver.WaitForImageCreationErr = errors.New("error")
|
||||||
|
|
||||||
|
if action := step.Run(context.Background(), state); action != multistep.ActionHalt {
|
||||||
|
t.Fatalf("bad action: %#v", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := state.GetOk("error"); !ok {
|
||||||
|
t.Fatalf("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := state.GetOk("image"); ok {
|
||||||
|
t.Fatalf("should not have image")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
type stepInstanceInfo struct{}
|
||||||
|
|
||||||
|
func (s *stepInstanceInfo) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
|
||||||
|
var (
|
||||||
|
driver = state.Get("driver").(Driver)
|
||||||
|
ui = state.Get("ui").(packersdk.Ui)
|
||||||
|
id = state.Get("instance_id").(string)
|
||||||
|
)
|
||||||
|
|
||||||
|
ip, err := driver.GetInstanceIP(ctx, id)
|
||||||
|
if err != nil {
|
||||||
|
err = fmt.Errorf("Error getting instance's IP: %s", err)
|
||||||
|
ui.Error(err.Error())
|
||||||
|
state.Put("error", err)
|
||||||
|
return multistep.ActionHalt
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Put("instance_ip", ip)
|
||||||
|
|
||||||
|
ui.Say(fmt.Sprintf("Instance has IP: %s.", ip))
|
||||||
|
|
||||||
|
return multistep.ActionContinue
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stepInstanceInfo) Cleanup(state multistep.StateBag) {
|
||||||
|
// no cleanup
|
||||||
|
}
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
package oci
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/hashicorp/packer-plugin-sdk/multistep"
|
||||||
|
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInstanceInfo(t *testing.T) {
|
||||||
|
state := testState()
|
||||||
|
state.Put("instance_id", "ocid1...")
|
||||||
|
|
||||||
|
step := new(stepInstanceInfo)
|
||||||
|
defer step.Cleanup(state)
|
||||||
|
|
||||||
|
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
|
||||||
|
t.Fatalf("bad action: %#v", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceIPRaw, ok := state.GetOk("instance_ip")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("should have instance_ip")
|
||||||
|
}
|
||||||
|
|
||||||
|
if instanceIPRaw.(string) != "ip" {
|
||||||
|
t.Fatalf("should've got ip ('%s' != 'ip')", instanceIPRaw.(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstanceInfoPrivateIP(t *testing.T) {
|
||||||
|
baseTestConfig := baseTestConfig()
|
||||||
|
baseTestConfig.UsePrivateIP = true
|
||||||
|
state := new(multistep.BasicStateBag)
|
||||||
|
state.Put("config", baseTestConfig)
|
||||||
|
state.Put("driver", &driverMock{cfg: baseTestConfig})
|
||||||
|
state.Put("hook", &packersdk.MockHook{})
|
||||||
|
state.Put("ui", &packersdk.BasicUi{
|
||||||
|
Reader: new(bytes.Buffer),
|
||||||
|
Writer: new(bytes.Buffer),
|
||||||
|
})
|
||||||
|
state.Put("instance_id", "ocid1...")
|
||||||
|
|
||||||
|
step := new(stepInstanceInfo)
|
||||||
|
defer step.Cleanup(state)
|
||||||
|
|
||||||
|
if action := step.Run(context.Background(), state); action != multistep.ActionContinue {
|
||||||
|
t.Fatalf("bad action: %#v", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
instanceIPRaw, ok := state.GetOk("instance_ip")
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("should have instance_ip")
|
||||||
|
}
|
||||||
|
|
||||||
|
if instanceIPRaw.(string) != "private_ip" {
|
||||||
|
t.Fatalf("should've got ip ('%s' != 'private_ip')", instanceIPRaw.(string))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestInstanceInfo_GetInstanceIPErr(t *testing.T) {
|
||||||
|
state := testState()
|
||||||
|
state.Put("instance_id", "ocid1...")
|
||||||
|
|
||||||
|
step := new(stepInstanceInfo)
|
||||||
|
defer step.Cleanup(state)
|
||||||
|
|
||||||
|
driver := state.Get("driver").(*driverMock)
|
||||||
|
driver.GetInstanceIPErr = errors.New("error")
|
||||||
|
|
||||||
|
if action := step.Run(context.Background(), state); action != multistep.ActionHalt {
|
||||||
|
t.Fatalf("bad action: %#v", action)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := state.GetOk("error"); !ok {
|
||||||
|
t.Fatalf("should have error")
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := state.GetOk("instance_ip"); ok {
|
||||||
|
t.Fatalf("should NOT have instance_ip")
|
||||||
|
}
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user