Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
38 changes: 21 additions & 17 deletions .web-docs/components/builder/instance/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ The configuration arguments for the builder. Arguments can either be required or
- `project` (string) - Name or ID of the project where the temporary instance and resulting image
will be created.

- `artifact_name` (string) - Name of the resulting image artifact. Required unless `skip_create_image`
is `true`.

<!-- End of code generated from the comments of the Config struct in component/builder/instance/config.go; -->


Expand Down Expand Up @@ -62,14 +65,12 @@ The configuration arguments for the builder. Arguments can either be required or

- `subnet` (string) - Subnet to create the instance within. Defaults to `default`.

- `name` (string) - Name of the temporary instance. Defaults to `packer-BUILD_NAME-RUN_ID` where
`BUILD_NAME` is the Packer source name and `RUN_ID` is a short prefix of the
unique ID Packer assigns to the current run. This must be unique to prevent
Oxide instance name conflicts.
- `name` (string) - Name of the temporary instance and its boot disk, primary network interface,
and snapshot. Defaults to `packer-UUID`, where `UUID` is a unique identifier
generated for the current build. This must be unique to prevent Oxide
resource name conflicts.

- `hostname` (string) - Hostname of the temporary instance. Defaults to `packer-BUILD_NAME-RUN_ID`
where `BUILD_NAME` is the Packer source name and `RUN_ID` is a short prefix
of the unique ID Packer assigns to the current run.
- `hostname` (string) - Hostname of the temporary instance. Defaults to the value of `name`.

- `cpus` (uint64) - Number of vCPUs to provision the instance with. Defaults to `1`.

Expand All @@ -78,14 +79,8 @@ The configuration arguments for the builder. Arguments can either be required or

- `ssh_public_keys` ([]string) - An array of names or IDs of SSH public keys to inject into the instance.

- `artifact_name` (string) - Name of the resulting image artifact. Defaults to
`SOURCE_IMAGE_NAME-BUILD_NAME-RUN_ID` where `SOURCE_IMAGE_NAME` is the name
of the source image as retrieved from Oxide, `BUILD_NAME` is the Packer
source name, and `RUN_ID` is a short prefix of the unique ID Packer assigns
to the current run.

- `artifact_description` (string) - Description of the resulting image artifact. Defaults to the description of
the source image as retrieved from Oxide.
- `artifact_description` (string) - Description of the resulting image artifact. Defaults to a description that
identifies the Packer build that created it.

- `artifact_os` (string) - Operating system of the resulting image artifact. Defaults to the OS of the
source image as retrieved from Oxide.
Expand All @@ -111,6 +106,13 @@ The configuration arguments for the builder. Arguments can either be required or
<!-- End of code generated from the comments of the Config struct in component/builder/instance/config.go; -->


## Temporary Resources

Generated temporary resources use the name `packer-UUID`, where `UUID` is the
unique identifier generated for the current build. The builder logs the names
and IDs of created instances, snapshots, and SSH keys and sets temporary
resource descriptions to identify the build that created them.

## Interpolation

This builder does not support Go template interpolation (e.g., `{{timestamp}}`).
Expand Down Expand Up @@ -145,8 +147,9 @@ Packer to connect to the instance.

The name of the temporary SSH public key uploaded to Oxide can be set using the
[`temporary_key_pair_name`](/packer/docs/communicators/ssh#temporary_key_pair_name)
argument. Generally there's no reason to set this but it's available should it
be necessary.
argument. Generally there's no reason to set this but it's available should
it be necessary. It defaults to `packer-UUID`, where `UUID` is the unique
identifier generated for the current build.

## Provisioner

Expand Down Expand Up @@ -206,6 +209,7 @@ with the builder.
source "oxide-instance" "example" {
project = "packer-acc-test"
boot_disk_image_id = "feb2c8ee-5a1d-4d66-beeb-289b860561bf"
artifact_name = "packer-${formatdate("YYYYMMDD-hhmmss", timestamp())}"

# SSH communicator configuration.
ssh_username = "ubuntu"
Expand Down
101 changes: 46 additions & 55 deletions component/builder/instance/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,6 @@ package instance
import (
"errors"
"fmt"
"os"
"strings"

"github.com/hashicorp/packer-plugin-sdk/common"
"github.com/hashicorp/packer-plugin-sdk/communicator"
Expand Down Expand Up @@ -68,15 +66,13 @@ type Config struct {
// Subnet to create the instance within. Defaults to `default`.
Subnet string `mapstructure:"subnet"`

// Name of the temporary instance. Defaults to `packer-BUILD_NAME-RUN_ID` where
// `BUILD_NAME` is the Packer source name and `RUN_ID` is a short prefix of the
// unique ID Packer assigns to the current run. This must be unique to prevent
// Oxide instance name conflicts.
// Name of the temporary instance and its boot disk, primary network interface,
// and snapshot. Defaults to `packer-UUID`, where `UUID` is a unique identifier
// generated for the current build. This must be unique to prevent Oxide
// resource name conflicts.
Name string `mapstructure:"name"`

// Hostname of the temporary instance. Defaults to `packer-BUILD_NAME-RUN_ID`
// where `BUILD_NAME` is the Packer source name and `RUN_ID` is a short prefix
// of the unique ID Packer assigns to the current run.
// Hostname of the temporary instance. Defaults to the value of `name`.
Hostname string `mapstructure:"hostname"`

// Number of vCPUs to provision the instance with. Defaults to `1`.
Expand All @@ -89,15 +85,12 @@ type Config struct {
// An array of names or IDs of SSH public keys to inject into the instance.
SSHPublicKeys []string `mapstructure:"ssh_public_keys"`

// Name of the resulting image artifact. Defaults to
// `SOURCE_IMAGE_NAME-BUILD_NAME-RUN_ID` where `SOURCE_IMAGE_NAME` is the name
// of the source image as retrieved from Oxide, `BUILD_NAME` is the Packer
// source name, and `RUN_ID` is a short prefix of the unique ID Packer assigns
// to the current run.
ArtifactName string `mapstructure:"artifact_name"`
// Name of the resulting image artifact. Required unless `skip_create_image`
// is `true`.
ArtifactName string `mapstructure:"artifact_name" required:"true"`

// Description of the resulting image artifact. Defaults to the description of
// the source image as retrieved from Oxide.
// Description of the resulting image artifact. Defaults to a description that
// identifies the Packer build that created it.
ArtifactDescription string `mapstructure:"artifact_description"`

// Operating system of the resulting image artifact. Defaults to the OS of the
Expand All @@ -124,6 +117,9 @@ type Config struct {
// created, run `cloud-init status --wait` or an equivalent in a
// provisioner.
UserData string `mapstructure:"user_data" required:"false"`

// Unique suffix shared by generated resource names for this build.
generatedSuffix string
}

// Prepare decodes the configuration and validates it.
Expand All @@ -141,11 +137,15 @@ func (c *Config) Prepare(args ...any) ([]string, error) {
// Set defaults.
{
if c.Name == "" {
c.Name = fmt.Sprintf("packer-%s", c.uniqueSuffix())
c.Name = c.uniqueName("packer")
}

if c.Hostname == "" {
c.Hostname = fmt.Sprintf("packer-%s", c.uniqueSuffix())
c.Hostname = c.Name
}

if c.ArtifactDescription == "" {
c.ArtifactDescription = c.buildDescription()
}

if c.CPUs == 0 {
Expand Down Expand Up @@ -178,7 +178,7 @@ func (c *Config) Prepare(args ...any) ([]string, error) {
}

if c.Comm.SSHTemporaryKeyPairName == "" {
c.Comm.SSHTemporaryKeyPairName = fmt.Sprintf("packer-%s", c.uniqueSuffix())
c.Comm.SSHTemporaryKeyPairName = c.uniqueName("packer")
}

c.Comm.SSHTemporaryKeyPairType = "ed25519"
Expand All @@ -194,6 +194,13 @@ func (c *Config) Prepare(args ...any) ([]string, error) {
)
}

if c.ArtifactName == "" && !c.SkipCreateImage {
multiErr = packer.MultiErrorAppend(
multiErr,
errors.New("artifact_name is required unless skip_create_image is true"),
)
}

if len(c.UserData) > 32*1024 {
multiErr = packer.MultiErrorAppend(
multiErr,
Expand All @@ -211,43 +218,27 @@ func (c *Config) Prepare(args ...any) ([]string, error) {
return nil, nil
}

// uniqueSuffix returns an identifier, derived from Packer-provided values,
// that is used to configure resource names that are unique and traceable to the
// Packer build that created them.
//
// The following Packer-provided values are used to generate the identifier.
//
// - [common.PackerConfig.PackerBuildName]: The build source name which is
// unique for each build in a Packer configuration.
// - PACKER_RUN_UUID: The unique ID Packer assigned to the current run, which is
// shared among the builds in a Packer configuration. When this is unset, it
// falls back to a generated UUID that's unique for each build. This value is
// truncated to 8 characters to keep the generated identifier within Oxide's
// 63-character name limit.
func (c *Config) uniqueSuffix() string {
runID := os.Getenv("PACKER_RUN_UUID")
if runID == "" {
runID = uuid.TimeOrderedUUID()
// buildDescription returns a description identifying the Packer build that
// created a resource.
func (c *Config) buildDescription() string {
if c.PackerBuildName == "" {
return "Created by Packer."
}

if len(runID) > 8 {
runID = runID[:8]
}
return fmt.Sprintf("Created by Packer build %q.", c.PackerBuildName)
}

// Transform the Packer build name into a name that the Oxide API will accept.
// This is mainly here to transform `_` into `-`.
buildName := strings.Map(func(r rune) rune {
switch {
case r >= 'a' && r <= 'z',
r >= 'A' && r <= 'Z',
r >= '0' && r <= '9',
r == '-':
return r
default:
return '-'
}
}, c.PackerBuildName)
buildName = strings.Trim(buildName, "-")
// uniqueName returns a name containing the unique suffix for this build.
func (c *Config) uniqueName(prefix string) string {
return fmt.Sprintf("%s-%s", prefix, c.uniqueSuffix())
}

// uniqueSuffix returns the identifier used to generate unique resource names
// for this build.
func (c *Config) uniqueSuffix() string {
if c.generatedSuffix == "" {
c.generatedSuffix = uuid.TimeOrderedUUID()
}

return fmt.Sprintf("%s-%s", buildName, runID)
return c.generatedSuffix
}
2 changes: 1 addition & 1 deletion component/builder/instance/config.hcl2spec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

122 changes: 122 additions & 0 deletions component/builder/instance/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// 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 https://mozilla.org/MPL/2.0/.

package instance

import (
"regexp"
"strings"
"testing"
)

func TestUniqueSuffix(t *testing.T) {
t.Setenv("PACKER_RUN_UUID", "packer-run-uuid")

config := &Config{}
suffix := config.uniqueSuffix()

if suffix != config.uniqueSuffix() {
t.Fatal("uniqueSuffix returned different values for the same config")
}

uuidPattern := regexp.MustCompile(
`^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$`,
)
if !uuidPattern.MatchString(suffix) {
t.Fatalf("uniqueSuffix returned invalid UUID %q", suffix)
}
}

func TestUniqueName(t *testing.T) {
config := &Config{}
prefix := "packer"
name := config.uniqueName(prefix)

if got, want := name, prefix+"-"+config.uniqueSuffix(); got != want {
t.Fatalf("uniqueName returned %q; want %q", got, want)
}
}

func TestBuildDescription(t *testing.T) {
tests := []struct {
name string
buildName string
want string
}{
{
name: "build name",
buildName: "service-image",
want: `Created by Packer build "service-image".`,
},
{
name: "missing build name",
want: "Created by Packer.",
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
config := &Config{}
config.PackerBuildName = test.buildName

if got := config.buildDescription(); got != test.want {
t.Fatalf("buildDescription returned %q; want %q", got, test.want)
}
})
}
}

func TestPrepareGeneratedNames(t *testing.T) {
config := &Config{}
_, err := config.Prepare(map[string]any{
"artifact_name": "artifact",
"boot_disk_image_id": "image-id",
"communicator": "none",
"packer_build_name": "service-image",
"project": "project",
})
if err != nil {
t.Fatalf("Prepare returned an error: %v", err)
}

if config.Hostname != config.Name {
t.Fatalf("Hostname is %q; want %q", config.Hostname, config.Name)
}
if got, want := config.ArtifactDescription,
`Created by Packer build "service-image".`; got != want {
t.Fatalf("ArtifactDescription is %q; want %q", got, want)
}
if config.Comm.SSHTemporaryKeyPairName != config.Name {
t.Fatalf(
"SSHTemporaryKeyPairName is %q; want %q",
config.Comm.SSHTemporaryKeyPairName,
config.Name,
)
}
}

func TestPrepareRequiresArtifactName(t *testing.T) {
config := &Config{}
_, err := config.Prepare(map[string]any{
"boot_disk_image_id": "image-id",
"communicator": "none",
"project": "project",
})
if err == nil || !strings.Contains(err.Error(), "artifact_name is required") {
t.Fatalf("Prepare returned error %v", err)
}
}

func TestPrepareAllowsMissingArtifactNameWhenSkippingImage(t *testing.T) {
config := &Config{}
_, err := config.Prepare(map[string]any{
"boot_disk_image_id": "image-id",
"communicator": "none",
"project": "project",
"skip_create_image": true,
})
if err != nil {
t.Fatalf("Prepare returned an error: %v", err)
}
}
2 changes: 1 addition & 1 deletion component/builder/instance/step_image_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ func (s *stepImageCreate) Run(
return multistep.ActionHalt
}

ui.Sayf("Created Oxide image: %s", image.Id)
ui.Sayf("Created Oxide image %s (%s)", image.Name, image.Id)

stateBag.Put("image_id", string(image.Id))
stateBag.Put("image_name", string(image.Name))
Expand Down
Loading
Loading