diff --git a/.web-docs/components/builder/linode/README.md b/.web-docs/components/builder/linode/README.md index d50efbb0..8fe72fd8 100644 --- a/.web-docs/components/builder/linode/README.md +++ b/.web-docs/components/builder/linode/README.md @@ -99,6 +99,11 @@ can also be supplied to override the typical auto-generated key: - `cloud_init` (bool) - Whether the newly created image supports cloud-init. +- `firewall_id` (int) - The ID of the Firewall to attach this Linode to upon creation. + +- `metadata` ((Metadata)[#metadata]) - An object containing user-defined data relevant + to the creation of Linodes. + #### Interface This section outlines the fields configurable for a single interface object. @@ -123,6 +128,12 @@ VPC-specific fields: - `nat_1_1` (string) - The public IPv4 address assigned to this Linode to be 1:1 NATed with the VPC IPv4 address. +#### Metadata + +This section outlines the fields configurable for a single metadata object. + +- `user_data` (string) - Base64-encoded (cloud-config)[https://www.linode.com/docs/products/compute/compute-instances/guides/metadata-cloud-config/] data. + ## Examples ### Basic Example @@ -200,7 +211,9 @@ source "linode" "example" { region = "us-east" ssh_username = "root" private_ip = true + firewall_id = 12345 + instance_tags = ["abc", "foo=bar"] authorized_users = ["your_authorized_username"] authorized_keys = ["ssh-rsa AAAA_valid_public_ssh_key_123456785== user@their-computer"] stackscript_id = 1177256 @@ -220,6 +233,20 @@ source "linode" "example" { nat_1_1 = "any" } } + + metadata { + user_data = base64encode(< 0 { + t.Fatalf("bad: %#v", warnings) + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + expectedFirewallID := 123 + config["firewall_id"] = expectedFirewallID + + expectedUserData := "foo" + expectedMetadata := Metadata{ + UserData: expectedUserData, + } + config["metadata"] = map[string]string{ + "user_data": expectedUserData, + } + + expectedTags := []string{ + "foo", + "bar=baz", + ":!@#$%^&*()_+-=[]\\{}|;'\",./<>?`~", + } + config["instance_tags"] = expectedTags + + 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 !reflect.DeepEqual(b.config.FirewallID, expectedFirewallID) { + t.Errorf("got %v, expected %v", b.config.FirewallID, expectedFirewallID) + } + + if !reflect.DeepEqual(b.config.Metadata, expectedMetadata) { + t.Errorf("got %v, expected %v", b.config.Metadata, expectedMetadata) + } + + if !reflect.DeepEqual(b.config.Tags, expectedTags) { + t.Errorf("got %v, expected %v", b.config.Tags, expectedTags) + } +} diff --git a/builder/linode/config.go b/builder/linode/config.go index 9e28de51..39d2c1be 100644 --- a/builder/linode/config.go +++ b/builder/linode/config.go @@ -1,4 +1,4 @@ -//go:generate packer-sdc mapstructure-to-hcl2 -type Config,Interface,InterfaceIPv4 +//go:generate packer-sdc mapstructure-to-hcl2 -type Config,Interface,InterfaceIPv4,Metadata package linode @@ -24,6 +24,10 @@ type InterfaceIPv4 struct { NAT1To1 *string `mapstructure:"nat_1_1"` } +type Metadata struct { + UserData string `mapstructure:"user_data"` +} + type Interface struct { Purpose string `mapstructure:"purpose"` Label string `mapstructure:"label"` @@ -40,24 +44,26 @@ type Config struct { ctx interpolate.Context Comm communicator.Config `mapstructure:",squash"` - Interfaces []Interface `mapstructure:"interface"` + Interfaces []Interface `mapstructure:"interface" required:"false"` Region string `mapstructure:"region"` - AuthorizedKeys []string `mapstructure:"authorized_keys"` - AuthorizedUsers []string `mapstructure:"authorized_users"` + AuthorizedKeys []string `mapstructure:"authorized_keys" required:"false"` + AuthorizedUsers []string `mapstructure:"authorized_users" required:"false"` InstanceType string `mapstructure:"instance_type"` - Label string `mapstructure:"instance_label"` - Tags []string `mapstructure:"instance_tags"` + Label string `mapstructure:"instance_label" required:"false"` + Tags []string `mapstructure:"instance_tags" required:"false"` Image string `mapstructure:"image"` - SwapSize int `mapstructure:"swap_size"` - PrivateIP bool `mapstructure:"private_ip"` - RootPass string `mapstructure:"root_pass"` - ImageLabel string `mapstructure:"image_label"` - Description string `mapstructure:"image_description"` + SwapSize int `mapstructure:"swap_size" required:"false"` + PrivateIP bool `mapstructure:"private_ip" required:"false"` + RootPass string `mapstructure:"root_pass" required:"false"` + ImageLabel string `mapstructure:"image_label" required:"false"` + Description string `mapstructure:"image_description" required:"false"` StateTimeout time.Duration `mapstructure:"state_timeout" required:"false"` - StackScriptData map[string]string `mapstructure:"stackscript_data"` - StackScriptID int `mapstructure:"stackscript_id"` + StackScriptData map[string]string `mapstructure:"stackscript_data" required:"false"` + StackScriptID int `mapstructure:"stackscript_id" required:"false"` ImageCreateTimeout time.Duration `mapstructure:"image_create_timeout" required:"false"` CloudInit bool `mapstructure:"cloud_init" required:"false"` + Metadata Metadata `mapstructure:"metadata" required:"false"` + FirewallID int `mapstructure:"firewall_id" required:"false"` } func createRandomRootPassword() (string, error) { @@ -70,7 +76,7 @@ func createRandomRootPassword() (string, error) { return rootPass, nil } -func (c *Config) Prepare(raws ...interface{}) ([]string, error) { +func (c *Config) Prepare(raws ...any) ([]string, error) { if err := config.Decode(c, &config.DecodeOpts{ Interpolate: true, InterpolateContext: &c.ctx, @@ -157,7 +163,7 @@ func (c *Config) Prepare(raws ...interface{}) ([]string, error) { if c.Tags == nil { c.Tags = make([]string, 0) } - tagRe := regexp.MustCompile("^[[:alnum:]:_-]{1,255}$") + tagRe := regexp.MustCompile("^[[:print:]]{3,50}$") for _, t := range c.Tags { if !tagRe.MatchString(t) { diff --git a/builder/linode/config.hcl2spec.go b/builder/linode/config.hcl2spec.go index 3ca9481a..20585bc1 100644 --- a/builder/linode/config.hcl2spec.go +++ b/builder/linode/config.hcl2spec.go @@ -68,24 +68,26 @@ type FlatConfig struct { 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"` - Interfaces []FlatInterface `mapstructure:"interface" cty:"interface" hcl:"interface"` + Interfaces []FlatInterface `mapstructure:"interface" required:"false" cty:"interface" hcl:"interface"` Region *string `mapstructure:"region" cty:"region" hcl:"region"` - AuthorizedKeys []string `mapstructure:"authorized_keys" cty:"authorized_keys" hcl:"authorized_keys"` - AuthorizedUsers []string `mapstructure:"authorized_users" cty:"authorized_users" hcl:"authorized_users"` + AuthorizedKeys []string `mapstructure:"authorized_keys" required:"false" cty:"authorized_keys" hcl:"authorized_keys"` + AuthorizedUsers []string `mapstructure:"authorized_users" required:"false" cty:"authorized_users" hcl:"authorized_users"` InstanceType *string `mapstructure:"instance_type" cty:"instance_type" hcl:"instance_type"` - Label *string `mapstructure:"instance_label" cty:"instance_label" hcl:"instance_label"` - Tags []string `mapstructure:"instance_tags" cty:"instance_tags" hcl:"instance_tags"` + Label *string `mapstructure:"instance_label" required:"false" cty:"instance_label" hcl:"instance_label"` + Tags []string `mapstructure:"instance_tags" required:"false" cty:"instance_tags" hcl:"instance_tags"` Image *string `mapstructure:"image" cty:"image" hcl:"image"` - SwapSize *int `mapstructure:"swap_size" cty:"swap_size" hcl:"swap_size"` - PrivateIP *bool `mapstructure:"private_ip" cty:"private_ip" hcl:"private_ip"` - RootPass *string `mapstructure:"root_pass" cty:"root_pass" hcl:"root_pass"` - ImageLabel *string `mapstructure:"image_label" cty:"image_label" hcl:"image_label"` - Description *string `mapstructure:"image_description" cty:"image_description" hcl:"image_description"` + SwapSize *int `mapstructure:"swap_size" required:"false" cty:"swap_size" hcl:"swap_size"` + PrivateIP *bool `mapstructure:"private_ip" required:"false" cty:"private_ip" hcl:"private_ip"` + RootPass *string `mapstructure:"root_pass" required:"false" cty:"root_pass" hcl:"root_pass"` + ImageLabel *string `mapstructure:"image_label" required:"false" cty:"image_label" hcl:"image_label"` + Description *string `mapstructure:"image_description" required:"false" cty:"image_description" hcl:"image_description"` StateTimeout *string `mapstructure:"state_timeout" required:"false" cty:"state_timeout" hcl:"state_timeout"` - StackScriptData map[string]string `mapstructure:"stackscript_data" cty:"stackscript_data" hcl:"stackscript_data"` - StackScriptID *int `mapstructure:"stackscript_id" cty:"stackscript_id" hcl:"stackscript_id"` + StackScriptData map[string]string `mapstructure:"stackscript_data" required:"false" cty:"stackscript_data" hcl:"stackscript_data"` + StackScriptID *int `mapstructure:"stackscript_id" required:"false" cty:"stackscript_id" hcl:"stackscript_id"` ImageCreateTimeout *string `mapstructure:"image_create_timeout" required:"false" cty:"image_create_timeout" hcl:"image_create_timeout"` CloudInit *bool `mapstructure:"cloud_init" required:"false" cty:"cloud_init" hcl:"cloud_init"` + Metadata *FlatMetadata `mapstructure:"metadata" required:"false" cty:"metadata" hcl:"metadata"` + FirewallID *int `mapstructure:"firewall_id" required:"false" cty:"firewall_id" hcl:"firewall_id"` } // FlatMapstructure returns a new FlatConfig. @@ -176,6 +178,8 @@ func (*FlatConfig) HCL2Spec() map[string]hcldec.Spec { "stackscript_id": &hcldec.AttrSpec{Name: "stackscript_id", Type: cty.Number, Required: false}, "image_create_timeout": &hcldec.AttrSpec{Name: "image_create_timeout", Type: cty.String, Required: false}, "cloud_init": &hcldec.AttrSpec{Name: "cloud_init", Type: cty.Bool, Required: false}, + "metadata": &hcldec.BlockSpec{TypeName: "metadata", Nested: hcldec.ObjectSpec((*FlatMetadata)(nil).HCL2Spec())}, + "firewall_id": &hcldec.AttrSpec{Name: "firewall_id", Type: cty.Number, Required: false}, } return s } @@ -239,3 +243,26 @@ func (*FlatInterfaceIPv4) HCL2Spec() map[string]hcldec.Spec { } return s } + +// FlatMetadata is an auto-generated flat version of Metadata. +// Where the contents of a field with a `mapstructure:,squash` tag are bubbled up. +type FlatMetadata struct { + UserData *string `mapstructure:"user_data" cty:"user_data" hcl:"user_data"` +} + +// FlatMapstructure returns a new FlatMetadata. +// FlatMetadata is an auto-generated flat version of Metadata. +// Where the contents a fields with a `mapstructure:,squash` tag are bubbled up. +func (*Metadata) FlatMapstructure() interface{ HCL2Spec() map[string]hcldec.Spec } { + return new(FlatMetadata) +} + +// HCL2Spec returns the hcl spec of a Metadata. +// This spec is used by HCL to read the fields of Metadata. +// The decoded values from this spec will then be applied to a FlatMetadata. +func (*FlatMetadata) HCL2Spec() map[string]hcldec.Spec { + s := map[string]hcldec.Spec{ + "user_data": &hcldec.AttrSpec{Name: "user_data", Type: cty.String, Required: false}, + } + return s +} diff --git a/builder/linode/step_create_linode.go b/builder/linode/step_create_linode.go index b7a6a509..525dbdab 100644 --- a/builder/linode/step_create_linode.go +++ b/builder/linode/step_create_linode.go @@ -37,6 +37,16 @@ func flattenConfigInterface(i Interface) linodego.InstanceConfigInterfaceCreateO } } +func flattenMetadata(m Metadata) *linodego.InstanceMetadataOptions { + if m.UserData == "" { + return nil + } + + return &linodego.InstanceMetadataOptions{ + UserData: m.UserData, + } +} + func (s *stepCreateLinode) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction { c := state.Get("config").(*Config) ui := state.Get("ui").(packersdk.Ui) @@ -65,6 +75,9 @@ func (s *stepCreateLinode) Run(ctx context.Context, state multistep.StateBag) mu Label: c.Label, Image: c.Image, SwapSize: &c.SwapSize, + Tags: c.Tags, + FirewallID: c.FirewallID, + Metadata: flattenMetadata(c.Metadata), } if pubKey := string(c.Comm.SSHPublicKey); pubKey != "" { diff --git a/docs/builders/linode.mdx b/docs/builders/linode.mdx index 8db20116..4c2d2a8a 100644 --- a/docs/builders/linode.mdx +++ b/docs/builders/linode.mdx @@ -105,6 +105,11 @@ can also be supplied to override the typical auto-generated key: - `cloud_init` (bool) - Whether the newly created image supports cloud-init. +- `firewall_id` (int) - The ID of the Firewall to attach this Linode to upon creation. + +- `metadata` ((Metadata)[#metadata]) - An object containing user-defined data relevant + to the creation of Linodes. + #### Interface This section outlines the fields configurable for a single interface object. @@ -129,6 +134,12 @@ VPC-specific fields: - `nat_1_1` (string) - The public IPv4 address assigned to this Linode to be 1:1 NATed with the VPC IPv4 address. +#### Metadata + +This section outlines the fields configurable for a single metadata object. + +- `user_data` (string) - Base64-encoded (cloud-config)[https://www.linode.com/docs/products/compute/compute-instances/guides/metadata-cloud-config/] data. + ## Examples ### Basic Example @@ -206,7 +217,9 @@ source "linode" "example" { region = "us-east" ssh_username = "root" private_ip = true + firewall_id = 12345 + instance_tags = ["abc", "foo=bar"] authorized_users = ["your_authorized_username"] authorized_keys = ["ssh-rsa AAAA_valid_public_ssh_key_123456785== user@their-computer"] stackscript_id = 1177256 @@ -226,6 +239,20 @@ source "linode" "example" { nat_1_1 = "any" } } + + metadata { + user_data = base64encode(<