diff --git a/docs/resources/machine_config_v2.md b/docs/resources/machine_config_v2.md index 68d248b6c..79fae0124 100644 --- a/docs/resources/machine_config_v2.md +++ b/docs/resources/machine_config_v2.md @@ -236,6 +236,11 @@ The following attributes are exported: * `authorized_users` - (Optional) Linode user accounts (seperated by commas) whose Linode SSH keys will be permitted root access to the created node. (string) * `create_private_ip` - (Optional) Create private IP for the instance. Default `false` (bool) +* `use_interfaces` - (Optional) Enable Linode interface/VPC networking (new networking stack). Conflicts with `create_private_ip`. Default `false` (bool) +* `vpc_subnet_id` - (Required with `use_interfaces`) VPC subnet ID to attach when interface networking is enabled. (string) +* `vpc_private_ip` - (Optional) IPv4 address to request on the VPC interface when using interface networking. (string) +* `public_interface_firewall_id` - (Optional) Firewall ID to attach to the public interface when using interface networking. (string) +* `vpc_interface_firewall_id` - (Optional) Firewall ID to attach to the VPC interface when using interface networking. (string) * `docker_port` - (Optional) Docker Port. Default `2376` (string) * `image` - (Optional) Specifies the Linode Instance image which determines the OS distribution and base files. Default `linode/ubuntu18.04` (string) * `instance_type` - (Optional) Specifies the Linode Instance type which determines CPU, memory, disk size, etc. Default `g6-standard-4` (string) @@ -246,6 +251,7 @@ The following attributes are exported: * `ssh_user` - (Optional) SSH username. Default `root` (string) * `stackscript` - (Optional) Specifies the Linode StackScript to use to create the instance. (string) * `stackscript_data` - (Optional) A JSON string specifying data for the selected StackScript. (string) +* `user_data` - (Optional) Cloud-init user data for the Linode Metadata service; supply plain text. (string) * `swap_size` - (Optional) Linode Instance Swap Size (MB). Default `512` (string) * `tags` - (Optional) A comma separated list of tags to apply to the the Linode resource (string) * `token` - (Optional/Sensitive) Linode API token. Mandatory on Rancher v2.0.x and v2.1.x. Use `rancher2_cloud_credential` from Rancher v2.2.x (string) diff --git a/rancher2/resource_rancher2_machine_config_v2.go b/rancher2/resource_rancher2_machine_config_v2.go index 3698a99e1..d2bf45ab2 100644 --- a/rancher2/resource_rancher2_machine_config_v2.go +++ b/rancher2/resource_rancher2_machine_config_v2.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "log" + "strings" "time" "github.com/hashicorp/terraform-plugin-sdk/helper/resource" @@ -18,6 +19,42 @@ func resourceRancher2MachineConfigV2() *schema.Resource { Update: resourceRancher2MachineConfigV2Update, Delete: resourceRancher2MachineConfigV2Delete, Schema: machineConfigV2Fields(), + CustomizeDiff: func(d *schema.ResourceDiff, i interface{}) error { + v := d.Get("linode_config") + configs, ok := v.([]interface{}) + if !ok || len(configs) == 0 || configs[0] == nil { + return nil + } + + cfg, ok := configs[0].(map[string]interface{}) + if !ok { + return nil + } + + useInterfaces, _ := cfg["use_interfaces"].(bool) + + if useInterfaces { + subnetID, ok := cfg["vpc_subnet_id"].(string) + // Treat missing or non-string values as empty to ensure validation + if !ok || strings.TrimSpace(subnetID) == "" { + return fmt.Errorf("linode_config.0.vpc_subnet_id must be set when linode_config.0.use_interfaces is true") + } + } else { + if subnetID, ok := cfg["vpc_subnet_id"].(string); ok && strings.TrimSpace(subnetID) != "" { + return fmt.Errorf("linode_config.0.use_interfaces must be true when linode_config.0.vpc_subnet_id is set") + } + if vpcIP, ok := cfg["vpc_private_ip"].(string); ok && strings.TrimSpace(vpcIP) != "" { + return fmt.Errorf("linode_config.0.use_interfaces must be true when linode_config.0.vpc_private_ip is set") + } + if publicFirewallID, ok := cfg["public_interface_firewall_id"].(string); ok && strings.TrimSpace(publicFirewallID) != "" { + return fmt.Errorf("linode_config.0.use_interfaces must be true when linode_config.0.public_interface_firewall_id is set") + } + if vpcFirewallID, ok := cfg["vpc_interface_firewall_id"].(string); ok && strings.TrimSpace(vpcFirewallID) != "" { + return fmt.Errorf("linode_config.0.use_interfaces must be true when linode_config.0.vpc_interface_firewall_id is set") + } + } + return nil + }, Timeouts: &schema.ResourceTimeout{ Create: schema.DefaultTimeout(3 * time.Minute), Update: schema.DefaultTimeout(10 * time.Minute), diff --git a/rancher2/schema_machine_config_v2_linode.go b/rancher2/schema_machine_config_v2_linode.go index ea07c6334..c4718dbd3 100644 --- a/rancher2/schema_machine_config_v2_linode.go +++ b/rancher2/schema_machine_config_v2_linode.go @@ -15,9 +15,12 @@ func machineConfigV2LinodeFields() map[string]*schema.Schema { Description: "Linode user accounts (seperated by commas) whose Linode SSH keys will be permitted root access to the created node", }, "create_private_ip": { - Type: schema.TypeBool, - Optional: true, - Default: false, + Type: schema.TypeBool, + Optional: true, + Default: false, + ConflictsWith: []string{ + "linode_config.0.use_interfaces", + }, Description: "Create private IP for the instance", }, "docker_port": { @@ -76,6 +79,11 @@ func machineConfigV2LinodeFields() map[string]*schema.Schema { Optional: true, Description: "A JSON string specifying data for the selected StackScript", }, + "user_data": { + Type: schema.TypeString, + Optional: true, + Description: "Cloud-init user data for the Linode Metadata service", + }, "swap_size": { Type: schema.TypeString, Optional: true, @@ -98,6 +106,36 @@ func machineConfigV2LinodeFields() map[string]*schema.Schema { Optional: true, Description: "Prefix the User-Agent in Linode API calls with some 'product/version'", }, + "use_interfaces": { + Type: schema.TypeBool, + Optional: true, + Default: false, + ConflictsWith: []string{ + "linode_config.0.create_private_ip", + }, + Description: "Enable Linode interface/VPC networking instead of legacy private IP mode", + }, + "vpc_subnet_id": { + Type: schema.TypeString, + Optional: true, + RequiredWith: []string{"linode_config.0.use_interfaces"}, + Description: "VPC subnet ID to attach when using interface/VPC networking", + }, + "vpc_private_ip": { + Type: schema.TypeString, + Optional: true, + Description: "Optional IPv4 address to request on the VPC interface (interface networking only)", + }, + "public_interface_firewall_id": { + Type: schema.TypeString, + Optional: true, + Description: "Firewall ID to attach to the public interface when using interface networking", + }, + "vpc_interface_firewall_id": { + Type: schema.TypeString, + Optional: true, + Description: "Firewall ID to attach to the VPC interface when using interface networking", + }, } return s diff --git a/rancher2/structure_machine_config_v2_linode.go b/rancher2/structure_machine_config_v2_linode.go index 7c9ca6b3d..0fabd253a 100644 --- a/rancher2/structure_machine_config_v2_linode.go +++ b/rancher2/structure_machine_config_v2_linode.go @@ -15,24 +15,30 @@ const ( //Types type machineConfigV2Linode struct { - metav1.TypeMeta `json:",inline"` - metav1.ObjectMeta `json:"metadata,omitempty"` - AuthorizedUsers string `json:"authorizedUsers,omitempty" yaml:"authorizedUsers,omitempty"` - CreatePrivateIP bool `json:"createPrivateIp,omitempty" yaml:"createPrivateIp,omitempty"` - DockerPort string `json:"dockerPort,omitempty" yaml:"dockerPort,omitempty"` - Image string `json:"image,omitempty" yaml:"image,omitempty"` - InstanceType string `json:"instanceType,omitempty" yaml:"instanceType,omitempty"` - Label string `json:"label,omitempty" yaml:"label,omitempty"` - Region string `json:"region,omitempty" yaml:"region,omitempty"` - RootPass string `json:"rootPass,omitempty" yaml:"rootPass,omitempty"` - SSHPort string `json:"sshPort,omitempty" yaml:"sshPort,omitempty"` - SSHUser string `json:"sshUser,omitempty" yaml:"sshUser,omitempty"` - StackScript string `json:"stackscript,omitempty" yaml:"stackscript,omitempty"` - StackscriptData string `json:"stackscriptData,omitempty" yaml:"stackscriptData,omitempty"` - SwapSize string `json:"swapSize,omitempty" yaml:"swapSize,omitempty"` - Tags string `json:"tags,omitempty" yaml:"tags,omitempty"` - Token string `json:"token,omitempty" yaml:"token,omitempty"` - UAPrefix string `json:"uaPrefix,omitempty" yaml:"uaPrefix,omitempty"` + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + AuthorizedUsers string `json:"authorizedUsers,omitempty" yaml:"authorizedUsers,omitempty"` + CreatePrivateIP bool `json:"createPrivateIp,omitempty" yaml:"createPrivateIp,omitempty"` + DockerPort string `json:"dockerPort,omitempty" yaml:"dockerPort,omitempty"` + Image string `json:"image,omitempty" yaml:"image,omitempty"` + InstanceType string `json:"instanceType,omitempty" yaml:"instanceType,omitempty"` + Label string `json:"label,omitempty" yaml:"label,omitempty"` + Region string `json:"region,omitempty" yaml:"region,omitempty"` + RootPass string `json:"rootPass,omitempty" yaml:"rootPass,omitempty"` + SSHPort string `json:"sshPort,omitempty" yaml:"sshPort,omitempty"` + SSHUser string `json:"sshUser,omitempty" yaml:"sshUser,omitempty"` + StackScript string `json:"stackscript,omitempty" yaml:"stackscript,omitempty"` + StackscriptData string `json:"stackscriptData,omitempty" yaml:"stackscriptData,omitempty"` + SwapSize string `json:"swapSize,omitempty" yaml:"swapSize,omitempty"` + Tags string `json:"tags,omitempty" yaml:"tags,omitempty"` + Token string `json:"token,omitempty" yaml:"token,omitempty"` + UserData string `json:"userData,omitempty" yaml:"userData,omitempty"` + UAPrefix string `json:"uaPrefix,omitempty" yaml:"uaPrefix,omitempty"` + UseInterfaces bool `json:"useInterfaces,omitempty" yaml:"useInterfaces,omitempty"` + VPCInterfaceFirewallID string `json:"vpcInterfaceFirewallId,omitempty" yaml:"vpcInterfaceFirewallId,omitempty"` + VPCPrivateIP string `json:"vpcPrivateIp,omitempty" yaml:"vpcPrivateIp,omitempty"` + VPCSubnetID string `json:"vpcSubnetId,omitempty" yaml:"vpcSubnetId,omitempty"` + PublicInterfaceFirewallID string `json:"publicInterfaceFirewallId,omitempty" yaml:"publicInterfaceFirewallId,omitempty"` } type MachineConfigV2Linode struct { @@ -95,6 +101,10 @@ func flattenMachineConfigV2Linode(in *MachineConfigV2Linode) []interface{} { obj["stackscript_data"] = in.StackscriptData } + if len(in.UserData) > 0 { + obj["user_data"] = in.UserData + } + if len(in.SwapSize) > 0 { obj["swap_size"] = in.SwapSize } @@ -107,6 +117,24 @@ func flattenMachineConfigV2Linode(in *MachineConfigV2Linode) []interface{} { obj["token"] = in.Token } + obj["use_interfaces"] = in.UseInterfaces + + if len(in.VPCSubnetID) > 0 { + obj["vpc_subnet_id"] = in.VPCSubnetID + } + + if len(in.VPCPrivateIP) > 0 { + obj["vpc_private_ip"] = in.VPCPrivateIP + } + + if len(in.PublicInterfaceFirewallID) > 0 { + obj["public_interface_firewall_id"] = in.PublicInterfaceFirewallID + } + + if len(in.VPCInterfaceFirewallID) > 0 { + obj["vpc_interface_firewall_id"] = in.VPCInterfaceFirewallID + } + if len(in.UAPrefix) > 0 { obj["ua_prefix"] = in.UAPrefix } @@ -180,6 +208,10 @@ func expandMachineConfigV2Linode(p []interface{}, source *MachineConfigV2) *Mach obj.StackscriptData = v } + if v, ok := in["user_data"].(string); ok && len(v) > 0 { + obj.UserData = v + } + if v, ok := in["swap_size"].(string); ok && len(v) > 0 { obj.SwapSize = v } @@ -192,6 +224,26 @@ func expandMachineConfigV2Linode(p []interface{}, source *MachineConfigV2) *Mach obj.Token = v } + if v, ok := in["use_interfaces"].(bool); ok { + obj.UseInterfaces = v + } + + if v, ok := in["vpc_subnet_id"].(string); ok && len(v) > 0 { + obj.VPCSubnetID = v + } + + if v, ok := in["vpc_private_ip"].(string); ok && len(v) > 0 { + obj.VPCPrivateIP = v + } + + if v, ok := in["public_interface_firewall_id"].(string); ok && len(v) > 0 { + obj.PublicInterfaceFirewallID = v + } + + if v, ok := in["vpc_interface_firewall_id"].(string); ok && len(v) > 0 { + obj.VPCInterfaceFirewallID = v + } + if v, ok := in["ua_prefix"].(string); ok && len(v) > 0 { obj.UAPrefix = v } diff --git a/rancher2/structure_machine_config_v2_linode_test.go b/rancher2/structure_machine_config_v2_linode_test.go new file mode 100644 index 000000000..7ca4a349e --- /dev/null +++ b/rancher2/structure_machine_config_v2_linode_test.go @@ -0,0 +1,129 @@ +package rancher2 + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestFlattenMachineConfigV2Linode(t *testing.T) { + input := &MachineConfigV2Linode{ + machineConfigV2Linode: machineConfigV2Linode{ + AuthorizedUsers: "user1,user2", + CreatePrivateIP: false, + DockerPort: "2377", + Image: "linode/ubuntu22.04", + InstanceType: "g6-standard-2", + Label: "example", + Region: "us-east", + RootPass: "secret", + SSHPort: "2222", + SSHUser: "ubuntu", + StackScript: "user/stack", + StackscriptData: "{\"foo\":\"bar\"}", + UserData: "#cloud-config\npackages:\n- htop\n", + SwapSize: "256", + Tags: "tag1,tag2", + Token: "token", + UAPrefix: "tf-provider/1.0", + UseInterfaces: true, + VPCSubnetID: "456", + VPCPrivateIP: "10.0.0.10", + PublicInterfaceFirewallID: "789", + VPCInterfaceFirewallID: "321", + }, + } + + expected := []interface{}{ + map[string]interface{}{ + "authorized_users": "user1,user2", + "create_private_ip": false, + "docker_port": "2377", + "image": "linode/ubuntu22.04", + "instance_type": "g6-standard-2", + "label": "example", + "region": "us-east", + "root_pass": "secret", + "ssh_port": "2222", + "ssh_user": "ubuntu", + "stackscript": "user/stack", + "stackscript_data": "{\"foo\":\"bar\"}", + "user_data": "#cloud-config\npackages:\n- htop\n", + "swap_size": "256", + "tags": "tag1,tag2", + "token": "token", + "use_interfaces": true, + "vpc_subnet_id": "456", + "vpc_private_ip": "10.0.0.10", + "public_interface_firewall_id": "789", + "vpc_interface_firewall_id": "321", + "ua_prefix": "tf-provider/1.0", + }, + } + + assert.Equal(t, expected, flattenMachineConfigV2Linode(input)) +} + +func TestExpandMachineConfigV2Linode(t *testing.T) { + input := []interface{}{ + map[string]interface{}{ + "authorized_users": "user1,user2", + "create_private_ip": false, + "docker_port": "2377", + "image": "linode/ubuntu22.04", + "instance_type": "g6-standard-2", + "label": "example", + "region": "us-east", + "root_pass": "secret", + "ssh_port": "2222", + "ssh_user": "ubuntu", + "stackscript": "user/stack", + "stackscript_data": "{\"foo\":\"bar\"}", + "user_data": "#cloud-config\npackages:\n- htop\n", + "swap_size": "256", + "tags": "tag1,tag2", + "token": "token", + "use_interfaces": true, + "vpc_subnet_id": "456", + "vpc_private_ip": "10.0.0.10", + "public_interface_firewall_id": "789", + "vpc_interface_firewall_id": "321", + "ua_prefix": "tf-provider/1.0", + }, + } + + source := &MachineConfigV2{} + got := expandMachineConfigV2Linode(input, source) + + expected := &MachineConfigV2Linode{ + machineConfigV2Linode: machineConfigV2Linode{ + AuthorizedUsers: "user1,user2", + CreatePrivateIP: false, + DockerPort: "2377", + Image: "linode/ubuntu22.04", + InstanceType: "g6-standard-2", + Label: "example", + Region: "us-east", + RootPass: "secret", + SSHPort: "2222", + SSHUser: "ubuntu", + StackScript: "user/stack", + StackscriptData: "{\"foo\":\"bar\"}", + UserData: "#cloud-config\npackages:\n- htop\n", + SwapSize: "256", + Tags: "tag1,tag2", + Token: "token", + UAPrefix: "tf-provider/1.0", + UseInterfaces: true, + VPCSubnetID: "456", + VPCPrivateIP: "10.0.0.10", + PublicInterfaceFirewallID: "789", + VPCInterfaceFirewallID: "321", + }, + } + expected.TypeMeta.Kind = machineConfigV2LinodeKind + expected.TypeMeta.APIVersion = machineConfigV2LinodeAPIVersion + + assert.Equal(t, expected, got) + assert.Equal(t, expected.TypeMeta, source.TypeMeta) +}