diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 11d418806ff2cc5d23b7a945d498d8f799a31b42..d35450d52080534935ef11f4cc22d050a1bfe61c 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -30,6 +30,10 @@ jobs: if: ${{ startsWith(github.ref, 'refs/tags/') && contains(fromJSON('["mxschmitt", "JonasProgrammer"]'), github.actor) }} steps: - uses: actions/checkout@v2 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.16 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 with: diff --git a/.github/workflows/manual-build.yml b/.github/workflows/manual-build.yml index 49d6db27118879f112307638e1e3e14f1ac5c4c4..3609d2cf7cff11d6032c7e283e110b88f4e0dd78 100644 --- a/.github/workflows/manual-build.yml +++ b/.github/workflows/manual-build.yml @@ -6,6 +6,10 @@ jobs: runs-on: ubuntu-20.04 steps: - uses: actions/checkout@v2 + - name: Set up Go + uses: actions/setup-go@v2 + with: + go-version: 1.16 - name: Run GoReleaser uses: goreleaser/goreleaser-action@v2 with: diff --git a/.goreleaser.yml b/.goreleaser.yml index 18c01d8d80627e91b63842eb011845edc3b27acd..d4f70b4e0a5ec6feebaae8868f66b8c914689aff 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -5,5 +5,6 @@ builds: - windows goarch: - amd64 + - arm64 env: - CGO_ENABLED=0 diff --git a/README.md b/README.md index 0fb22eee999c2ba42c78f19d7472038024a45280..35f9bd859926f7a6ab6f1224419dddd71549cea8 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.3.0/docker-machine-driver-hetzner_3.3.0_linux_amd64.tar.gz -$ tar -xvf docker-machine-driver-hetzner_3.3.0_linux_amd64.tar.gz +$ wget https://github.com/JonasProgrammer/docker-machine-driver-hetzner/releases/download/3.5.0/docker-machine-driver-hetzner_3.5.0_linux_amd64.tar.gz +$ tar -xvf docker-machine-driver-hetzner_3.5.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 @@ -105,6 +105,9 @@ $ docker-machine create \ - `--hetzner-use-private-network`: Use private network - `--hetzner-firewalls`: Firewall IDs or names which should be applied on the server - `--hetzner-server-label`: `key=value` pairs of additional metadata to assign to the server. +- `--hetzner-key-label`: `key=value` pairs of additional metadata to assign to SSH key (only applies if newly creadted). +- `--hetzner-placement-group`: Add to a placement group by name or ID; a spread-group will be created on demand if it does not exist +- `--hetzner-auto-spread`: Add to a `docker-machine` provided `spread` group (mutually exclusive with `--hetzner-placement-group`) #### Existing SSH keys @@ -125,21 +128,23 @@ was used during creation. | CLI option | Environment variable | Default | | ----------------------------------- | --------------------------------- | -------------------------- | -| **`--hetzner-api-token`** | `HETZNER_API_TOKEN` | - | +| **`--hetzner-api-token`** | `HETZNER_API_TOKEN` | | | `--hetzner-image` | `HETZNER_IMAGE` | `ubuntu-18.04` | -| `--hetzner-image-id` | `HETZNER_IMAGE_ID` | - | +| `--hetzner-image-id` | `HETZNER_IMAGE_ID` | | | `--hetzner-server-type` | `HETZNER_TYPE` | `cx11` | -| `--hetzner-server-location` | `HETZNER_LOCATION` | - *(let Hetzner choose)* | -| `--hetzner-existing-key-path` | `HETZNER_EXISTING_KEY_PATH` | - *(generate new keypair)* | +| `--hetzner-server-location` | `HETZNER_LOCATION` | *(let Hetzner choose)* | +| `--hetzner-existing-key-path` | `HETZNER_EXISTING_KEY_PATH` | *(generate new keypair)* | | `--hetzner-existing-key-id` | `HETZNER_EXISTING_KEY_ID` | 0 *(upload new key)* | -| `--hetzner-additional-key` | `HETZNER_ADDITIONAL_KEYS` | - | -| `--hetzner-user-data` | `HETZNER_USER_DATA` | - | -| `--hetzner-networks` | `HETZNER_NETWORKS` | - | -| `--hetzner-firewalls` | `HETZNER_FIREWALLS` | - | -| `--hetzner-volumes` | `HETZNER_VOLUMES` | - | +| `--hetzner-additional-key` | `HETZNER_ADDITIONAL_KEYS` | | +| `--hetzner-user-data` | `HETZNER_USER_DATA` | | +| `--hetzner-networks` | `HETZNER_NETWORKS` | | +| `--hetzner-firewalls` | `HETZNER_FIREWALLS` | | +| `--hetzner-volumes` | `HETZNER_VOLUMES` | | | `--hetzner-use-private-network` | `HETZNER_USE_PRIVATE_NETWORK` | false | -| `--hetzner-server-label` | `HETZNER_SERVER_LABELS` | `[]` | - +| `--hetzner-server-label` | (inoperative) | `[]` | +| `--hetzner-key-label` | (inoperative) | `[]` | +| `--hetzner-placement-group` | `HETZNER_PLACEMENT_GROUP` | | +| `--hetzner-auto-spread` | `HETZNER_AUTO_SPREAD` | false | ## Building from source diff --git a/driver.go b/driver.go index b9009bb8fdbe06ebab4a9fefd606bc14cff78cd0..b274958721bece247e1d6294790bdc5a70c51ff5 100644 --- a/driver.go +++ b/driver.go @@ -20,6 +20,7 @@ import ( "golang.org/x/crypto/ssh" ) +// Driver contains hetzner-specific data to implement [drivers.Driver] type Driver struct { *drivers.BaseDriver @@ -35,7 +36,7 @@ type Driver struct { cachedKey *hcloud.SSHKey IsExistingKey bool originalKey string - danglingKeys []*hcloud.SSHKey + dangling []func() ServerID int cachedServer *hcloud.Server userData string @@ -44,6 +45,9 @@ type Driver struct { UsePrivateNetwork bool Firewalls []string ServerLabels map[string]string + keyLabels map[string]string + placementGroup string + cachedPGrp *hcloud.PlacementGroup AdditionalKeys []string AdditionalKeyIDs []int @@ -68,14 +72,23 @@ const ( flagFirewalls = "hetzner-firewalls" flagAdditionalKeys = "hetzner-additional-key" flagServerLabel = "hetzner-server-label" + flagKeyLabel = "hetzner-key-label" + flagPlacementGroup = "hetzner-placement-group" + flagAutoSpread = "hetzner-auto-spread" + + labelNamespace = "docker-machine" + labelAutoSpreadPg = "auto-spread" + labelAutoCreated = "auto-created" + + autoSpreadPgName = "__auto_spread" ) +// NewDriver initializes a new driver instance; see [drivers.Driver.NewDriver] func NewDriver() *Driver { return &Driver{ Image: defaultImage, Type: defaultType, IsExistingKey: false, - danglingKeys: []*hcloud.SSHKey{}, BaseDriver: &drivers.BaseDriver{ SSHUser: drivers.DefaultSSHUser, SSHPort: drivers.DefaultSSHPort, @@ -83,10 +96,12 @@ func NewDriver() *Driver { } } +// DriverName returns the hard-coded string "hetzner"; see [drivers.Driver.DriverName] func (d *Driver) DriverName() string { return "hetzner" } +// GetCreateFlags retrieves additional driver-specific arguments; see [drivers.Driver.GetCreateFlags] func (d *Driver) GetCreateFlags() []mcnflag.Flag { return []mcnflag.Flag{ mcnflag.StringFlag{ @@ -171,9 +186,28 @@ func (d *Driver) GetCreateFlags() []mcnflag.Flag { Usage: "Key value pairs of additional labels to assign to the server", Value: []string{}, }, + mcnflag.StringSliceFlag{ + EnvVar: "HETZNER_KEY_LABELS", + Name: flagKeyLabel, + Usage: "Key value pairs of additional labels to assign to the SSH key", + Value: []string{}, + }, + mcnflag.StringFlag{ + EnvVar: "HETZNER_PLACEMENT_GROUP", + Name: flagPlacementGroup, + Usage: "Placement group ID or name to add the server to; will be created if it does not exist", + Value: "", + }, + mcnflag.BoolFlag{ + EnvVar: "HETZNER_AUTO_SPREAD", + Name: flagAutoSpread, + Usage: "Auto-spread on a docker-machine-specific default placement group", + }, } } +// SetConfigFromFlags handles additional driver arguments as retrieved by [Driver.GetCreateFlags]; +// see [drivers.Driver.SetConfigFromFlags] func (d *Driver) SetConfigFromFlags(opts drivers.DriverOptions) error { d.AccessToken = opts.String(flagAPIToken) d.Image = opts.String(flagImage) @@ -190,6 +224,14 @@ func (d *Driver) SetConfigFromFlags(opts drivers.DriverOptions) error { d.Firewalls = opts.StringSlice(flagFirewalls) d.AdditionalKeys = opts.StringSlice(flagAdditionalKeys) + d.placementGroup = opts.String(flagPlacementGroup) + if opts.Bool(flagAutoSpread) { + if d.placementGroup != "" { + return errors.Errorf(flagAutoSpread + " and " + flagPlacementGroup + " are mutually exclusive") + } + d.placementGroup = autoSpreadPgName + } + err := d.setLabelsFromFlags(opts) if err != nil { return err @@ -217,9 +259,18 @@ func (d *Driver) setLabelsFromFlags(opts drivers.DriverOptions) error { } d.ServerLabels[split[0]] = split[1] } + d.keyLabels = make(map[string]string) + for _, label := range opts.StringSlice(flagKeyLabel) { + split := strings.SplitN(label, "=", 2) + if len(split) != 2 { + return errors.Errorf("key label %v is not in key=value format", label) + } + d.keyLabels[split[0]] = split[1] + } return nil } +// PreCreateCheck validates the Driver data is in a valid state for creation; see [drivers.Driver.PreCreateCheck] func (d *Driver) PreCreateCheck() error { if d.IsExistingKey { if d.originalKey == "" { @@ -260,6 +311,10 @@ func (d *Driver) PreCreateCheck() error { return errors.Wrap(err, "could not get location") } + if _, err := d.getPlacementGroup(); err != nil { + return fmt.Errorf("could not create placement group: %w", err) + } + if d.UsePrivateNetwork && len(d.Networks) == 0 { return errors.Errorf("No private network attached.") } @@ -267,188 +322,259 @@ func (d *Driver) PreCreateCheck() error { return nil } +// Create actually creates the hetzner-cloud server; see [drivers.Driver.Create] func (d *Driver) Create() error { - if d.originalKey != "" { - log.Debugf("Copying SSH key...") - if err := d.copySSHKeyPair(d.originalKey); err != nil { - return errors.Wrap(err, "could not copy ssh key pair") - } - } else { - log.Debugf("Generating SSH key...") - if err := mcnssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil { - return errors.Wrap(err, "could not generate ssh key") - } + err := d.prepareLocalKey() + if err != nil { + return err } - defer d.destroyDanglingKeys() - if d.KeyID == 0 { - log.Infof("Creating SSH key...") + defer d.destroyDangling() + err = d.createRemoteKeys() + if err != nil { + return err + } - buf, err := ioutil.ReadFile(d.GetSSHKeyPath() + ".pub") - if err != nil { - return errors.Wrap(err, "could not read ssh public key") - } + log.Infof("Creating Hetzner server...") - key, err := d.getRemoteKeyWithSameFingerprint(buf) - if err != nil { - return errors.Wrap(err, "error retrieving potentially existing key") - } - if key == nil { - log.Infof("SSH key not found in Hetzner. Uploading...") + srvopts, err := d.makeCreateServerOptions() + if err != nil { + return err + } - key, err = d.makeKey(d.GetMachineName(), string(buf)) + srv, _, err := d.getClient().Server.Create(context.Background(), *srvopts) + if err != nil { + return errors.Wrap(err, "could not create server") + } + + log.Infof(" -> Creating server %s[%d] in %s[%d]", srv.Server.Name, srv.Server.ID, srv.Action.Command, srv.Action.ID) + if err = d.waitForAction(srv.Action); err != nil { + return errors.Wrap(err, "could not wait for action") + } + + d.ServerID = srv.Server.ID + log.Infof(" -> Server %s[%d]: Waiting to come up...", srv.Server.Name, srv.Server.ID) + + err = d.waitForRunningServer() + if err != nil { + return err + } + + err = d.configureNetworkAccess(srv) + if err != nil { + return err + } + + log.Infof(" -> Server %s[%d] ready. Ip %s", srv.Server.Name, srv.Server.ID, d.IPAddress) + // Successful creation, so no keys dangle anymore + d.dangling = nil + + return nil +} + +func (d *Driver) configureNetworkAccess(srv hcloud.ServerCreateResult) error { + if d.UsePrivateNetwork { + for { + // we need to wait until network is attached + log.Infof("Wait until private network attached ...") + server, _, err := d.getClient().Server.GetByID(context.Background(), srv.Server.ID) if err != nil { - return err + return errors.Wrapf(err, "could not get newly created server [%d]", srv.Server.ID) } - } else { - d.IsExistingKey = true - log.Debugf("SSH key found in Hetzner. ID: %d", key.ID) + if server.PrivateNet != nil { + d.IPAddress = server.PrivateNet[0].IP.String() + break + } + time.Sleep(1 * time.Second) } - - d.KeyID = key.ID + } else { + log.Infof("Using public network ...") + d.IPAddress = srv.Server.PublicNet.IPv4.IP.String() } - for i, pubkey := range d.AdditionalKeys { - key, err := d.getRemoteKeyWithSameFingerprint([]byte(pubkey)) + return nil +} + +func (d *Driver) waitForRunningServer() error { + for { + srvstate, err := d.GetState() if err != nil { - return errors.Wrapf(err, "error checking for existing key for %v", pubkey) + return errors.Wrap(err, "could not get state") } - if key == nil { - log.Infof("Creating new key for %v...", pubkey) - key, err = d.makeKey(fmt.Sprintf("%v-additional-%d", d.GetMachineName(), i), pubkey) - if err != nil { - return errors.Wrapf(err, "error creating new key for %v", pubkey) - } - - log.Infof(" -> Created %v", key.ID) - d.AdditionalKeyIDs = append(d.AdditionalKeyIDs, key.ID) - } else { - log.Infof("Using existing key (%v) %v", key.ID, key.Name) + if srvstate == state.Running { + break } - d.cachedAdditionalKeys = append(d.cachedAdditionalKeys, key) + time.Sleep(1 * time.Second) } + return nil +} - log.Infof("Creating Hetzner server...") +func (d *Driver) makeCreateServerOptions() (*hcloud.ServerCreateOpts, error) { + pgrp, err := d.getPlacementGroup() + if err != nil { + return nil, err + } srvopts := hcloud.ServerCreateOpts{ - Name: d.GetMachineName(), - UserData: d.userData, - Labels: d.ServerLabels, + Name: d.GetMachineName(), + UserData: d.userData, + Labels: d.ServerLabels, + PlacementGroup: pgrp, } + networks, err := d.createNetworks() + if err != nil { + return nil, err + } + srvopts.Networks = networks + + firewalls, err := d.createFirewalls() + if err != nil { + return nil, err + } + srvopts.Firewalls = firewalls + + volumes, err := d.createVolumes() + if err != nil { + return nil, err + } + srvopts.Volumes = volumes + + if srvopts.Location, err = d.getLocation(); err != nil { + return nil, errors.Wrap(err, "could not get location") + } + if srvopts.ServerType, err = d.getType(); err != nil { + return nil, errors.Wrap(err, "could not get type") + } + if srvopts.Image, err = d.getImage(); err != nil { + return nil, errors.Wrap(err, "could not get image") + } + key, err := d.getKey() + if err != nil { + return nil, errors.Wrap(err, "could not get ssh key") + } + srvopts.SSHKeys = append(d.cachedAdditionalKeys, key) + return &srvopts, nil +} + +func (d *Driver) createNetworks() ([]*hcloud.Network, error) { networks := []*hcloud.Network{} for _, networkIDorName := range d.Networks { network, _, err := d.getClient().Network.Get(context.Background(), networkIDorName) if err != nil { - return errors.Wrap(err, "could not get network by ID or name") + return nil, errors.Wrap(err, "could not get network by ID or name") } if network == nil { - return errors.Errorf("network '%s' not found", networkIDorName) + return nil, errors.Errorf("network '%s' not found", networkIDorName) } networks = append(networks, network) } - srvopts.Networks = networks + return networks, nil +} +func (d *Driver) createFirewalls() ([]*hcloud.ServerCreateFirewall, error) { firewalls := []*hcloud.ServerCreateFirewall{} for _, firewallIDorName := range d.Firewalls { firewall, _, err := d.getClient().Firewall.Get(context.Background(), firewallIDorName) if err != nil { - return errors.Wrap(err, "could not get firewall by ID or name") + return nil, errors.Wrap(err, "could not get firewall by ID or name") } if firewall == nil { - return errors.Errorf("firewall '%s' not found", firewallIDorName) + return nil, errors.Errorf("firewall '%s' not found", firewallIDorName) } firewalls = append(firewalls, &hcloud.ServerCreateFirewall{Firewall: *firewall}) } - srvopts.Firewalls = firewalls + return firewalls, nil +} +func (d *Driver) createVolumes() ([]*hcloud.Volume, error) { volumes := []*hcloud.Volume{} for _, volumeIDorName := range d.Volumes { volume, _, err := d.getClient().Volume.Get(context.Background(), volumeIDorName) if err != nil { - return errors.Wrap(err, "could not get volume by ID or name") + return nil, errors.Wrap(err, "could not get volume by ID or name") } if volume == nil { - return errors.Errorf("volume '%s' not found", volumeIDorName) + return nil, errors.Errorf("volume '%s' not found", volumeIDorName) } volumes = append(volumes, volume) } - srvopts.Volumes = volumes - - var err error - if srvopts.Location, err = d.getLocation(); err != nil { - return errors.Wrap(err, "could not get location") - } - if srvopts.ServerType, err = d.getType(); err != nil { - return errors.Wrap(err, "could not get type") - } - if srvopts.Image, err = d.getImage(); err != nil { - return errors.Wrap(err, "could not get image") - } - key, err := d.getKey() - if err != nil { - return errors.Wrap(err, "could not get ssh key") - } - srvopts.SSHKeys = append(d.cachedAdditionalKeys, key) - - srv, _, err := d.getClient().Server.Create(context.Background(), srvopts) - if err != nil { - return errors.Wrap(err, "could not create server") - } + return volumes, nil +} - log.Infof(" -> Creating server %s[%d] in %s[%d]", srv.Server.Name, srv.Server.ID, srv.Action.Command, srv.Action.ID) - if err = d.waitForAction(srv.Action); err != nil { - return errors.Wrap(err, "could not wait for action") - } +func (d *Driver) createRemoteKeys() error { + if d.KeyID == 0 { + log.Infof("Creating SSH key...") - d.ServerID = srv.Server.ID - log.Infof(" -> Server %s[%d]: Waiting to come up...", srv.Server.Name, srv.Server.ID) + buf, err := ioutil.ReadFile(d.GetSSHKeyPath() + ".pub") + if err != nil { + return errors.Wrap(err, "could not read ssh public key") + } - for { - srvstate, err := d.GetState() + key, err := d.getRemoteKeyWithSameFingerprint(buf) if err != nil { - return errors.Wrap(err, "could not get state") + return errors.Wrap(err, "error retrieving potentially existing key") } + if key == nil { + log.Infof("SSH key not found in Hetzner. Uploading...") - if srvstate == state.Running { - break + key, err = d.makeKey(d.GetMachineName(), string(buf), d.keyLabels) + if err != nil { + return err + } + } else { + d.IsExistingKey = true + log.Debugf("SSH key found in Hetzner. ID: %d", key.ID) } - time.Sleep(1 * time.Second) + d.KeyID = key.ID } + for i, pubkey := range d.AdditionalKeys { + key, err := d.getRemoteKeyWithSameFingerprint([]byte(pubkey)) + if err != nil { + return errors.Wrapf(err, "error checking for existing key for %v", pubkey) + } + if key == nil { + log.Infof("Creating new key for %v...", pubkey) + key, err = d.makeKey(fmt.Sprintf("%v-additional-%d", d.GetMachineName(), i), pubkey, d.keyLabels) - if d.UsePrivateNetwork { - for { - // we need to wait until network is attached - log.Infof("Wait until private network attached ...") - server, _, err := d.getClient().Server.GetByID(context.Background(), srv.Server.ID) if err != nil { - return errors.Wrapf(err, "could not get newly created server [%d]", srv.Server.ID) - } - if server.PrivateNet != nil { - d.IPAddress = server.PrivateNet[0].IP.String() - break + return errors.Wrapf(err, "error creating new key for %v", pubkey) } - time.Sleep(1 * time.Second) + + log.Infof(" -> Created %v", key.ID) + d.AdditionalKeyIDs = append(d.AdditionalKeyIDs, key.ID) + } else { + log.Infof("Using existing key (%v) %v", key.ID, key.Name) } - } else { - log.Infof("Using public network ...") - d.IPAddress = srv.Server.PublicNet.IPv4.IP.String() - } - log.Infof(" -> Server %s[%d] ready. Ip %s", srv.Server.Name, srv.Server.ID, d.IPAddress) - // Successful creation, so no keys dangle anymore - d.danglingKeys = nil + d.cachedAdditionalKeys = append(d.cachedAdditionalKeys, key) + } + return nil +} +func (d *Driver) prepareLocalKey() error { + if d.originalKey != "" { + log.Debugf("Copying SSH key...") + if err := d.copySSHKeyPair(d.originalKey); err != nil { + return errors.Wrap(err, "could not copy ssh key pair") + } + } else { + log.Debugf("Generating SSH key...") + if err := mcnssh.GenerateSSHKey(d.GetSSHKeyPath()); err != nil { + return errors.Wrap(err, "could not generate ssh key") + } + } return nil } // Creates a new key for the machine and appends it to the dangling key list -func (d *Driver) makeKey(name string, pubkey string) (*hcloud.SSHKey, error) { +func (d *Driver) makeKey(name string, pubkey string, labels map[string]string) (*hcloud.SSHKey, error) { keyopts := hcloud.SSHKeyCreateOpts{ Name: name, PublicKey: pubkey, + Labels: labels, } key, _, err := d.getClient().SSHKey.Create(context.Background(), keyopts) @@ -458,23 +584,28 @@ func (d *Driver) makeKey(name string, pubkey string) (*hcloud.SSHKey, error) { return nil, errors.Errorf("key upload did not return an error, but key was nil") } - d.danglingKeys = append(d.danglingKeys, key) + d.dangling = append(d.dangling, func() { + _, err := d.getClient().SSHKey.Delete(context.Background(), key) + if err != nil { + log.Error(fmt.Errorf("could not delete ssh key: %w", err)) + } + }) + return key, nil } -func (d *Driver) destroyDanglingKeys() { - for _, key := range d.danglingKeys { - if _, err := d.getClient().SSHKey.Delete(context.Background(), key); err != nil { - log.Errorf("could not delete ssh key: %v", err) - return - } +func (d *Driver) destroyDangling() { + for _, destructor := range d.dangling { + destructor() } } +// GetSSHHostname retrieves the SSH host to connect to the machine; see [drivers.Driver.GetSSHHostname] func (d *Driver) GetSSHHostname() (string, error) { return d.GetIP() } +// GetURL retrieves the URL of the docker daemon on the machine; see [drivers.Driver.GetURL] func (d *Driver) GetURL() (string, error) { if err := drivers.MustBeRunning(d); err != nil { return "", errors.Wrap(err, "could not execute drivers.MustBeRunning") @@ -488,6 +619,7 @@ func (d *Driver) GetURL() (string, error) { return fmt.Sprintf("tcp://%s", net.JoinHostPort(ip, "2376")), nil } +// GetState retrieves the state the machine is currently in; see [drivers.Driver.GetState] func (d *Driver) GetState() (state.State, error) { srv, _, err := d.getClient().Server.GetByID(context.Background(), d.ServerID) if err != nil { @@ -508,6 +640,7 @@ func (d *Driver) GetState() (state.State, error) { return state.None, nil } +// Remove deletes the hetzner server and additional resources created during creation; see [drivers.Driver.Remove] func (d *Driver) Remove() error { if d.ServerID != 0 { srv, err := d.getServerHandle() @@ -523,25 +656,31 @@ func (d *Driver) Remove() error { if _, err := d.getClient().Server.Delete(context.Background(), srv); err != nil { return errors.Wrap(err, "could not delete server") } + + // failure to remove a placement group is not a hard error + if softErr := d.removeEmptyServerPlacementGroup(srv); softErr != nil { + log.Error(softErr) + } } } - // Failing to remove these is just a soft error + // failure to remove a key is not ha hard error for i, id := range d.AdditionalKeyIDs { log.Infof(" -> Destroying additional key #%d (%d)", i, id) - key, _, err := d.getClient().SSHKey.GetByID(context.Background(), id) - if err != nil { - log.Warnf(" -> -> could not retrieve key %v", err) + key, _, softErr := d.getClient().SSHKey.GetByID(context.Background(), id) + if softErr != nil { + log.Warnf(" -> -> could not retrieve key %v", softErr) } else if key == nil { log.Warnf(" -> -> %d no longer exists", id) } - _, err = d.getClient().SSHKey.Delete(context.Background(), key) - if err != nil { - log.Warnf(" -> -> could not remove key: %v", err) + _, softErr = d.getClient().SSHKey.Delete(context.Background(), key) + if softErr != nil { + log.Warnf(" -> -> could not remove key: %v", softErr) } } + // failure to remove a server-specific key is a hard error if !d.IsExistingKey && d.KeyID != 0 { key, err := d.getKey() if err != nil { @@ -562,6 +701,7 @@ func (d *Driver) Remove() error { return nil } +// Restart instructs the hetzner cloud server to reboot; see [drivers.Driver.Restart] func (d *Driver) Restart() error { srv, err := d.getServerHandle() if err != nil { @@ -581,6 +721,7 @@ func (d *Driver) Restart() error { return d.waitForAction(act) } +// Start instructs the hetzner cloud server to power up; see [drivers.Driver.Start] func (d *Driver) Start() error { srv, err := d.getServerHandle() if err != nil { @@ -600,6 +741,7 @@ func (d *Driver) Start() error { return d.waitForAction(act) } +// Stop instructs the hetzner cloud server to shut down; see [drivers.Driver.Stop] func (d *Driver) Stop() error { srv, err := d.getServerHandle() if err != nil { @@ -619,6 +761,7 @@ func (d *Driver) Stop() error { return d.waitForAction(act) } +// Kill forcefully shuts down the hetzner cloud server; see [drivers.Driver.Kill] func (d *Driver) Kill() error { srv, err := d.getServerHandle() if err != nil { @@ -721,19 +864,19 @@ func (d *Driver) getKey() (*hcloud.SSHKey, error) { return stype, nil } -func (d *Driver) getRemoteKeyWithSameFingerprint(pubkey_byte []byte) (*hcloud.SSHKey, error) { - pubkey, _, _, _, err := ssh.ParseAuthorizedKey(pubkey_byte) +func (d *Driver) getRemoteKeyWithSameFingerprint(publicKeyBytes []byte) (*hcloud.SSHKey, error) { + publicKey, _, _, _, err := ssh.ParseAuthorizedKey(publicKeyBytes) if err != nil { return nil, errors.Wrap(err, "could not parse ssh public key") } - fp := ssh.FingerprintLegacyMD5(pubkey) + fp := ssh.FingerprintLegacyMD5(publicKey) - remotekey, _, err := d.getClient().SSHKey.GetByFingerprint(context.Background(), fp) + remoteKey, _, err := d.getClient().SSHKey.GetByFingerprint(context.Background(), fp) if err != nil { - return remotekey, errors.Wrap(err, "could not get sshkey by fingerprint") + return remoteKey, errors.Wrap(err, "could not get sshkey by fingerprint") } - return remotekey, nil + return remoteKey, nil } func (d *Driver) getServerHandle() (*hcloud.Server, error) { @@ -774,3 +917,101 @@ func (d *Driver) waitForAction(a *hcloud.Action) error { } return nil } + +func (d *Driver) labelName(name string) string { + return labelNamespace + "/" + name +} + +func (d *Driver) getAutoPlacementGroup() (*hcloud.PlacementGroup, error) { + res, err := d.getClient().PlacementGroup.AllWithOpts(context.Background(), hcloud.PlacementGroupListOpts{ + ListOpts: hcloud.ListOpts{LabelSelector: d.labelName(labelAutoSpreadPg)}, + }) + + if err != nil { + return nil, err + } + + if len(res) != 0 { + return res[0], nil + } + + grp, err := d.makePlacementGroup("Docker-Machine auto spread", map[string]string{ + d.labelName(labelAutoSpreadPg): "true", + d.labelName(labelAutoCreated): "true", + }) + + return grp, err +} + +func (d *Driver) makePlacementGroup(name string, labels map[string]string) (*hcloud.PlacementGroup, error) { + grp, _, err := d.getClient().PlacementGroup.Create(context.Background(), hcloud.PlacementGroupCreateOpts{ + Name: name, + Labels: labels, + Type: "spread", + }) + + if grp.PlacementGroup != nil { + d.dangling = append(d.dangling, func() { + _, err := d.getClient().PlacementGroup.Delete(context.Background(), grp.PlacementGroup) + if err != nil { + log.Errorf("could not delete placement group: %v", err) + } + }) + } + + if err != nil { + return nil, fmt.Errorf("could not create placement group: %w", err) + } + + return grp.PlacementGroup, nil +} + +func (d *Driver) getPlacementGroup() (*hcloud.PlacementGroup, error) { + if d.placementGroup == "" { + return nil, nil + } else if d.cachedPGrp != nil { + return d.cachedPGrp, nil + } + + name := d.placementGroup + if name == autoSpreadPgName { + grp, err := d.getAutoPlacementGroup() + d.cachedPGrp = grp + return grp, err + } else { + client := d.getClient().PlacementGroup + grp, _, err := client.Get(context.Background(), name) + if err != nil { + return nil, fmt.Errorf("could not get placement group: %w", err) + } + + if grp != nil { + return grp, nil + } + + return d.makePlacementGroup(name, map[string]string{d.labelName(labelAutoCreated): "true"}) + } +} + +func (d *Driver) removeEmptyServerPlacementGroup(srv *hcloud.Server) error { + pg := srv.PlacementGroup + if pg == nil { + return nil + } + + if len(pg.Servers) > 1 { + log.Debugf("more than 1 servers in group, ignoring %v", pg) + return nil + } + + if auto, exists := pg.Labels[d.labelName(labelAutoCreated)]; exists && auto == "true" { + _, err := d.getClient().PlacementGroup.Delete(context.Background(), pg) + if err != nil { + return fmt.Errorf("could not remove placement group: %w", err) + } + return nil + } else { + log.Debugf("group not auto-created, ignoring: %v", pg) + return nil + } +} diff --git a/go.mod b/go.mod index 3ab9e7e882e389e2ce74f2382075b8d89048b528..2a00d125307ffd356d20cd1204035f6f728a0b89 100644 --- a/go.mod +++ b/go.mod @@ -6,12 +6,8 @@ require ( github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect github.com/docker/docker v0.0.0-20181018193557-f7e5154f37a4 // indirect github.com/docker/machine v0.16.2 - github.com/hetznercloud/hcloud-go v1.24.0 - github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect - github.com/pkg/errors v0.8.1 - github.com/sirupsen/logrus v1.4.2 // indirect - github.com/stretchr/testify v1.3.0 // indirect - golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 - golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb // indirect + github.com/hetznercloud/hcloud-go v1.32.0 + github.com/pkg/errors v0.9.1 + golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 gotest.tools v2.2.0+incompatible // indirect ) diff --git a/go.sum b/go.sum index 5b1ed4497a9f93aabc19947a8beaf240878d6654..f0e14198d4bd2f5c06ef60642e6ddcd9e2f18370 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,17 @@ +cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 h1:w+iIsaOQNcT7OZ575w+acHgRric5iCyQh+xv+KJ4HB8= github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78/go.mod h1:LmzpDX56iTiv29bbRTIsUNlaFfuhWRQBWjQdVyAevI8= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= +github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY= +github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -7,37 +19,143 @@ github.com/docker/docker v0.0.0-20181018193557-f7e5154f37a4 h1:u7P9ul4ElF92ZjxTw github.com/docker/docker v0.0.0-20181018193557-f7e5154f37a4/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= github.com/docker/machine v0.16.2 h1:jyF9k3Zg+oIGxxSdYKPScyj3HqFZ6FjgA/3sblcASiU= github.com/docker/machine v0.16.2/go.mod h1:I8mPNDeK1uH+JTcUU7X0ZW8KiYz0jyAgNaeSJ1rCfDI= -github.com/google/go-cmp v0.5.2 h1:X2ev0eStA3AbceY54o37/0PQ/UWqKEiiO2dKL5OPaFM= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/hetznercloud/hcloud-go v1.24.0 h1:/CeHDzhH3Fhm83pjxvE3xNNLbvACl0Lu1/auJ83gG5U= -github.com/hetznercloud/hcloud-go v1.24.0/go.mod h1:3YmyK8yaZZ48syie6xpm3dt26rtB6s65AisBHylXYFA= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= +github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/golang/protobuf v1.4.3 h1:JjCZWpVbqXDqFVmTfYWEVTMIYrL/NPdPSCHPJ0T/raM= +github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/hetznercloud/hcloud-go v1.32.0 h1:7zyN2V7hMlhm3HZdxOarmOtvzKvkcYKjM0hcwYMQZz0= +github.com/hetznercloud/hcloud-go v1.32.0/go.mod h1:XX/TQub3ge0yWR2yHWmnDVIrB+MQbda1pHxkUmDlUME= +github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/konsorten/go-windows-terminal-sequences v1.0.3 h1:CE8S1cTafDpPvMhIxNJKvHsGVBgn1xWYf1NbHQhywc8= +github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sirupsen/logrus v1.4.2 h1:SPIRibHv4MatM3XXNO2BJeFLZwZ2LvZgfQ5+UNI2im4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= +github.com/prometheus/client_golang v1.11.0 h1:HNkLOAEQMIDv/K+04rukrLx6ch7msSRwf3/SASFAGtQ= +github.com/prometheus/client_golang v1.11.0/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M= +github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.26.0 h1:iMAkS2TDoNWnKM+Kopnx/8tnEStIfpYA0ur0xQzzhMQ= +github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.6.0 h1:mxy4L2jP6qMonqmq+aTtOx1ifVWUgG/TAmntgbh3xv4= +github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= +github.com/sirupsen/logrus v1.6.0 h1:UBcNElsrwanuuMsnGSlYmtmgbb23qDR5dG+6X6Oo89I= +github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4 h1:HuIa8hRrWRSrqYzx1qI49NNxhdi2PrY7gxVSq1JjLDc= -golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 h1:psW17arqaxU48Z5kZ0CQnkZWQJsqcURM6tKiBApRjXI= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb h1:fgwFCsaw9buMuxNd6+DQfAuSFqbNiQZpcgJQAgJsK6k= -golang.org/x/sys v0.0.0-20190626221950-04f50cda93cb/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40 h1:JWgyZ1qgdTaF3N3oxC+MdTV7qvEEgHo3otj+HB5CM7Q= +golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +google.golang.org/protobuf v1.26.0-rc.1 h1:7QnIQpGRHE5RnLKnESfDoxm2dTapTZua5a0kS0A+VXQ= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo= +gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo= gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=