From 46f246907eed9238fd06a8f43ee6c7de4f8fc9e3 Mon Sep 17 00:00:00 2001 From: Matthew Sanabria Date: Mon, 12 May 2025 14:07:42 -0400 Subject: [PATCH] component: add oxide-image data source Added an `oxide-image` data source that fetches image information from Oxide and makes that information available for other Packer plugin components to use. --- .../builder/instance/step_instance_create.go | 1 + component/data-source/image/data_source.go | 130 ++++++++++++++++++ .../data-source/image/data_source.hcl2spec.go | 60 ++++++++ .../data-source/image/Config-not-required.mdx | 12 ++ .../component/data-source/image/Config.mdx | 5 + .../data-source/image/DataSource.mdx | 6 + .../data-source/image/Output-not-required.mdx | 5 + .../component/data-source/image/Output.mdx | 6 + main.go | 2 + template.pkr.hcl | 8 +- 10 files changed, 233 insertions(+), 2 deletions(-) create mode 100644 component/data-source/image/data_source.go create mode 100644 component/data-source/image/data_source.hcl2spec.go create mode 100644 docs-partials/component/data-source/image/Config-not-required.mdx create mode 100644 docs-partials/component/data-source/image/Config.mdx create mode 100644 docs-partials/component/data-source/image/DataSource.mdx create mode 100644 docs-partials/component/data-source/image/Output-not-required.mdx create mode 100644 docs-partials/component/data-source/image/Output.mdx diff --git a/component/builder/instance/step_instance_create.go b/component/builder/instance/step_instance_create.go index bd1993c..b3da2d4 100644 --- a/component/builder/instance/step_instance_create.go +++ b/component/builder/instance/step_instance_create.go @@ -30,6 +30,7 @@ func (o *stepInstanceCreate) Run(ctx context.Context, stateBag multistep.StateBa instance, err := oxideClient.InstanceCreate(ctx, oxide.InstanceCreateParams{ Project: oxide.NameOrId(config.Project), Body: &oxide.InstanceCreate{ + AntiAffinityGroups: []oxide.NameOrId{}, BootDisk: &oxide.InstanceDiskAttachment{ Description: "Created by Packer.", DiskSource: oxide.DiskSource{ diff --git a/component/data-source/image/data_source.go b/component/data-source/image/data_source.go new file mode 100644 index 0000000..9ac0f49 --- /dev/null +++ b/component/data-source/image/data_source.go @@ -0,0 +1,130 @@ +// 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/. + +//go:generate packer-sdc mapstructure-to-hcl2 -type Config,Output +//go:generate packer-sdc struct-markdown + +package image + +import ( + "context" + "errors" + "fmt" + "os" + + "github.com/hashicorp/hcl/v2/hcldec" + "github.com/hashicorp/packer-plugin-sdk/hcl2helper" + "github.com/hashicorp/packer-plugin-sdk/packer" + "github.com/hashicorp/packer-plugin-sdk/template/config" + "github.com/oxidecomputer/oxide.go/oxide" + "github.com/zclconf/go-cty/cty" +) + +// Config represents the Packer configuration for this plugin component. +type Config struct { + // Oxide API URL (e.g., silo.sys.example.com). + Host string `mapstructure:"host"` + + // Oxide API token. + Token string `mapstructure:"token"` + + // Name of the image to fetch. + Name string `mapstructure:"name"` + + // Name or ID of the project containing the image to fetch. Leave blank to fetch + // a silo image instead of a project image. + Project string `mapstructure:"project"` +} + +// Output represents the information returned by this plugin component for use +// in other Packer plugin components. +type Output struct { + // ID of the image that was fetched. + ImageID string `mapstructure:"image_id"` +} + +// Compile-time assertion to ensure the DataSource type implements the Packer +// data source component interface. +var _ packer.Datasource = (*DataSource)(nil) + +// DataSource is the concrete type that implements the Packer data source +// component interface. +type DataSource struct { + config Config +} + +// ConfigSpec returns the HCL specification that Packer uses to validate and +// configure this plugin component. +func (d *DataSource) ConfigSpec() hcldec.ObjectSpec { + return d.config.FlatMapstructure().HCL2Spec() +} + +// Configure decodes the configuration for this plugin component, checks whether +// the configuration is valid, and stores any necessary state for future methods +// to use during execution. +func (d *DataSource) Configure(args ...any) error { + if err := config.Decode(&d.config, nil, args...); err != nil { + return fmt.Errorf("failed decoding configuration: %w", err) + } + + var multiErr *packer.MultiError + + if d.config.Host == "" { + d.config.Host = os.Getenv("OXIDE_HOST") + } + + if d.config.Token == "" { + d.config.Token = os.Getenv("OXIDE_TOKEN") + } + + if d.config.Host == "" { + multiErr = packer.MultiErrorAppend(multiErr, errors.New("host is required")) + } + + if d.config.Token == "" { + multiErr = packer.MultiErrorAppend(multiErr, errors.New("token is required")) + } + + if d.config.Name == "" { + multiErr = packer.MultiErrorAppend(multiErr, errors.New("name is required")) + } + + if multiErr != nil && len(multiErr.Errors) > 0 { + return multiErr + } + + return nil +} + +// Execute fetches image information from the Oxide API and returns that +// information in the format specified by [OutputSpec]. +func (d *DataSource) Execute() (cty.Value, error) { + oxideClient, err := oxide.NewClient(&oxide.Config{ + Host: d.config.Host, + Token: d.config.Token, + }) + if err != nil { + return cty.NullVal(cty.EmptyObject), fmt.Errorf("failed creating oxide client: %w", err) + } + + image, err := oxideClient.ImageView(context.TODO(), oxide.ImageViewParams{ + Image: oxide.NameOrId(d.config.Name), + Project: oxide.NameOrId(d.config.Project), // This relies on the Go SDK omitting empty strings from serialization to fetch silo images. + }) + if err != nil { + return cty.NullVal(cty.EmptyObject), fmt.Errorf("failed fetching image %q within project %q: %w", d.config.Name, d.config.Project, err) + } + + output := Output{ + ImageID: image.Id, + } + + return hcl2helper.HCL2ValueFromConfig(output, d.OutputSpec()), nil +} + +// OutputSpec returns the HCL specification that Packer uses to populate output +// values for this plugin component. +func (d *DataSource) OutputSpec() hcldec.ObjectSpec { + return (&Output{}).FlatMapstructure().HCL2Spec() +} diff --git a/component/data-source/image/data_source.hcl2spec.go b/component/data-source/image/data_source.hcl2spec.go new file mode 100644 index 0000000..fb39889 --- /dev/null +++ b/component/data-source/image/data_source.hcl2spec.go @@ -0,0 +1,60 @@ +// Code generated by "packer-sdc mapstructure-to-hcl2"; DO NOT EDIT. + +package image + +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 { + Host *string `mapstructure:"host" cty:"host" hcl:"host"` + Token *string `mapstructure:"token" cty:"token" hcl:"token"` + Name *string `mapstructure:"name" cty:"name" hcl:"name"` + Project *string `mapstructure:"project" cty:"project" hcl:"project"` +} + +// 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{ + "host": &hcldec.AttrSpec{Name: "host", Type: cty.String, Required: false}, + "token": &hcldec.AttrSpec{Name: "token", Type: cty.String, Required: false}, + "name": &hcldec.AttrSpec{Name: "name", Type: cty.String, Required: false}, + "project": &hcldec.AttrSpec{Name: "project", Type: cty.String, Required: false}, + } + return s +} + +// FlatOutput is an auto-generated flat version of Output. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatOutput struct { + ImageID *string `mapstructure:"image_id" cty:"image_id" hcl:"image_id"` +} + +// FlatMapstructure returns a new FlatOutput. +// FlatOutput is an auto-generated flat version of Output. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*Output) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatOutput) +} + +// HCL2Spec returns the hcl spec of a Output. +// This spec is used by HCL to read the fields of Output. +// The decoded values from this spec will then be applied to a FlatOutput. +func (*FlatOutput) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "image_id": &hcldec.AttrSpec{Name: "image_id", Type: cty.String, Required: false}, + } + return s +} diff --git a/docs-partials/component/data-source/image/Config-not-required.mdx b/docs-partials/component/data-source/image/Config-not-required.mdx new file mode 100644 index 0000000..ea6bba7 --- /dev/null +++ b/docs-partials/component/data-source/image/Config-not-required.mdx @@ -0,0 +1,12 @@ + + +- `host` (string) - Oxide API URL (e.g., silo.sys.example.com). + +- `token` (string) - Oxide API token. + +- `name` (string) - Name of the image to fetch. + +- `project` (string) - Name or ID of the project containing the image to fetch. Leave blank to fetch + a silo image instead of a project image. + + diff --git a/docs-partials/component/data-source/image/Config.mdx b/docs-partials/component/data-source/image/Config.mdx new file mode 100644 index 0000000..f2a0ddf --- /dev/null +++ b/docs-partials/component/data-source/image/Config.mdx @@ -0,0 +1,5 @@ + + +Config represents the Packer configuration for this plugin component. + + diff --git a/docs-partials/component/data-source/image/DataSource.mdx b/docs-partials/component/data-source/image/DataSource.mdx new file mode 100644 index 0000000..8b5eb3e --- /dev/null +++ b/docs-partials/component/data-source/image/DataSource.mdx @@ -0,0 +1,6 @@ + + +DataSource is the concrete type that implements the Packer data source +component interface. + + diff --git a/docs-partials/component/data-source/image/Output-not-required.mdx b/docs-partials/component/data-source/image/Output-not-required.mdx new file mode 100644 index 0000000..d6d411a --- /dev/null +++ b/docs-partials/component/data-source/image/Output-not-required.mdx @@ -0,0 +1,5 @@ + + +- `image_id` (string) - ID of the image that was fetched. + + diff --git a/docs-partials/component/data-source/image/Output.mdx b/docs-partials/component/data-source/image/Output.mdx new file mode 100644 index 0000000..0c58532 --- /dev/null +++ b/docs-partials/component/data-source/image/Output.mdx @@ -0,0 +1,6 @@ + + +Output represents the information returned by this plugin component for use +in other Packer plugin components. + + diff --git a/main.go b/main.go index 607b2b1..a9c77c3 100644 --- a/main.go +++ b/main.go @@ -11,6 +11,7 @@ import ( "github.com/hashicorp/packer-plugin-sdk/plugin" "github.com/hashicorp/packer-plugin-sdk/version" "github.com/oxidecomputer/packer-plugin-oxide/component/builder/instance" + "github.com/oxidecomputer/packer-plugin-oxide/component/data-source/image" ) var ( @@ -30,6 +31,7 @@ var ( func main() { pluginSet := plugin.NewSet() pluginSet.RegisterBuilder("instance", new(instance.Builder)) + pluginSet.RegisterDatasource("image", new(image.DataSource)) pluginSet.SetVersion( version.NewPluginVersion(Version, VersionPreRelease, VersionMetadata), ) diff --git a/template.pkr.hcl b/template.pkr.hcl index 07ff0dd..df44fb1 100644 --- a/template.pkr.hcl +++ b/template.pkr.hcl @@ -1,15 +1,19 @@ packer { required_plugins { oxide = { - version = ">=v0.0.1" + version = ">= 0.0.1" source = "github.com/oxidecomputer/oxide" } } } +data "oxide-image" "ubuntu" { + name = "noble" +} + source "oxide-instance" "example" { project = "matthewsanabria" - image_id = "feb2c8ee-5a1d-4d66-beeb-289b860561bf" + image_id = data.oxide-image.ubuntu.image_id ssh_username = "ubuntu" ssh_agent_auth = true