diff --git a/.goreleaser.yml b/.goreleaser.yml index b731c84..819c7b5 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -4,12 +4,41 @@ before: hooks: - go mod download builds: -- env: +- id: docker-machine-driver-metal + env: - CGO_ENABLED=0 - GO111MODULE=on binary: docker-machine-driver-metal ldflags: - - -X github.com/equinix/docker-machine-driver-metal/pkg/drivers/metal/metal.version={{.Version}} + - -s -w -X github.com/equinix/docker-machine-driver-metal/pkg/drivers/metal.version={{.Version}} + goos: + - windows + - darwin + - linux + goarch: + - amd64 + - arm + - arm64 + goarm: + - 6 + - 7 + ignore: + - goos: windows + goarch: arm + - goos: windows + goarch: arm64 + - goos: darwin + goarch: arm64 + - goos: darwin + goarch: arm +- id: docker-machine-driver-packet + env: + - CGO_ENABLED=0 + - GO111MODULE=on + binary: docker-machine-driver-packet + ldflags: + - -s -w -X github.com/equinix/docker-machine-driver-metal/pkg/drivers/metal.version={{.Version}} + - -s -w -X github.com/equinix/docker-machine-driver-metal/pkg/drivers/metal.driverName=packet goos: - windows - darwin diff --git a/README.md b/README.md index 65fb25d..9ba1072 100644 --- a/README.md +++ b/README.md @@ -22,16 +22,17 @@ docker-machine create --driver metal You can find the supported arguments by running `docker-machine create -d metal --help` (Equinix Metal specific arguments are shown below): -| Argument | Default | Description | Environment | Config | -| --------------------------- | -------------- | ---------------------------------------------------------------------------- | ----------- | ---------- | -| `--metal-api-key` | | Equinix Metal API Key | `METAL_AUTH_TOKEN` | `token` or `auth-token` +| Argument | Default | Description | Environment | Config | +| --------------------------- | -------------- | ---------------------------------------------------------------------------- | ------------------------ | ----------------------- | +| `--metal-api-key` | | Deprecated API Key flag (use auth token) | `METAL_API_KEY` | +| `--metal-auth-token` | | Equinix Metal Authentication Token | `METAL_AUTH_TOKEN` | `token` or `auth-token` | | `--metal-billing-cycle` | `hourly` | Equinix Metal billing cycle, hourly or monthly | `METAL_BILLING_CYCLE` | -| `--metal-facility-code` | | Equinix Metal facility code | `METAL_FACILITY_CODE` |`facility` +| `--metal-facility-code` | | Equinix Metal facility code | `METAL_FACILITY_CODE` | `facility` | | `--metal-hw-reservation-id` | | Equinix Metal Reserved hardware ID | `METAL_HW_ID` | -| `--metal-metro-code` | | Equinix Metal metro code ("dc" is used if empty and facility is not set) | `METAL_METRO_CODE` |`metro` -| `--metal-os` | `ubuntu_20_04` | Equinix Metal OS | `METAL_OS` |`operating-system` -| `--metal-plan` | `c3.small.x86` | Equinix Metal Server Plan | `METAL_PLAN` |`plan` -| `--metal-project-id` | | Equinix Metal Project Id | `METAL_PROJECT_ID` |`project` +| `--metal-metro-code` | | Equinix Metal metro code ("dc" is used if empty and facility is not set) | `METAL_METRO_CODE` | `metro` | +| `--metal-os` | `ubuntu_20_04` | Equinix Metal OS | `METAL_OS` | `operating-system` | +| `--metal-plan` | `c3.small.x86` | Equinix Metal Server Plan | `METAL_PLAN` | `plan` | +| `--metal-project-id` | | Equinix Metal Project Id | `METAL_PROJECT_ID` | `project` | | `--metal-spot-instance` | | Request a Equinix Metal Spot Instance | `METAL_SPOT_INSTANCE` | | `--metal-spot-price-max` | | The maximum Equinix Metal Spot Price | `METAL_SPOT_PRICE_MAX` | | `--metal-termination-time` | | The Equinix Metal Instance Termination Time | `METAL_TERMINATION_TIME` | @@ -40,6 +41,8 @@ You can find the supported arguments by running `docker-machine create -d metal Where denoted, values may be loaded from the environment or from the `~/.config/equinix/metal.yaml` file which can be created with the [Equinix Metal CLI](https://github.com/equinix/metal-cli#metal-cli). +In order to support existing installations, a Packet branded binary is also available with each release. When the `packet` binary is used, all `METAL` environment variables and `metal` arguments should be substituted for `PACKET` and `packet`, respectively. + ### Example usage This creates the following: @@ -108,7 +111,7 @@ To monitor the Docker debugging details and the Equinix Metal API calls: go build PACKNGO_DEBUG=1 PATH=`pwd`:$PATH docker-machine \ --debug create -d metal \ - --metal-api-key=$METAL_AUTH_TOKEN \ + --metal-auth-token=$METAL_AUTH_TOKEN \ --metal-project-id=$METAL_PROJECT \ foo ``` diff --git a/go.mod b/go.mod index 9c0c894..95ca0cb 100644 --- a/go.mod +++ b/go.mod @@ -7,8 +7,7 @@ require ( github.com/carmo-evan/strtotime v0.0.0-20200108203155-3136cf889e3b github.com/docker/docker v0.0.0-20180805161158-f57f260b49b6 // indirect github.com/docker/machine v0.16.2 - github.com/google/go-cmp v0.3.0 // indirect - github.com/packethost/packngo v0.17.0 + github.com/packethost/packngo v0.19.1 github.com/pkg/errors v0.8.1 // indirect github.com/sirupsen/logrus v1.6.0 // indirect github.com/stretchr/testify v1.5.1 diff --git a/go.sum b/go.sum index 473851b..b0d5c52 100644 --- a/go.sum +++ b/go.sum @@ -11,12 +11,12 @@ github.com/docker/docker v0.0.0-20180805161158-f57f260b49b6 h1:6mGj2QkqSqEV8KFY6 github.com/docker/docker v0.0.0-20180805161158-f57f260b49b6/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.3.0 h1:crn/baboCvb5fXaQ0IJ1SGTsTVrWpDsCWC8EGETZijY= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.6 h1:BKbKCqvP6I+rmFHt06ZmyQtvB8xAkWdhFyr0ZUNZcxQ= +github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 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/packethost/packngo v0.17.0 h1:fGPlj9NDt6ejOrAMfUgx955oCaR1QBKA9pec14m0L38= -github.com/packethost/packngo v0.17.0/go.mod h1:YrtUNN9IRjjqN6zK+cy2IYoi3EjHfoWTWxJkI1I1Vk0= +github.com/packethost/packngo v0.19.1 h1:zuZasgaV4qHMeQ+djENj21w8vhgpoZO2h1a09buVjD8= +github.com/packethost/packngo v0.19.1/go.mod h1:/UHguFdPs6Lf6FOkkSEPnRY5tgS0fsVM+Zv/bvBrmt0= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -38,6 +38,7 @@ golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9 h1:YTzHMGlqJu67/uEo1lBv0n3wBXhXNeUbB1XfN2vmTm0= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/pkg/drivers/metal/metal.go b/pkg/drivers/metal/metal.go index a4937f3..7bb6d94 100644 --- a/pkg/drivers/metal/metal.go +++ b/pkg/drivers/metal/metal.go @@ -30,14 +30,57 @@ const ( defaultMetro = "dc" ) +type envSuffix string +type argSuffix string + var ( // version is set by goreleaser at build time version = "devel" + driverName = "metal" + + envAuthToken envSuffix = "_AUTH_TOKEN" + envApiKey envSuffix = "_API_KEY" + envProjectID envSuffix = "_PROJECT_ID" + envOS envSuffix = "_OS" + envFacilityCode envSuffix = "_FACILITY_CODE" + envMetroCode envSuffix = "_METRO_CODE" + envPlan envSuffix = "_PLAN" + envHwId envSuffix = "_HW_ID" + envBillingCycle envSuffix = "_BILLING_CYCLE" + envUserdata envSuffix = "_USERDATA" + envSpotInstance envSuffix = "_SPOT_INSTANCE" + envSpotPriceMax envSuffix = "_SPOT_PRICE_MAX" + envTerminationTime envSuffix = "_TERMINATION_TIME" + envUAPrefix envSuffix = "_UA_PREFIX" + + argAuthToken argSuffix = "-auth-token" + argApiKey argSuffix = "-api-key" + argProjectID argSuffix = "-project-id" + argOS argSuffix = "-os" + argFacilityCode argSuffix = "-facility-code" + argMetroCode argSuffix = "-metro-code" + argPlan argSuffix = "-plan" + argHwId argSuffix = "-hw-reservation-id" + argBillingCycle argSuffix = "-billing-cycle" + argUserdata argSuffix = "-userdata" + argSpotInstance argSuffix = "-spot-instance" + argSpotPriceMax argSuffix = "-spot-price-max" + argTerminationTime argSuffix = "-termination-time" + argUAPrefix argSuffix = "-ua-prefix" + // build time check that the Driver type implements the Driver interface _ drivers.Driver = &Driver{} ) +func argPrefix(f argSuffix) string { + return driverName + string(f) +} + +func envPrefix(f envSuffix) string { + return strings.ToUpper(driverName) + string(f) +} + type Driver struct { *drivers.BaseDriver ApiKey string @@ -74,82 +117,87 @@ func NewDriver(hostName, storePath string) *Driver { func (d *Driver) GetCreateFlags() []mcnflag.Flag { return []mcnflag.Flag{ mcnflag.StringFlag{ - Name: "metal-api-key", - Usage: "Equinix Metal API Key", - EnvVar: "METAL_AUTH_TOKEN", + Name: argPrefix(argAuthToken), + Usage: "Equinix Metal Authentication Token", + EnvVar: envPrefix(envAuthToken), + }, + mcnflag.StringFlag{ + Name: argPrefix(argApiKey), + Usage: "Authentication Key (deprecated name, use Auth Token)", + EnvVar: envPrefix(envApiKey), }, mcnflag.StringFlag{ - Name: "metal-project-id", + Name: argPrefix(argProjectID), Usage: "Equinix Metal Project Id", - EnvVar: "METAL_PROJECT_ID", + EnvVar: envPrefix(envProjectID), }, mcnflag.StringFlag{ - Name: "metal-os", + Name: argPrefix(argOS), Usage: "Equinix Metal OS", Value: defaultOS, - EnvVar: "METAL_OS", + EnvVar: envPrefix(envOS), }, mcnflag.StringFlag{ - Name: "metal-facility-code", + Name: argPrefix(argFacilityCode), Usage: "Equinix Metal facility code", - EnvVar: "METAL_FACILITY_CODE", + EnvVar: envPrefix(envFacilityCode), }, mcnflag.StringFlag{ - Name: "metal-metro-code", + Name: argPrefix(argMetroCode), Usage: fmt.Sprintf("Equinix Metal metro code (%q is used if empty and facility is not set)", defaultMetro), - EnvVar: "METAL_METRO_CODE", + EnvVar: envPrefix(envMetroCode), // We don't set Value because Facility was previously required and // defaulted. Existing configurations with "Facility" should not // break. Setting a default metro value would break those // configurations. }, mcnflag.StringFlag{ - Name: "metal-plan", + Name: argPrefix(argPlan), Usage: "Equinix Metal Server Plan", Value: "c3.small.x86", - EnvVar: "METAL_PLAN", + EnvVar: envPrefix(envPlan), }, mcnflag.StringFlag{ - Name: "metal-hw-reservation-id", + Name: argPrefix(argHwId), Usage: "Equinix Metal Reserved hardware ID", - EnvVar: "METAL_HW_ID", + EnvVar: envPrefix(envHwId), }, mcnflag.StringFlag{ - Name: "metal-billing-cycle", + Name: argPrefix(argBillingCycle), Usage: "Equinix Metal billing cycle, hourly or monthly", Value: "hourly", - EnvVar: "METAL_BILLING_CYCLE", + EnvVar: envPrefix(envBillingCycle), }, mcnflag.StringFlag{ - Name: "metal-userdata", + Name: argPrefix(argUserdata), Usage: "Path to file with cloud-init user-data", - EnvVar: "METAL_USERDATA", + EnvVar: envPrefix(envUserdata), }, mcnflag.BoolFlag{ - Name: "metal-spot-instance", + Name: argPrefix(argSpotInstance), Usage: "Request a Equinix Metal Spot Instance", - EnvVar: "METAL_SPOT_INSTANCE", + EnvVar: envPrefix(envSpotInstance), }, mcnflag.StringFlag{ - Name: "metal-spot-price-max", + Name: argPrefix(argSpotPriceMax), Usage: "The maximum Equinix Metal Spot Price", - EnvVar: "METAL_SPOT_PRICE_MAX", + EnvVar: envPrefix(envSpotPriceMax), }, mcnflag.StringFlag{ - Name: "metal-termination-time", + Name: argPrefix(argTerminationTime), Usage: "The Equinix Metal Instance Termination Time", - EnvVar: "METAL_TERMINATION_TIME", + EnvVar: envPrefix(envTerminationTime), }, mcnflag.StringFlag{ - EnvVar: "METAL_UA_PREFIX", - Name: "metal-ua-prefix", - Usage: "Prefix the User-Agent in Equinix Metal API calls with some 'product/version'", + Name: argPrefix(argUAPrefix), + Usage: fmt.Sprintf("Prefix the User-Agent in Equinix Metal API calls with some 'product/version' %s %s", version, driverName), + EnvVar: envPrefix(envUAPrefix), }, } } func (d *Driver) DriverName() string { - return "metal" + return driverName } func (d *Driver) setConfigFromFile() error { @@ -183,18 +231,30 @@ func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { } // override config file values with command-line values for k, p := range map[string]*string{ - "metal-os": &d.OperatingSystem, - "metal-api-key": &d.ApiKey, - "metal-project-id": &d.ProjectID, - "metal-metro-code": &d.Metro, - "metal-facility-code": &d.Facility, - "metal-plan": &d.Plan, + argPrefix(argOS): &d.OperatingSystem, + argPrefix(argAuthToken): &d.ApiKey, + argPrefix(argProjectID): &d.ProjectID, + argPrefix(argMetroCode): &d.Metro, + argPrefix(argFacilityCode): &d.Facility, + argPrefix(argPlan): &d.Plan, } { if v := flags.String(k); v != "" { *p = v } } + oldApiKey := flags.String(argPrefix(argApiKey)) + + if d.ApiKey == "" { + d.ApiKey = oldApiKey + + if d.ApiKey == "" { + return fmt.Errorf("%s driver requires the --%s option", driverName, argPrefix(argAuthToken)) + } + } else if oldApiKey != "" { + log.Warnf("ignoring API Key setting (%s, %s)", argPrefix(argApiKey), envPrefix(envApiKey)) + } + if strings.Contains(d.OperatingSystem, "coreos") { d.SSHUser = "core" } @@ -202,14 +262,14 @@ func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { d.SSHUser = "rancher" } - d.BillingCycle = flags.String("metal-billing-cycle") - d.UserAgentPrefix = flags.String("metal-ua-prefix") - d.UserDataFile = flags.String("metal-userdata") - d.HardwareReserverationID = flags.String("metal-hw-reservation-id") - d.SpotInstance = flags.Bool("metal-spot-instance") + d.BillingCycle = flags.String(argPrefix(argBillingCycle)) + d.UserAgentPrefix = flags.String(argPrefix(argUAPrefix)) + d.UserDataFile = flags.String(argPrefix(argUserdata)) + d.HardwareReserverationID = flags.String(argPrefix(argHwId)) + d.SpotInstance = flags.Bool(argPrefix(argSpotInstance)) if d.SpotInstance { - SpotPriceMax := flags.String("metal-spot-price-max") + SpotPriceMax := flags.String(argPrefix(argSpotPriceMax)) if SpotPriceMax == "" { d.SpotPriceMax = -1 } else { @@ -220,7 +280,7 @@ func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { d.SpotPriceMax = SpotPriceMax } - TerminationTime := flags.String("metal-termination-time") + TerminationTime := flags.String(argPrefix(argTerminationTime)) if TerminationTime == "" { d.TerminationTime = nil } else { @@ -229,17 +289,14 @@ func (d *Driver) SetConfigFromFlags(flags drivers.DriverOptions) error { return err } if Timestamp <= time.Now().Unix() { - return fmt.Errorf("--metal-termination-time cannot be in the past") + return fmt.Errorf("--%s cannot be in the past", argPrefix(argTerminationTime)) } d.TerminationTime = &packngo.Timestamp{Time: time.Unix(Timestamp, 0)} } } - if d.ApiKey == "" { - return fmt.Errorf("metal driver requires the --metal-api-key option") - } if d.ProjectID == "" { - return fmt.Errorf("metal driver requires the --metal-project-id option") + return fmt.Errorf("%s driver requires the --%s option", driverName, argPrefix(argProjectID)) } return nil @@ -261,7 +318,7 @@ func (d *Driver) PreCreateCheck() error { return err } if !stringInSlice(d.OperatingSystem, flavors) { - return fmt.Errorf("specified --metal-os not one of %v", strings.Join(flavors, ", ")) + return fmt.Errorf("specified --%s not one of %v", argPrefix(argOS), strings.Join(flavors, ", ")) } if d.Metro == "" && d.Facility == "" { @@ -329,11 +386,12 @@ func (d *Driver) Create() error { log.Info("Provisioning Equinix Metal server...") newDevice, _, err := client.Devices.Create(createRequest) if err != nil { - //cleanup ssh keys if device faild - if _, err := client.SSHKeys.Delete(d.SSHKeyID); err != nil { - if er, ok := err.(*packngo.ErrorResponse); !ok || er.Response.StatusCode != http.StatusNotFound { - return err - } + log.Errorf("device could not be created: %s", err) + + //cleanup ssh keys if device failed + if _, err := client.SSHKeys.Delete(d.SSHKeyID); ignoreStatusCodes(err, http.StatusForbidden, http.StatusNotFound) != nil { + log.Errorf("ssh-key could not be deleted: %s", err) + return err } return err } @@ -462,21 +520,28 @@ func (d *Driver) Stop() error { return err } -func (d *Driver) Remove() error { - client := d.getClient() +func ignoreStatusCodes(err error, codes ...int) error { + e, ok := err.(*packngo.ErrorResponse) + if !ok || e.Response == nil { + return err + } - if _, err := client.SSHKeys.Delete(d.SSHKeyID); err != nil { - if er, ok := err.(*packngo.ErrorResponse); !ok || er.Response.StatusCode != 404 { - return err + for _, c := range codes { + if e.Response.StatusCode == c { + return nil } } + return err +} - if _, err := client.Devices.Delete(d.DeviceID, false); err != nil { - if er, ok := err.(*packngo.ErrorResponse); !ok || er.Response.StatusCode != 404 { - return err - } +func (d *Driver) Remove() error { + client := d.getClient() + if _, err := client.SSHKeys.Delete(d.SSHKeyID); ignoreStatusCodes(err, http.StatusForbidden, http.StatusNotFound) != nil { + return err } - return nil + + _, err := client.Devices.Delete(d.DeviceID, false) + return ignoreStatusCodes(err, http.StatusForbidden, http.StatusNotFound) } func (d *Driver) Restart() error { @@ -543,7 +608,7 @@ func validateFacility(client *packngo.Client, facility string) error { } } - return fmt.Errorf("metal requires a valid facility") + return fmt.Errorf("%s requires a valid facility", driverName) } func validateMetro(client *packngo.Client, metro string) error { @@ -557,7 +622,7 @@ func validateMetro(client *packngo.Client, metro string) error { } } - return fmt.Errorf("metal requires a valid metro") + return fmt.Errorf("%s requires a valid metro", driverName) } func stringInSlice(a string, list []string) bool { diff --git a/pkg/drivers/metal/metal_test.go b/pkg/drivers/metal/metal_test.go index 0debdff..13b59cc 100644 --- a/pkg/drivers/metal/metal_test.go +++ b/pkg/drivers/metal/metal_test.go @@ -1,6 +1,7 @@ package metal import ( + "os" "testing" "github.com/docker/machine/libmachine/drivers" @@ -9,7 +10,8 @@ import ( func TestSetConfigFromFlags(t *testing.T) { driver := NewDriver("", "") - + configPath := os.Getenv("METAL_CONFIG") + os.Setenv("METAL_CONFIG", "/does-not-exist") checkFlags := &drivers.CheckDriverOptions{ FlagsValues: map[string]interface{}{ "metal-api-key": "APIKEY", @@ -19,7 +21,7 @@ func TestSetConfigFromFlags(t *testing.T) { } err := driver.SetConfigFromFlags(checkFlags) - + os.Setenv("METAL_CONFIG", configPath) assert.NoError(t, err) assert.Empty(t, checkFlags.InvalidFlags) }