diff --git a/README.md b/README.md index 2f7f9f53e232f9f0474549e97463579a853509d4..513dfa2b913950094bd7e8ee74e500c9162aa7b7 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ You can find sources and pre-compiled binaries [here](https://github.com/JonasPr ```bash # Download the binary (this example downloads the binary for linux amd64) -$ wget https://github.com/JonasProgrammer/docker-machine-driver-hetzner/releases/download/3.8.1/docker-machine-driver-hetzner_3.8.1_linux_amd64.tar.gz -$ tar -xvf docker-machine-driver-hetzner_3.8.1_linux_amd64.tar.gz +$ wget https://github.com/JonasProgrammer/docker-machine-driver-hetzner/releases/download/3.9.0/docker-machine-driver-hetzner_3.9.0_linux_amd64.tar.gz +$ tar -xvf docker-machine-driver-hetzner_3.9.0_linux_amd64.tar.gz # Make it executable and copy the binary in a directory accessible with your $PATH $ chmod +x docker-machine-driver-hetzner @@ -110,6 +110,7 @@ $ docker-machine create \ - `--hetzner-auto-spread`: Add to a `docker-machine` provided `spread` group (mutually exclusive with `--hetzner-placement-group`) - `--hetzner-ssh-user`: Change the default SSH-User - `--hetzner-ssh-port`: Change the default SSH-Port +- `--hetzner-primary-ipv4/6`: Sets an existing primary IP (v4 or v6 respectively) for the server, as documented in [Networking](#networking) #### Existing SSH keys @@ -152,8 +153,24 @@ was used during creation. | `--hetzner-auto-spread` | `HETZNER_AUTO_SPREAD` | false | | `--hetzner-ssh-user` | `HETZNER_SSH_USER` | root | | `--hetzner-ssh-port` | `HETZNER_SSH_PORT` | 22 | +| `--hetzner-primary-ipv4` | `HETZNER_PRIMARY_IPV4` | | +| `--hetzner-primary-ipv6` | `HETZNER_PRIMARY_IPV6` | | -**Networking hint:** When disabling all public IPs, `--hetzner-use-private-network` must be given. +#### Networking + +Given `--hetzner-primary-ipv4` or `--hetzner-primary-ipv6`, the driver +attempts to set up machine creation with an existing [primary IP](https://docs.hetzner.com/cloud/servers/primary-ips/overview/) +as follows: If the passed argument parses to a valid IP address, the primary IP is resolved via address. +Otherwise, it is resolved in the default Hetzner Cloud API way (i.e. via ID and name as a fallback). + +No address family validation is performed, so when specifying an IP address it is the user's responsibility to pass the +appropriate type. This also applies to any given preconditions regarding the state of the address being attached. + +If no existing primary IPs are specified and public address creation is not disabled for a given address family, a new +primary IP will be auto-generated by default. Primary IPs created in that fashion will exhibit whatever default behavior +Hetzner assigns them at the given time, so users should take care what retention flags etc. are being set. + +When disabling all public IPs, `--hetzner-use-private-network` must be given. `--hetzner-disable-public` will take care of that, and behaves as if `--hetzner-disable-public-4 --hetzner-disable-public-6 --hetzner-use-private-network` were given. diff --git a/driver.go b/driver.go index 68d78e818cbc7ab4600d2e8cbfbb6fd2b1aba762..9cc3db4f5127d506b2bfbae1568509fd6bd156cd 100644 --- a/driver.go +++ b/driver.go @@ -44,6 +44,10 @@ type Driver struct { UsePrivateNetwork bool DisablePublic4 bool DisablePublic6 bool + PrimaryIPv4 string + cachedPrimaryIPv4 *hcloud.PrimaryIP + PrimaryIPv6 string + cachedPrimaryIPv6 *hcloud.PrimaryIP Firewalls []string ServerLabels map[string]string keyLabels map[string]string @@ -72,6 +76,8 @@ const ( flagUsePrivateNetwork = "hetzner-use-private-network" flagDisablePublic4 = "hetzner-disable-public-4" flagDisablePublic6 = "hetzner-disable-public-6" + flagPrimary4 = "hetzner-primary-ipv4" + flagPrimary6 = "hetzner-primary-ipv6" flagDisablePublic = "hetzner-disable-public" flagFirewalls = "hetzner-firewalls" flagAdditionalKeys = "hetzner-additional-key" @@ -189,6 +195,18 @@ func (d *Driver) GetCreateFlags() []mcnflag.Flag { Name: flagDisablePublic, Usage: "Disable public ip (v4 & v6)", }, + mcnflag.StringFlag{ + EnvVar: "HETZNER_PRIMARY_IPV4", + Name: flagPrimary4, + Usage: "Existing primary IPv4 address", + Value: "", + }, + mcnflag.StringFlag{ + EnvVar: "HETZNER_PRIMARY_IPV6", + Name: flagPrimary6, + Usage: "Existing primary IPv6 address", + Value: "", + }, mcnflag.StringSliceFlag{ EnvVar: "HETZNER_FIREWALLS", Name: flagFirewalls, @@ -261,6 +279,8 @@ func (d *Driver) setConfigFromFlagsImpl(opts drivers.DriverOptions) error { d.UsePrivateNetwork = opts.Bool(flagUsePrivateNetwork) || disablePublic d.DisablePublic4 = opts.Bool(flagDisablePublic4) || disablePublic d.DisablePublic6 = opts.Bool(flagDisablePublic6) || disablePublic + d.PrimaryIPv4 = opts.String(flagPrimary4) + d.PrimaryIPv6 = opts.String(flagPrimary6) d.Firewalls = opts.StringSlice(flagFirewalls) d.AdditionalKeys = opts.StringSlice(flagAdditionalKeys) @@ -297,6 +317,14 @@ func (d *Driver) setConfigFromFlagsImpl(opts drivers.DriverOptions) error { flagUsePrivateNetwork, flagDisablePublic) } + if d.DisablePublic4 && d.PrimaryIPv4 != "" { + return d.flagFailure("--%v and --%v are mutually exclusive", flagPrimary4, flagDisablePublic4) + } + + if d.DisablePublic6 && d.PrimaryIPv6 != "" { + return d.flagFailure("--%v and --%v are mutually exclusive", flagPrimary6, flagDisablePublic6) + } + return nil } @@ -375,6 +403,14 @@ func (d *Driver) PreCreateCheck() error { return fmt.Errorf("could not create placement group: %w", err) } + if _, err := d.getPrimaryIPv4(); err != nil { + return fmt.Errorf("could not resolve primary IPv4: %w", err) + } + + if _, err := d.getPrimaryIPv6(); err != nil { + return fmt.Errorf("could not resolve primary IPv6: %w", err) + } + if d.UsePrivateNetwork && len(d.Networks) == 0 { return errors.Errorf("No private network attached.") } @@ -495,11 +531,9 @@ func (d *Driver) makeCreateServerOptions() (*hcloud.ServerCreateOpts, error) { PlacementGroup: pgrp, } - if d.DisablePublic4 || d.DisablePublic6 { - srvopts.PublicNet = &hcloud.ServerCreatePublicNet{ - EnableIPv4: !d.DisablePublic4, - EnableIPv6: !d.DisablePublic6, - } + err = d.setPublicNetIfRequired(srvopts) + if err != nil { + return nil, err } networks, err := d.createNetworks() @@ -537,6 +571,27 @@ func (d *Driver) makeCreateServerOptions() (*hcloud.ServerCreateOpts, error) { return &srvopts, nil } +func (d *Driver) setPublicNetIfRequired(srvopts hcloud.ServerCreateOpts) error { + pip4, err := d.getPrimaryIPv4() + if err != nil { + return err + } + pip6, err := d.getPrimaryIPv6() + if err != nil { + return err + } + + if d.DisablePublic4 || d.DisablePublic6 || pip4 != nil || pip6 != nil { + srvopts.PublicNet = &hcloud.ServerCreatePublicNet{ + EnableIPv4: !d.DisablePublic4, + EnableIPv6: !d.DisablePublic6, + IPv4: pip4, + IPv6: pip6, + } + } + return nil +} + func (d *Driver) createNetworks() ([]*hcloud.Network, error) { networks := []*hcloud.Network{} for _, networkIDorName := range d.Networks { @@ -1094,3 +1149,52 @@ func (d *Driver) removeEmptyServerPlacementGroup(srv *hcloud.Server) error { return nil } } + +func (d *Driver) getPrimaryIPv4() (*hcloud.PrimaryIP, error) { + raw := d.PrimaryIPv4 + if raw == "" { + return nil, nil + } else if d.cachedPrimaryIPv4 != nil { + return d.cachedPrimaryIPv4, nil + } + + ip, err := d.resolvePrimaryIP(raw) + d.cachedPrimaryIPv4 = ip + return ip, err +} + +func (d *Driver) getPrimaryIPv6() (*hcloud.PrimaryIP, error) { + raw := d.PrimaryIPv6 + if raw == "" { + return nil, nil + } else if d.cachedPrimaryIPv6 != nil { + return d.cachedPrimaryIPv6, nil + } + + ip, err := d.resolvePrimaryIP(raw) + d.cachedPrimaryIPv6 = ip + return ip, err +} + +func (d *Driver) resolvePrimaryIP(raw string) (*hcloud.PrimaryIP, error) { + client := d.getClient().PrimaryIP + + var getter func(context.Context, string) (*hcloud.PrimaryIP, *hcloud.Response, error) + if net.ParseIP(raw) != nil { + getter = client.GetByIP + } else { + getter = client.Get + } + + ip, _, err := getter(context.Background(), raw) + + if err != nil { + return nil, fmt.Errorf("could not get primary IP: %w", err) + } + + if ip != nil { + return ip, nil + } + + return nil, fmt.Errorf("primary IP not found: %v", raw) +}