Skip to content

Commit 014aa8c

Browse files
committed
feat: add VPC interface networking and firewall controls
1 parent fc8bc66 commit 014aa8c

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
@@ -63,9 +63,19 @@ docker-machine create -d linode --linode-token=<linode-token> linode
6363
| `linode-stackscript` | `LINODE_STACKSCRIPT` | None | Specifies the Linode StackScript to use to create the instance, either by numeric ID, or using the form *username*/*label*.
6464
| `linode-stackscript-data` | `LINODE_STACKSCRIPT_DATA` | None | A JSON string specifying data that is passed (via UDF) to the selected StackScript.
6565
| `linode-create-private-ip` | `LINODE_CREATE_PRIVATE_IP` | None | A flag specifying to create private IP for the Linode instance.
66+
| `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`).
67+
| `linode-vpc-subnet-id` | `LINODE_VPC_SUBNET_ID` | None | VPC subnet ID to attach when using interface networking.
68+
| `linode-vpc-private-ip` | `LINODE_VPC_PRIVATE_IP` | None | Optional IPv4 address to request on the VPC interface (requires `linode-use-interfaces`).
69+
| `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.
70+
| `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.
6671
| `linode-tags` | `LINODE_TAGS` | None | A comma separated list of tags to apply to the Linode resource
6772
| `linode-ua-prefix` | `LINODE_UA_PREFIX` | None | Prefix the User-Agent in Linode API calls with some 'product/version'
6873

74+
## Networking Modes
75+
76+
- **Legacy (default):** uses public networking and optionally `--linode-create-private-ip` to attach a private address.
77+
- **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`.
78+
6979
## Notes
7080

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

139+
### Interface/VPC Networking Example
140+
141+
Use Linode's newer interface generation to attach a VPC subnet while keeping the public interface for provisioning traffic.
142+
143+
```bash
144+
docker-machine create \
145+
-d linode \
146+
--linode-token=$LINODE_TOKEN \
147+
--linode-use-interfaces \
148+
--linode-vpc-subnet-id=67890 \
149+
--linode-vpc-private-ip=10.0.0.25 \
150+
linode-vpc
151+
```
152+
153+
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.
154+
155+
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.
156+
129157
### Provisioning Docker Swarm
130158

131159
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
@@ -122,6 +127,19 @@ func createRandomRootPassword() (string, error) {
122127
return rootPass, nil
123128
}
124129

130+
// FirewallID is a **int in linodego so callers can distinguish between
131+
// omitting the field entirely and explicitly sending a value.
132+
func firewallIDPtr(id int) **int {
133+
if id == 0 {
134+
return nil
135+
}
136+
137+
value := id
138+
valuePtr := &value
139+
140+
return &valuePtr
141+
}
142+
125143
// DriverName returns the name of the driver
126144
func (d *Driver) DriverName() string {
127145
return "linode"
@@ -226,6 +244,31 @@ func (d *Driver) GetCreateFlags() []mcnflag.Flag {
226244
Name: "linode-create-private-ip",
227245
Usage: "Create private IP for the instance",
228246
},
247+
mcnflag.BoolFlag{
248+
EnvVar: "LINODE_USE_INTERFACES",
249+
Name: "linode-use-interfaces",
250+
Usage: "Enable Linode interface/VPC networking (opt-in, keeps legacy defaults otherwise)",
251+
},
252+
mcnflag.IntFlag{
253+
EnvVar: "LINODE_VPC_SUBNET_ID",
254+
Name: "linode-vpc-subnet-id",
255+
Usage: "VPC subnet ID to attach when using interface/VPC networking",
256+
},
257+
mcnflag.StringFlag{
258+
EnvVar: "LINODE_VPC_PRIVATE_IP",
259+
Name: "linode-vpc-private-ip",
260+
Usage: "Optional IPv4 address to request on the VPC interface (requires --linode-use-interfaces)",
261+
},
262+
mcnflag.IntFlag{
263+
EnvVar: "LINODE_PUBLIC_INTERFACE_FIREWALL_ID",
264+
Name: "linode-public-interface-firewall-id",
265+
Usage: "Firewall ID to attach to the public interface when using interface/VPC networking",
266+
},
267+
mcnflag.IntFlag{
268+
EnvVar: "LINODE_VPC_INTERFACE_FIREWALL_ID",
269+
Name: "linode-vpc-interface-firewall-id",
270+
Usage: "Firewall ID to attach to the VPC interface when using interface/VPC networking",
271+
},
229272
mcnflag.StringFlag{
230273
EnvVar: "LINODE_UA_PREFIX",
231274
Name: "linode-ua-prefix",
@@ -276,6 +319,11 @@ func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error {
276319
d.SwapSize = flags.Int("linode-swap-size")
277320
d.DockerPort = flags.Int("linode-docker-port")
278321
d.CreatePrivateIP = flags.Bool("linode-create-private-ip")
322+
d.UseInterfaces = flags.Bool("linode-use-interfaces")
323+
d.VPCSubnetID = flags.Int("linode-vpc-subnet-id")
324+
d.VPCPrivateIP = strings.TrimSpace(flags.String("linode-vpc-private-ip"))
325+
d.VPCInterfaceFirewallID = flags.Int("linode-vpc-interface-firewall-id")
326+
d.PublicInterfaceFirewallID = flags.Int("linode-public-interface-firewall-id")
279327
d.UserAgentPrefix = flags.String("linode-ua-prefix")
280328
d.Tags = flags.String("linode-tags")
281329

@@ -320,6 +368,34 @@ func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error {
320368

321369
d.InstanceLabel = newLabel
322370

371+
if d.PublicInterfaceFirewallID < 0 {
372+
return fmt.Errorf("invalid value for --linode-public-interface-firewall-id: must be zero or positive")
373+
}
374+
if d.VPCInterfaceFirewallID < 0 {
375+
return fmt.Errorf("invalid value for --linode-vpc-interface-firewall-id: must be zero or positive")
376+
}
377+
378+
if d.UseInterfaces && d.CreatePrivateIP {
379+
return fmt.Errorf("cannot combine --linode-use-interfaces with --linode-create-private-ip; choose one networking mode")
380+
}
381+
382+
if d.UseInterfaces {
383+
if d.VPCSubnetID == 0 {
384+
return fmt.Errorf("linode interface networking requires --linode-vpc-subnet-id")
385+
}
386+
387+
if d.VPCPrivateIP != "" {
388+
parsed := net.ParseIP(d.VPCPrivateIP)
389+
if parsed == nil || parsed.To4() == nil {
390+
return fmt.Errorf("linode VPC private IP must be a valid IPv4 address")
391+
}
392+
}
393+
} else {
394+
if d.VPCSubnetID != 0 || d.VPCPrivateIP != "" || d.PublicInterfaceFirewallID != 0 || d.VPCInterfaceFirewallID != 0 {
395+
return fmt.Errorf("VPC/interface options require --linode-use-interfaces to be set")
396+
}
397+
}
398+
323399
return nil
324400
}
325401

@@ -391,6 +467,10 @@ func (d *Driver) Create() error {
391467
log.Infof("Using SSH port %d", d.SSHPort)
392468
}
393469

470+
if d.UseInterfaces {
471+
log.Infof("Using interface/VPC networking (subnet %d)", d.VPCSubnetID)
472+
}
473+
394474
publicKey, err := d.createSSHKey()
395475
if err != nil {
396476
return err
@@ -426,6 +506,38 @@ func (d *Driver) Create() error {
426506
log.Infof("Using StackScript %d: %s/%s", d.StackScriptID, d.StackScriptUser, d.StackScriptLabel)
427507
}
428508

509+
if d.UseInterfaces {
510+
defaultRoute := true
511+
vpcInterface := linodego.LinodeInterfaceCreateOptions{
512+
VPC: &linodego.VPCInterfaceCreateOptions{
513+
SubnetID: d.VPCSubnetID,
514+
},
515+
}
516+
vpcInterface.FirewallID = firewallIDPtr(d.VPCInterfaceFirewallID)
517+
518+
if d.VPCPrivateIP != "" {
519+
address := d.VPCPrivateIP
520+
primary := true
521+
vpcInterface.VPC.IPv4 = &linodego.VPCInterfaceIPv4CreateOptions{
522+
Addresses: &[]linodego.VPCInterfaceIPv4AddressCreateOptions{
523+
{
524+
Address: &address,
525+
Primary: &primary,
526+
},
527+
},
528+
}
529+
}
530+
531+
createOpts.InterfaceGeneration = linodego.GenerationLinode
532+
createOpts.PrivateIP = false
533+
publicInterface := linodego.LinodeInterfaceCreateOptions{
534+
DefaultRoute: &linodego.InterfaceDefaultRoute{IPv4: &defaultRoute},
535+
Public: &linodego.PublicInterfaceCreateOptions{},
536+
FirewallID: firewallIDPtr(d.PublicInterfaceFirewallID),
537+
}
538+
createOpts.LinodeInterfaces = []linodego.LinodeInterfaceCreateOptions{publicInterface, vpcInterface}
539+
}
540+
429541
linode, err := client.CreateInstance(context.TODO(), createOpts)
430542
if err != nil {
431543
return err
@@ -437,33 +549,59 @@ func (d *Driver) Create() error {
437549
// Don't persist alias region names
438550
d.Region = linode.Region
439551

440-
for _, address := range linode.IPv4 {
441-
if private := privateIP(*address); !private {
442-
d.IPAddress = address.String()
443-
} else if d.CreatePrivateIP {
444-
d.PrivateIPAddress = address.String()
552+
if d.UseInterfaces {
553+
ips, err := client.GetInstanceIPAddresses(context.TODO(), linode.ID)
554+
if err != nil {
555+
return err
445556
}
446-
}
447557

448-
if d.IPAddress == "" {
449-
return errors.New("Linode IP Address is not found")
450-
}
558+
if ips == nil || ips.IPv4 == nil {
559+
return errors.New("Linode IP information is not available")
560+
}
561+
562+
d.IPAddress = firstInstanceIP(ips.IPv4.Public)
563+
if d.IPAddress == "" {
564+
d.IPAddress = firstInstanceIP(ips.IPv4.Shared)
565+
}
566+
if d.IPAddress == "" {
567+
d.IPAddress = firstInstanceIP(ips.IPv4.Reserved)
568+
}
569+
570+
d.PrivateIPAddress = firstVPCIPv4(ips.IPv4.VPC)
451571

452-
if d.CreatePrivateIP && d.PrivateIPAddress == "" {
453-
return errors.New("Linode Private IP Address is not found")
572+
if d.IPAddress == "" {
573+
return errors.New("Linode public IP address was not found")
574+
}
575+
576+
if d.PrivateIPAddress == "" {
577+
return fmt.Errorf("Linode VPC private IP address not found for subnet %d", d.VPCSubnetID)
578+
}
579+
} else {
580+
for _, address := range linode.IPv4 {
581+
if private := privateIP(*address); !private {
582+
d.IPAddress = address.String()
583+
} else if d.CreatePrivateIP {
584+
d.PrivateIPAddress = address.String()
585+
}
586+
}
587+
588+
if d.IPAddress == "" {
589+
return errors.New("Linode IP Address is not found")
590+
}
591+
592+
if d.CreatePrivateIP && d.PrivateIPAddress == "" {
593+
return errors.New("Linode Private IP Address is not found")
594+
}
454595
}
455596

456-
log.Debugf("Created Linode Instance %s (%d), IP address %q, Private IP address %q",
597+
log.Debugf("Created Linode Instance %s (%d), IP address %q, Private IP address %q (interfaces enabled: %t)",
457598
d.InstanceLabel,
458599
d.InstanceID,
459600
d.IPAddress,
460601
d.PrivateIPAddress,
602+
d.UseInterfaces,
461603
)
462604

463-
if err != nil {
464-
return err
465-
}
466-
467605
if d.CreatePrivateIP {
468606
log.Debugf("Enabling Network Helper for Private IP configuration...")
469607

@@ -614,6 +752,32 @@ func ipInCIDR(ip net.IP, CIDR string) bool {
614752
return ipNet.Contains(ip)
615753
}
616754

755+
func firstInstanceIP(addresses []*linodego.InstanceIP) string {
756+
for _, address := range addresses {
757+
if address == nil {
758+
continue
759+
}
760+
if address.Address != "" {
761+
return address.Address
762+
}
763+
}
764+
765+
return ""
766+
}
767+
768+
func firstVPCIPv4(addresses []*linodego.VPCIP) string {
769+
for _, address := range addresses {
770+
if address == nil {
771+
continue
772+
}
773+
if address.Address != nil && *address.Address != "" {
774+
return *address.Address
775+
}
776+
}
777+
778+
return ""
779+
}
780+
617781
const noLabelDuplicates = "._-"
618782

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

0 commit comments

Comments
 (0)