Skip to content

Commit 471213d

Browse files
Codexlots0logsCopilot
authored
Handle Linode machine_config_v2 user_data as plain text (#1)
* feat: add linode vpc and user data fields Co-authored-by: lots0logs <4675662+lots0logs@users.noreply.github.com> * chore: user data plain string handling Co-authored-by: lots0logs <4675662+lots0logs@users.noreply.github.com> * fix: send linode user data plain text Co-authored-by: lots0logs <4675662+lots0logs@users.noreply.github.com> * Apply suggestion from @Copilot Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * chore: validate linode interfaces subnet requirement Co-authored-by: lots0logs <4675662+lots0logs@users.noreply.github.com> * test: align linode fixture with schema constraints Co-authored-by: lots0logs <4675662+lots0logs@users.noreply.github.com> * Harden linode config validation Co-authored-by: lots0logs <4675662+lots0logs@users.noreply.github.com> * Enforce vpc subnet interface validation Co-authored-by: lots0logs <4675662+lots0logs@users.noreply.github.com> --------- Co-authored-by: openai-code-agent[bot] <242516109+Codex@users.noreply.github.com> Co-authored-by: lots0logs <4675662+lots0logs@users.noreply.github.com> Co-authored-by: Dustin Falgout <dustin@falgout.us> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent b115402 commit 471213d

5 files changed

Lines changed: 286 additions & 21 deletions

docs/resources/machine_config_v2.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,11 @@ The following attributes are exported:
236236

237237
* `authorized_users` - (Optional) Linode user accounts (seperated by commas) whose Linode SSH keys will be permitted root access to the created node. (string)
238238
* `create_private_ip` - (Optional) Create private IP for the instance. Default `false` (bool)
239+
* `use_interfaces` - (Optional) Enable Linode interface/VPC networking (new networking stack). Conflicts with `create_private_ip`. Default `false` (bool)
240+
* `vpc_subnet_id` - (Required with `use_interfaces`) VPC subnet ID to attach when interface networking is enabled. (string)
241+
* `vpc_private_ip` - (Optional) IPv4 address to request on the VPC interface when using interface networking. (string)
242+
* `public_interface_firewall_id` - (Optional) Firewall ID to attach to the public interface when using interface networking. (string)
243+
* `vpc_interface_firewall_id` - (Optional) Firewall ID to attach to the VPC interface when using interface networking. (string)
239244
* `docker_port` - (Optional) Docker Port. Default `2376` (string)
240245
* `image` - (Optional) Specifies the Linode Instance image which determines the OS distribution and base files. Default `linode/ubuntu18.04` (string)
241246
* `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:
246251
* `ssh_user` - (Optional) SSH username. Default `root` (string)
247252
* `stackscript` - (Optional) Specifies the Linode StackScript to use to create the instance. (string)
248253
* `stackscript_data` - (Optional) A JSON string specifying data for the selected StackScript. (string)
254+
* `user_data` - (Optional) Cloud-init user data for the Linode Metadata service; supply plain text. (string)
249255
* `swap_size` - (Optional) Linode Instance Swap Size (MB). Default `512` (string)
250256
* `tags` - (Optional) A comma separated list of tags to apply to the the Linode resource (string)
251257
* `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)

rancher2/resource_rancher2_machine_config_v2.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"context"
55
"fmt"
66
"log"
7+
"strings"
78
"time"
89

910
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
@@ -18,6 +19,45 @@ func resourceRancher2MachineConfigV2() *schema.Resource {
1819
Update: resourceRancher2MachineConfigV2Update,
1920
Delete: resourceRancher2MachineConfigV2Delete,
2021
Schema: machineConfigV2Fields(),
22+
CustomizeDiff: func(d *schema.ResourceDiff, i interface{}) error {
23+
v := d.Get("linode_config")
24+
configs, ok := v.([]interface{})
25+
if !ok || len(configs) == 0 || configs[0] == nil {
26+
return nil
27+
}
28+
29+
cfg, ok := configs[0].(map[string]interface{})
30+
if !ok {
31+
return nil
32+
}
33+
34+
useInterfaces, _ := cfg["use_interfaces"].(bool)
35+
36+
if useInterfaces {
37+
subnetID, ok := cfg["vpc_subnet_id"].(string)
38+
if !ok {
39+
// Value is unknown; skip validation to avoid false positives.
40+
return nil
41+
}
42+
if strings.TrimSpace(subnetID) == "" {
43+
return fmt.Errorf("linode_config.0.vpc_subnet_id must be set when linode_config.0.use_interfaces is true")
44+
}
45+
} else {
46+
if subnetID, ok := cfg["vpc_subnet_id"].(string); ok && strings.TrimSpace(subnetID) != "" {
47+
return fmt.Errorf("linode_config.0.use_interfaces must be true when linode_config.0.vpc_subnet_id is set")
48+
}
49+
if vpcIP, ok := cfg["vpc_private_ip"].(string); ok && strings.TrimSpace(vpcIP) != "" {
50+
return fmt.Errorf("linode_config.0.use_interfaces must be true when linode_config.0.vpc_private_ip is set")
51+
}
52+
if publicFirewallID, ok := cfg["public_interface_firewall_id"].(string); ok && strings.TrimSpace(publicFirewallID) != "" {
53+
return fmt.Errorf("linode_config.0.use_interfaces must be true when linode_config.0.public_interface_firewall_id is set")
54+
}
55+
if vpcFirewallID, ok := cfg["vpc_interface_firewall_id"].(string); ok && strings.TrimSpace(vpcFirewallID) != "" {
56+
return fmt.Errorf("linode_config.0.use_interfaces must be true when linode_config.0.vpc_interface_firewall_id is set")
57+
}
58+
}
59+
return nil
60+
},
2161
Timeouts: &schema.ResourceTimeout{
2262
Create: schema.DefaultTimeout(3 * time.Minute),
2363
Update: schema.DefaultTimeout(10 * time.Minute),

rancher2/schema_machine_config_v2_linode.go

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,12 @@ func machineConfigV2LinodeFields() map[string]*schema.Schema {
1515
Description: "Linode user accounts (seperated by commas) whose Linode SSH keys will be permitted root access to the created node",
1616
},
1717
"create_private_ip": {
18-
Type: schema.TypeBool,
19-
Optional: true,
20-
Default: false,
18+
Type: schema.TypeBool,
19+
Optional: true,
20+
Default: false,
21+
ConflictsWith: []string{
22+
"linode_config.0.use_interfaces",
23+
},
2124
Description: "Create private IP for the instance",
2225
},
2326
"docker_port": {
@@ -76,6 +79,11 @@ func machineConfigV2LinodeFields() map[string]*schema.Schema {
7679
Optional: true,
7780
Description: "A JSON string specifying data for the selected StackScript",
7881
},
82+
"user_data": {
83+
Type: schema.TypeString,
84+
Optional: true,
85+
Description: "Cloud-init user data for the Linode Metadata service (plain text; provider will encode as needed)",
86+
},
7987
"swap_size": {
8088
Type: schema.TypeString,
8189
Optional: true,
@@ -98,6 +106,36 @@ func machineConfigV2LinodeFields() map[string]*schema.Schema {
98106
Optional: true,
99107
Description: "Prefix the User-Agent in Linode API calls with some 'product/version'",
100108
},
109+
"use_interfaces": {
110+
Type: schema.TypeBool,
111+
Optional: true,
112+
Default: false,
113+
ConflictsWith: []string{
114+
"linode_config.0.create_private_ip",
115+
},
116+
Description: "Enable Linode interface/VPC networking instead of legacy private IP mode",
117+
},
118+
"vpc_subnet_id": {
119+
Type: schema.TypeString,
120+
Optional: true,
121+
RequiredWith: []string{"linode_config.0.use_interfaces"},
122+
Description: "VPC subnet ID to attach when using interface/VPC networking",
123+
},
124+
"vpc_private_ip": {
125+
Type: schema.TypeString,
126+
Optional: true,
127+
Description: "Optional IPv4 address to request on the VPC interface (interface networking only)",
128+
},
129+
"public_interface_firewall_id": {
130+
Type: schema.TypeString,
131+
Optional: true,
132+
Description: "Firewall ID to attach to the public interface when using interface networking",
133+
},
134+
"vpc_interface_firewall_id": {
135+
Type: schema.TypeString,
136+
Optional: true,
137+
Description: "Firewall ID to attach to the VPC interface when using interface networking",
138+
},
101139
}
102140

103141
return s

rancher2/structure_machine_config_v2_linode.go

Lines changed: 70 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -15,24 +15,30 @@ const (
1515
//Types
1616

1717
type machineConfigV2Linode struct {
18-
metav1.TypeMeta `json:",inline"`
19-
metav1.ObjectMeta `json:"metadata,omitempty"`
20-
AuthorizedUsers string `json:"authorizedUsers,omitempty" yaml:"authorizedUsers,omitempty"`
21-
CreatePrivateIP bool `json:"createPrivateIp,omitempty" yaml:"createPrivateIp,omitempty"`
22-
DockerPort string `json:"dockerPort,omitempty" yaml:"dockerPort,omitempty"`
23-
Image string `json:"image,omitempty" yaml:"image,omitempty"`
24-
InstanceType string `json:"instanceType,omitempty" yaml:"instanceType,omitempty"`
25-
Label string `json:"label,omitempty" yaml:"label,omitempty"`
26-
Region string `json:"region,omitempty" yaml:"region,omitempty"`
27-
RootPass string `json:"rootPass,omitempty" yaml:"rootPass,omitempty"`
28-
SSHPort string `json:"sshPort,omitempty" yaml:"sshPort,omitempty"`
29-
SSHUser string `json:"sshUser,omitempty" yaml:"sshUser,omitempty"`
30-
StackScript string `json:"stackscript,omitempty" yaml:"stackscript,omitempty"`
31-
StackscriptData string `json:"stackscriptData,omitempty" yaml:"stackscriptData,omitempty"`
32-
SwapSize string `json:"swapSize,omitempty" yaml:"swapSize,omitempty"`
33-
Tags string `json:"tags,omitempty" yaml:"tags,omitempty"`
34-
Token string `json:"token,omitempty" yaml:"token,omitempty"`
35-
UAPrefix string `json:"uaPrefix,omitempty" yaml:"uaPrefix,omitempty"`
18+
metav1.TypeMeta `json:",inline"`
19+
metav1.ObjectMeta `json:"metadata,omitempty"`
20+
AuthorizedUsers string `json:"authorizedUsers,omitempty" yaml:"authorizedUsers,omitempty"`
21+
CreatePrivateIP bool `json:"createPrivateIp,omitempty" yaml:"createPrivateIp,omitempty"`
22+
DockerPort string `json:"dockerPort,omitempty" yaml:"dockerPort,omitempty"`
23+
Image string `json:"image,omitempty" yaml:"image,omitempty"`
24+
InstanceType string `json:"instanceType,omitempty" yaml:"instanceType,omitempty"`
25+
Label string `json:"label,omitempty" yaml:"label,omitempty"`
26+
Region string `json:"region,omitempty" yaml:"region,omitempty"`
27+
RootPass string `json:"rootPass,omitempty" yaml:"rootPass,omitempty"`
28+
SSHPort string `json:"sshPort,omitempty" yaml:"sshPort,omitempty"`
29+
SSHUser string `json:"sshUser,omitempty" yaml:"sshUser,omitempty"`
30+
StackScript string `json:"stackscript,omitempty" yaml:"stackscript,omitempty"`
31+
StackscriptData string `json:"stackscriptData,omitempty" yaml:"stackscriptData,omitempty"`
32+
SwapSize string `json:"swapSize,omitempty" yaml:"swapSize,omitempty"`
33+
Tags string `json:"tags,omitempty" yaml:"tags,omitempty"`
34+
Token string `json:"token,omitempty" yaml:"token,omitempty"`
35+
UserData string `json:"userData,omitempty" yaml:"userData,omitempty"`
36+
UAPrefix string `json:"uaPrefix,omitempty" yaml:"uaPrefix,omitempty"`
37+
UseInterfaces bool `json:"useInterfaces,omitempty" yaml:"useInterfaces,omitempty"`
38+
VPCInterfaceFirewallID string `json:"vpcInterfaceFirewallId,omitempty" yaml:"vpcInterfaceFirewallId,omitempty"`
39+
VPCPrivateIP string `json:"vpcPrivateIp,omitempty" yaml:"vpcPrivateIp,omitempty"`
40+
VPCSubnetID string `json:"vpcSubnetId,omitempty" yaml:"vpcSubnetId,omitempty"`
41+
PublicInterfaceFirewallID string `json:"publicInterfaceFirewallId,omitempty" yaml:"publicInterfaceFirewallId,omitempty"`
3642
}
3743

3844
type MachineConfigV2Linode struct {
@@ -95,6 +101,10 @@ func flattenMachineConfigV2Linode(in *MachineConfigV2Linode) []interface{} {
95101
obj["stackscript_data"] = in.StackscriptData
96102
}
97103

104+
if len(in.UserData) > 0 {
105+
obj["user_data"] = in.UserData
106+
}
107+
98108
if len(in.SwapSize) > 0 {
99109
obj["swap_size"] = in.SwapSize
100110
}
@@ -107,6 +117,24 @@ func flattenMachineConfigV2Linode(in *MachineConfigV2Linode) []interface{} {
107117
obj["token"] = in.Token
108118
}
109119

120+
obj["use_interfaces"] = in.UseInterfaces
121+
122+
if len(in.VPCSubnetID) > 0 {
123+
obj["vpc_subnet_id"] = in.VPCSubnetID
124+
}
125+
126+
if len(in.VPCPrivateIP) > 0 {
127+
obj["vpc_private_ip"] = in.VPCPrivateIP
128+
}
129+
130+
if len(in.PublicInterfaceFirewallID) > 0 {
131+
obj["public_interface_firewall_id"] = in.PublicInterfaceFirewallID
132+
}
133+
134+
if len(in.VPCInterfaceFirewallID) > 0 {
135+
obj["vpc_interface_firewall_id"] = in.VPCInterfaceFirewallID
136+
}
137+
110138
if len(in.UAPrefix) > 0 {
111139
obj["ua_prefix"] = in.UAPrefix
112140
}
@@ -180,6 +208,10 @@ func expandMachineConfigV2Linode(p []interface{}, source *MachineConfigV2) *Mach
180208
obj.StackscriptData = v
181209
}
182210

211+
if v, ok := in["user_data"].(string); ok && len(v) > 0 {
212+
obj.UserData = v
213+
}
214+
183215
if v, ok := in["swap_size"].(string); ok && len(v) > 0 {
184216
obj.SwapSize = v
185217
}
@@ -192,6 +224,26 @@ func expandMachineConfigV2Linode(p []interface{}, source *MachineConfigV2) *Mach
192224
obj.Token = v
193225
}
194226

227+
if v, ok := in["use_interfaces"].(bool); ok {
228+
obj.UseInterfaces = v
229+
}
230+
231+
if v, ok := in["vpc_subnet_id"].(string); ok && len(v) > 0 {
232+
obj.VPCSubnetID = v
233+
}
234+
235+
if v, ok := in["vpc_private_ip"].(string); ok && len(v) > 0 {
236+
obj.VPCPrivateIP = v
237+
}
238+
239+
if v, ok := in["public_interface_firewall_id"].(string); ok && len(v) > 0 {
240+
obj.PublicInterfaceFirewallID = v
241+
}
242+
243+
if v, ok := in["vpc_interface_firewall_id"].(string); ok && len(v) > 0 {
244+
obj.VPCInterfaceFirewallID = v
245+
}
246+
195247
if v, ok := in["ua_prefix"].(string); ok && len(v) > 0 {
196248
obj.UAPrefix = v
197249
}
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
package rancher2
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
func TestFlattenMachineConfigV2Linode(t *testing.T) {
10+
input := &MachineConfigV2Linode{
11+
machineConfigV2Linode: machineConfigV2Linode{
12+
AuthorizedUsers: "user1,user2",
13+
CreatePrivateIP: false,
14+
DockerPort: "2377",
15+
Image: "linode/ubuntu22.04",
16+
InstanceType: "g6-standard-2",
17+
Label: "example",
18+
Region: "us-east",
19+
RootPass: "secret",
20+
SSHPort: "2222",
21+
SSHUser: "ubuntu",
22+
StackScript: "user/stack",
23+
StackscriptData: "{\"foo\":\"bar\"}",
24+
UserData: "#cloud-config\npackages:\n- htop\n",
25+
SwapSize: "256",
26+
Tags: "tag1,tag2",
27+
Token: "token",
28+
UAPrefix: "tf-provider/1.0",
29+
UseInterfaces: true,
30+
VPCSubnetID: "456",
31+
VPCPrivateIP: "10.0.0.10",
32+
PublicInterfaceFirewallID: "789",
33+
VPCInterfaceFirewallID: "321",
34+
},
35+
}
36+
37+
expected := []interface{}{
38+
map[string]interface{}{
39+
"authorized_users": "user1,user2",
40+
"create_private_ip": false,
41+
"docker_port": "2377",
42+
"image": "linode/ubuntu22.04",
43+
"instance_type": "g6-standard-2",
44+
"label": "example",
45+
"region": "us-east",
46+
"root_pass": "secret",
47+
"ssh_port": "2222",
48+
"ssh_user": "ubuntu",
49+
"stackscript": "user/stack",
50+
"stackscript_data": "{\"foo\":\"bar\"}",
51+
"user_data": "#cloud-config\npackages:\n- htop\n",
52+
"swap_size": "256",
53+
"tags": "tag1,tag2",
54+
"token": "token",
55+
"use_interfaces": true,
56+
"vpc_subnet_id": "456",
57+
"vpc_private_ip": "10.0.0.10",
58+
"public_interface_firewall_id": "789",
59+
"vpc_interface_firewall_id": "321",
60+
"ua_prefix": "tf-provider/1.0",
61+
},
62+
}
63+
64+
assert.Equal(t, expected, flattenMachineConfigV2Linode(input))
65+
}
66+
67+
func TestExpandMachineConfigV2Linode(t *testing.T) {
68+
input := []interface{}{
69+
map[string]interface{}{
70+
"authorized_users": "user1,user2",
71+
"create_private_ip": false,
72+
"docker_port": "2377",
73+
"image": "linode/ubuntu22.04",
74+
"instance_type": "g6-standard-2",
75+
"label": "example",
76+
"region": "us-east",
77+
"root_pass": "secret",
78+
"ssh_port": "2222",
79+
"ssh_user": "ubuntu",
80+
"stackscript": "user/stack",
81+
"stackscript_data": "{\"foo\":\"bar\"}",
82+
"user_data": "#cloud-config\npackages:\n- htop\n",
83+
"swap_size": "256",
84+
"tags": "tag1,tag2",
85+
"token": "token",
86+
"use_interfaces": true,
87+
"vpc_subnet_id": "456",
88+
"vpc_private_ip": "10.0.0.10",
89+
"public_interface_firewall_id": "789",
90+
"vpc_interface_firewall_id": "321",
91+
"ua_prefix": "tf-provider/1.0",
92+
},
93+
}
94+
95+
source := &MachineConfigV2{}
96+
got := expandMachineConfigV2Linode(input, source)
97+
98+
expected := &MachineConfigV2Linode{
99+
machineConfigV2Linode: machineConfigV2Linode{
100+
AuthorizedUsers: "user1,user2",
101+
CreatePrivateIP: false,
102+
DockerPort: "2377",
103+
Image: "linode/ubuntu22.04",
104+
InstanceType: "g6-standard-2",
105+
Label: "example",
106+
Region: "us-east",
107+
RootPass: "secret",
108+
SSHPort: "2222",
109+
SSHUser: "ubuntu",
110+
StackScript: "user/stack",
111+
StackscriptData: "{\"foo\":\"bar\"}",
112+
UserData: "#cloud-config\npackages:\n- htop\n",
113+
SwapSize: "256",
114+
Tags: "tag1,tag2",
115+
Token: "token",
116+
UAPrefix: "tf-provider/1.0",
117+
UseInterfaces: true,
118+
VPCSubnetID: "456",
119+
VPCPrivateIP: "10.0.0.10",
120+
PublicInterfaceFirewallID: "789",
121+
VPCInterfaceFirewallID: "321",
122+
},
123+
}
124+
expected.TypeMeta.Kind = machineConfigV2LinodeKind
125+
expected.TypeMeta.APIVersion = machineConfigV2LinodeAPIVersion
126+
127+
assert.Equal(t, expected, got)
128+
assert.Equal(t, expected.TypeMeta, source.TypeMeta)
129+
}

0 commit comments

Comments
 (0)