Skip to content

Commit a9e2731

Browse files
committed
merge: combine cloud-init and vpc support
2 parents 14fedda + 014aa8c commit a9e2731

3 files changed

Lines changed: 353 additions & 22 deletions

File tree

README.md

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,9 +64,19 @@ docker-machine create -d linode --linode-token=<linode-token> linode
6464
| `linode-stackscript-data` | `LINODE_STACKSCRIPT_DATA` | None | A JSON string specifying data that is passed (via UDF) to the selected StackScript.
6565
| `linode-user-data` | `LINODE_USER_DATA` | None | Path to a cloud-init user-data file passed to the Linode Metadata service. File contents are base64-encoded automatically.
6666
| `linode-create-private-ip` | `LINODE_CREATE_PRIVATE_IP` | None | A flag specifying to create private IP for the Linode instance.
67+
| `linode-use-interfaces` | `LINODE_USE_INTERFACES` | None | Opt-in to Linode's interface/VPC networking stack (requires `linode-vpc-subnet-id`; conflicts with `linode-create-private-ip`).
68+
| `linode-vpc-subnet-id` | `LINODE_VPC_SUBNET_ID` | None | VPC subnet ID to attach when using interface networking.
69+
| `linode-vpc-private-ip` | `LINODE_VPC_PRIVATE_IP` | None | Optional IPv4 address to request on the VPC interface (requires `linode-use-interfaces`).
70+
| `linode-vpc-interface-firewall-id` | `LINODE_VPC_INTERFACE_FIREWALL_ID` | None | Firewall ID to attach to the VPC interface when using the interface/VPC networking stack.
71+
| `linode-public-interface-firewall-id` | `LINODE_PUBLIC_INTERFACE_FIREWALL_ID` | None | Firewall ID to attach to the public interface when using the interface/VPC networking stack.
6772
| `linode-tags` | `LINODE_TAGS` | None | A comma separated list of tags to apply to the Linode resource
6873
| `linode-ua-prefix` | `LINODE_UA_PREFIX` | None | Prefix the User-Agent in Linode API calls with some 'product/version'
6974

75+
## Networking Modes
76+
77+
- **Legacy (default):** uses public networking and optionally `--linode-create-private-ip` to attach a private address.
78+
- **Interface/VPC (opt-in):** enable with `--linode-use-interfaces` plus `--linode-vpc-subnet-id`. If your account does not define default interface firewalls, set `--linode-public-interface-firewall-id` and/or `--linode-vpc-interface-firewall-id` to avoid API errors. This mode is incompatible with `--linode-create-private-ip`.
79+
7080
## Notes
7181

7282
* When using the `linode/containerlinux` `linode-image`, the `linode-ssh-user` will default to `core`
@@ -127,6 +137,24 @@ Are you sure? (y/n): y
127137
Successfully removed linode
128138
```
129139

140+
### Interface/VPC Networking Example
141+
142+
Use Linode's newer interface generation to attach a VPC subnet while keeping the public interface for provisioning traffic.
143+
144+
```bash
145+
docker-machine create \
146+
-d linode \
147+
--linode-token=$LINODE_TOKEN \
148+
--linode-use-interfaces \
149+
--linode-vpc-subnet-id=67890 \
150+
--linode-vpc-private-ip=10.0.0.25 \
151+
linode-vpc
152+
```
153+
154+
If your account does not have default interface firewalls configured, include `--linode-public-interface-firewall-id=<firewall-id>` and/or `--linode-vpc-interface-firewall-id=<firewall-id>` to satisfy the Linode API requirement.
155+
156+
The `--linode-use-interfaces` flag is incompatible with `--linode-create-private-ip` to keep networking behavior deterministic. Omit `--linode-vpc-private-ip` to request an automatically assigned address from the subnet. Without `--linode-use-interfaces`, legacy networking remains the default.
157+
130158
### Provisioning Docker Swarm
131159

132160
The following script serves as an example for creating a [Docker Swarm](https://docs.docker.com/engine/swarm/) with master and worker nodes using the Linode Docker machine driver and private networking.

pkg/drivers/linode/linode.go

Lines changed: 186 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,17 @@ type Driver struct {
2828
*drivers.BaseDriver
2929
client *linodego.Client
3030

31-
APIToken string
32-
UserAgentPrefix string
33-
IPAddress string
34-
PrivateIPAddress string
35-
CreatePrivateIP bool
36-
DockerPort int
31+
APIToken string
32+
UserAgentPrefix string
33+
IPAddress string
34+
PrivateIPAddress string
35+
CreatePrivateIP bool
36+
UseInterfaces bool
37+
VPCSubnetID int
38+
VPCPrivateIP string
39+
VPCInterfaceFirewallID int
40+
PublicInterfaceFirewallID int
41+
DockerPort int
3742

3843
InstanceID int
3944
InstanceLabel string
@@ -124,6 +129,19 @@ func createRandomRootPassword() (string, error) {
124129
return rootPass, nil
125130
}
126131

132+
// FirewallID is a **int in linodego so callers can distinguish between
133+
// omitting the field entirely and explicitly sending a value.
134+
func firewallIDPtr(id int) **int {
135+
if id == 0 {
136+
return nil
137+
}
138+
139+
value := id
140+
valuePtr := &value
141+
142+
return &valuePtr
143+
}
144+
127145
// DriverName returns the name of the driver
128146
func (d *Driver) DriverName() string {
129147
return "linode"
@@ -233,6 +251,31 @@ func (d *Driver) GetCreateFlags() []mcnflag.Flag {
233251
Name: "linode-create-private-ip",
234252
Usage: "Create private IP for the instance",
235253
},
254+
mcnflag.BoolFlag{
255+
EnvVar: "LINODE_USE_INTERFACES",
256+
Name: "linode-use-interfaces",
257+
Usage: "Enable Linode interface/VPC networking (opt-in, keeps legacy defaults otherwise)",
258+
},
259+
mcnflag.IntFlag{
260+
EnvVar: "LINODE_VPC_SUBNET_ID",
261+
Name: "linode-vpc-subnet-id",
262+
Usage: "VPC subnet ID to attach when using interface/VPC networking",
263+
},
264+
mcnflag.StringFlag{
265+
EnvVar: "LINODE_VPC_PRIVATE_IP",
266+
Name: "linode-vpc-private-ip",
267+
Usage: "Optional IPv4 address to request on the VPC interface (requires --linode-use-interfaces)",
268+
},
269+
mcnflag.IntFlag{
270+
EnvVar: "LINODE_PUBLIC_INTERFACE_FIREWALL_ID",
271+
Name: "linode-public-interface-firewall-id",
272+
Usage: "Firewall ID to attach to the public interface when using interface/VPC networking",
273+
},
274+
mcnflag.IntFlag{
275+
EnvVar: "LINODE_VPC_INTERFACE_FIREWALL_ID",
276+
Name: "linode-vpc-interface-firewall-id",
277+
Usage: "Firewall ID to attach to the VPC interface when using interface/VPC networking",
278+
},
236279
mcnflag.StringFlag{
237280
EnvVar: "LINODE_UA_PREFIX",
238281
Name: "linode-ua-prefix",
@@ -283,6 +326,11 @@ func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error {
283326
d.SwapSize = flags.Int("linode-swap-size")
284327
d.DockerPort = flags.Int("linode-docker-port")
285328
d.CreatePrivateIP = flags.Bool("linode-create-private-ip")
329+
d.UseInterfaces = flags.Bool("linode-use-interfaces")
330+
d.VPCSubnetID = flags.Int("linode-vpc-subnet-id")
331+
d.VPCPrivateIP = strings.TrimSpace(flags.String("linode-vpc-private-ip"))
332+
d.VPCInterfaceFirewallID = flags.Int("linode-vpc-interface-firewall-id")
333+
d.PublicInterfaceFirewallID = flags.Int("linode-public-interface-firewall-id")
286334
d.UserAgentPrefix = flags.String("linode-ua-prefix")
287335
d.Tags = flags.String("linode-tags")
288336

@@ -337,6 +385,34 @@ func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error {
337385

338386
d.InstanceLabel = newLabel
339387

388+
if d.PublicInterfaceFirewallID < 0 {
389+
return fmt.Errorf("invalid value for --linode-public-interface-firewall-id: must be zero or positive")
390+
}
391+
if d.VPCInterfaceFirewallID < 0 {
392+
return fmt.Errorf("invalid value for --linode-vpc-interface-firewall-id: must be zero or positive")
393+
}
394+
395+
if d.UseInterfaces && d.CreatePrivateIP {
396+
return fmt.Errorf("cannot combine --linode-use-interfaces with --linode-create-private-ip; choose one networking mode")
397+
}
398+
399+
if d.UseInterfaces {
400+
if d.VPCSubnetID == 0 {
401+
return fmt.Errorf("linode interface networking requires --linode-vpc-subnet-id")
402+
}
403+
404+
if d.VPCPrivateIP != "" {
405+
parsed := net.ParseIP(d.VPCPrivateIP)
406+
if parsed == nil || parsed.To4() == nil {
407+
return fmt.Errorf("linode VPC private IP must be a valid IPv4 address")
408+
}
409+
}
410+
} else {
411+
if d.VPCSubnetID != 0 || d.VPCPrivateIP != "" || d.PublicInterfaceFirewallID != 0 || d.VPCInterfaceFirewallID != 0 {
412+
return fmt.Errorf("VPC/interface options require --linode-use-interfaces to be set")
413+
}
414+
}
415+
340416
return nil
341417
}
342418

@@ -426,6 +502,10 @@ func (d *Driver) Create() error {
426502
log.Infof("Using SSH port %d", d.SSHPort)
427503
}
428504

505+
if d.UseInterfaces {
506+
log.Infof("Using interface/VPC networking (subnet %d)", d.VPCSubnetID)
507+
}
508+
429509
publicKey, err := d.createSSHKey()
430510
if err != nil {
431511
return err
@@ -467,6 +547,38 @@ func (d *Driver) Create() error {
467547
}
468548
}
469549

550+
if d.UseInterfaces {
551+
defaultRoute := true
552+
vpcInterface := linodego.LinodeInterfaceCreateOptions{
553+
VPC: &linodego.VPCInterfaceCreateOptions{
554+
SubnetID: d.VPCSubnetID,
555+
},
556+
}
557+
vpcInterface.FirewallID = firewallIDPtr(d.VPCInterfaceFirewallID)
558+
559+
if d.VPCPrivateIP != "" {
560+
address := d.VPCPrivateIP
561+
primary := true
562+
vpcInterface.VPC.IPv4 = &linodego.VPCInterfaceIPv4CreateOptions{
563+
Addresses: &[]linodego.VPCInterfaceIPv4AddressCreateOptions{
564+
{
565+
Address: &address,
566+
Primary: &primary,
567+
},
568+
},
569+
}
570+
}
571+
572+
createOpts.InterfaceGeneration = linodego.GenerationLinode
573+
createOpts.PrivateIP = false
574+
publicInterface := linodego.LinodeInterfaceCreateOptions{
575+
DefaultRoute: &linodego.InterfaceDefaultRoute{IPv4: &defaultRoute},
576+
Public: &linodego.PublicInterfaceCreateOptions{},
577+
FirewallID: firewallIDPtr(d.PublicInterfaceFirewallID),
578+
}
579+
createOpts.LinodeInterfaces = []linodego.LinodeInterfaceCreateOptions{publicInterface, vpcInterface}
580+
}
581+
470582
linode, err := client.CreateInstance(context.TODO(), createOpts)
471583
if err != nil {
472584
return err
@@ -478,33 +590,59 @@ func (d *Driver) Create() error {
478590
// Don't persist alias region names
479591
d.Region = linode.Region
480592

481-
for _, address := range linode.IPv4 {
482-
if private := privateIP(*address); !private {
483-
d.IPAddress = address.String()
484-
} else if d.CreatePrivateIP {
485-
d.PrivateIPAddress = address.String()
593+
if d.UseInterfaces {
594+
ips, err := client.GetInstanceIPAddresses(context.TODO(), linode.ID)
595+
if err != nil {
596+
return err
486597
}
487-
}
488598

489-
if d.IPAddress == "" {
490-
return errors.New("Linode IP Address is not found")
491-
}
599+
if ips == nil || ips.IPv4 == nil {
600+
return errors.New("Linode IP information is not available")
601+
}
602+
603+
d.IPAddress = firstInstanceIP(ips.IPv4.Public)
604+
if d.IPAddress == "" {
605+
d.IPAddress = firstInstanceIP(ips.IPv4.Shared)
606+
}
607+
if d.IPAddress == "" {
608+
d.IPAddress = firstInstanceIP(ips.IPv4.Reserved)
609+
}
610+
611+
d.PrivateIPAddress = firstVPCIPv4(ips.IPv4.VPC)
492612

493-
if d.CreatePrivateIP && d.PrivateIPAddress == "" {
494-
return errors.New("Linode Private IP Address is not found")
613+
if d.IPAddress == "" {
614+
return errors.New("Linode public IP address was not found")
615+
}
616+
617+
if d.PrivateIPAddress == "" {
618+
return fmt.Errorf("Linode VPC private IP address not found for subnet %d", d.VPCSubnetID)
619+
}
620+
} else {
621+
for _, address := range linode.IPv4 {
622+
if private := privateIP(*address); !private {
623+
d.IPAddress = address.String()
624+
} else if d.CreatePrivateIP {
625+
d.PrivateIPAddress = address.String()
626+
}
627+
}
628+
629+
if d.IPAddress == "" {
630+
return errors.New("Linode IP Address is not found")
631+
}
632+
633+
if d.CreatePrivateIP && d.PrivateIPAddress == "" {
634+
return errors.New("Linode Private IP Address is not found")
635+
}
495636
}
496637

497-
log.Debugf("Created Linode Instance %s (%d), IP address %q, Private IP address %q",
638+
log.Debugf("Created Linode Instance %s (%d), IP address %q, Private IP address %q (interfaces enabled: %t)",
498639
d.InstanceLabel,
499640
d.InstanceID,
500641
d.IPAddress,
501642
d.PrivateIPAddress,
643+
d.UseInterfaces,
502644
)
503645

504-
if err != nil {
505-
return err
506-
}
507-
508646
if d.CreatePrivateIP {
509647
log.Debugf("Enabling Network Helper for Private IP configuration...")
510648

@@ -655,6 +793,32 @@ func ipInCIDR(ip net.IP, CIDR string) bool {
655793
return ipNet.Contains(ip)
656794
}
657795

796+
func firstInstanceIP(addresses []*linodego.InstanceIP) string {
797+
for _, address := range addresses {
798+
if address == nil {
799+
continue
800+
}
801+
if address.Address != "" {
802+
return address.Address
803+
}
804+
}
805+
806+
return ""
807+
}
808+
809+
func firstVPCIPv4(addresses []*linodego.VPCIP) string {
810+
for _, address := range addresses {
811+
if address == nil {
812+
continue
813+
}
814+
if address.Address != nil && *address.Address != "" {
815+
return *address.Address
816+
}
817+
}
818+
819+
return ""
820+
}
821+
658822
const noLabelDuplicates = "._-"
659823

660824
func normalizeInstanceLabel(label string) (string, error) {

0 commit comments

Comments
 (0)