From 13bf4f826babc58494f725e937b5a64795f773ba Mon Sep 17 00:00:00 2001 From: JonasS <jonass@dev.jsje.de> Date: Sun, 26 Feb 2023 13:01:05 +0100 Subject: [PATCH] feat: Add `--hetzner-user-data-file` (closes #99) - added `--hetzner-user-data-file` and deprecated `--hetzner-user-data-from-file` in favor of it (#99, thanks @perlun) --- README.md | 20 ++++++-- driver.go | 59 ++++++++++++++++++------ driver_test.go | 122 +++++++++++++++++++++++++++++++++++++++++++++++++ go.mod | 5 ++ go.sum | 18 +++++++- 5 files changed, 205 insertions(+), 19 deletions(-) create mode 100644 driver_test.go diff --git a/README.md b/README.md index 0ae4e9d..251dacb 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.11.0/docker-machine-driver-hetzner_3.11.0_linux_amd64.tar.gz -$ tar -xvf docker-machine-driver-hetzner_3.11.0_linux_amd64.tar.gz +$ wget https://github.com/JonasProgrammer/docker-machine-driver-hetzner/releases/download/3.12.0/docker-machine-driver-hetzner_3.12.0_linux_amd64.tar.gz +$ tar -xvf docker-machine-driver-hetzner_3.12.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 @@ -99,8 +99,9 @@ $ docker-machine create \ - `--hetzner-existing-key-id`: **requires `--hetzner-existing-key-path`**. Use an existing (remote) SSH key instead of uploading the imported key pair, see [SSH Keys API](https://docs.hetzner.cloud/#resources-ssh-keys-get) for how to get a list - `--hetzner-additional-key`: Upload an additional public key associated with the server, or associate an existing one with the same fingerprint. Can be specified multiple times. -- `--hetzner-user-data`: Cloud-init based User data -- `--hetzner-user-data-from-file`: Use Cloud-init based User data as file, `--hetzner-user-data` as file name +- `--hetzner-user-data`: Cloud-init based data, passed inline as-is. +- `--hetzner-user-data-file`: Cloud-init based data, read from passed file. +- `--hetzner-user-data-from-file`: DEPRECATED, use `--hetzner-user-data-file`. Read `--hetzner-user-data` as file name and use contents as user-data. - `--hetzner-volumes`: Volume IDs or names which should be attached to the server - `--hetzner-networks`: Network IDs or names which should be attached to the server private network interface - `--hetzner-use-private-network`: Use private network @@ -142,6 +143,7 @@ was used during creation. | `--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-user-data-file` | `HETZNER_USER_DATA_FILE` | | | `--hetzner-networks` | `HETZNER_NETWORKS` | | | `--hetzner-firewalls` | `HETZNER_FIREWALLS` | | | `--hetzner-volumes` | `HETZNER_VOLUMES` | | @@ -229,3 +231,13 @@ $ export PATH="$PATH:$GOBIN" # Make docker-machine output help including hetzner-specific options $ docker-machine create --driver hetzner ``` + +## Upcoming breaking changes + +### 4.0.0 + +* `--hetzner-user-data-from-file` will be fully deprecated and its flag description will only read 'DEPRECATED, legacy'; current fallback behaviour will be retained. `--hetzner-flag-user-data-file` should be used instead. + +### 5.0.0 + +* `--hetzner-user-data-from-file` will be removed entirely, including its fallback behavior diff --git a/driver.go b/driver.go index a4e26ec..1a241f1 100644 --- a/driver.go +++ b/driver.go @@ -39,7 +39,7 @@ type Driver struct { ServerID int cachedServer *hcloud.Server userData string - userDataFromFile bool + userDataFile string Volumes []string Networks []string UsePrivateNetwork bool @@ -74,7 +74,7 @@ const ( flagExKeyID = "hetzner-existing-key-id" flagExKeyPath = "hetzner-existing-key-path" flagUserData = "hetzner-user-data" - flagUserDataFromFile = "hetzner-user-data-from-file" + flagUserDataFile = "hetzner-user-data-file" flagVolumes = "hetzner-volumes" flagNetworks = "hetzner-networks" flagUsePrivateNetwork = "hetzner-use-private-network" @@ -104,6 +104,8 @@ const ( flagWaitOnError = "wait-on-error" defaultWaitOnError = 0 + + legacyFlagUserDataFromFile = "hetzner-user-data-from-file" ) // NewDriver initializes a new driver instance; see [drivers.Driver.NewDriver] @@ -167,13 +169,19 @@ func (d *Driver) GetCreateFlags() []mcnflag.Flag { mcnflag.StringFlag{ EnvVar: "HETZNER_USER_DATA", Name: flagUserData, - Usage: "Cloud-init based User data", + Usage: "Cloud-init based user data (inline).", Value: "", }, mcnflag.BoolFlag{ EnvVar: "HETZNER_USER_DATA_FROM_FILE", - Name: flagUserDataFromFile, - Usage: "Cloud-init based User data is file", + Name: legacyFlagUserDataFromFile, + Usage: "DEPRECATED, use --hetzner-user-data-file. Treat --hetzner-user-data argument as filename.", + }, + mcnflag.StringFlag{ + EnvVar: "HETZNER_USER_DATA_FILE", + Name: flagUserDataFile, + Usage: "Cloud-init based user data (read from file)", + Value: "", }, mcnflag.StringSliceFlag{ EnvVar: "HETZNER_VOLUMES", @@ -290,8 +298,10 @@ func (d *Driver) setConfigFromFlagsImpl(opts drivers.DriverOptions) error { d.KeyID = opts.Int(flagExKeyID) d.IsExistingKey = d.KeyID != 0 d.originalKey = opts.String(flagExKeyPath) - d.userData = opts.String(flagUserData) - d.userDataFromFile = opts.Bool(flagUserDataFromFile) + err := d.setUserDataFlags(opts) + if err != nil { + return err + } d.Volumes = opts.StringSlice(flagVolumes) d.Networks = opts.StringSlice(flagNetworks) disablePublic := opts.Bool(flagDisablePublic) @@ -316,7 +326,7 @@ func (d *Driver) setConfigFromFlagsImpl(opts drivers.DriverOptions) error { d.placementGroup = autoSpreadPgName } - err := d.setLabelsFromFlags(opts) + err = d.setLabelsFromFlags(opts) if err != nil { return err } @@ -351,6 +361,30 @@ func (d *Driver) setConfigFromFlagsImpl(opts drivers.DriverOptions) error { return nil } +func (d *Driver) setUserDataFlags(opts drivers.DriverOptions) error { + userData := opts.String(flagUserData) + userDataFile := opts.String(flagUserDataFile) + + if opts.Bool(legacyFlagUserDataFromFile) { + if userDataFile != "" { + return d.flagFailure("--%v and --%v are mutually exclusive", flagUserDataFile, legacyFlagUserDataFromFile) + } + + log.Warnf("--%v is deprecated, pass '--%v \"%v\"'", legacyFlagUserDataFromFile, flagUserDataFile, userData) + d.userDataFile = userData + return nil + } + + d.userData = userData + d.userDataFile = userDataFile + + if d.userData != "" && d.userDataFile != "" { + return d.flagFailure("--%v and --%v are mutually exclusive", flagUserData, flagUserDataFile) + } + + return nil +} + // GetSSHUsername retrieves the SSH username used to connect to the server during provisioning func (d *Driver) GetSSHUsername() string { return d.SSHUser @@ -601,13 +635,12 @@ func (d *Driver) makeCreateServerOptions() (*hcloud.ServerCreateOpts, error) { } func (d *Driver) getUserData() (string, error) { - userData := d.userData - - if !d.userDataFromFile { - return userData, nil + file := d.userDataFile + if file == "" { + return d.userData, nil } - readUserData, err := os.ReadFile(d.userData) + readUserData, err := os.ReadFile(file) if err != nil { return "", err } diff --git a/driver_test.go b/driver_test.go new file mode 100644 index 0000000..7ddea3b --- /dev/null +++ b/driver_test.go @@ -0,0 +1,122 @@ +package main + +import ( + "github.com/docker/machine/commands/commandstest" + "github.com/docker/machine/libmachine/drivers" + "os" + "strings" + "testing" +) + +var defaultFlags = map[string]interface{}{ + flagAPIToken: "foo", +} + +func makeFlags(args map[string]interface{}) drivers.DriverOptions { + combined := make(map[string]interface{}, len(defaultFlags)+len(args)) + for k, v := range defaultFlags { + combined[k] = v + } + for k, v := range args { + combined[k] = v + } + + return &commandstest.FakeFlagger{Data: combined} +} + +func TestUserData(t *testing.T) { + const fileContents = "User data from file" + const inlineContents = "User data" + + file := t.TempDir() + string(os.PathSeparator) + "userData" + err := os.WriteFile(file, []byte(fileContents), 0644) + if err != nil { + t.Fatal(err) + } + + // mutual exclusion data <=> data file + d := NewDriver() + err = d.setConfigFromFlagsImpl(makeFlags(map[string]interface{}{ + flagUserData: inlineContents, + flagUserDataFile: file, + })) + assertMutualExclusion(t, err, flagUserData, flagUserDataFile) + + // mutual exclusion data file <=> legacy flag + d = NewDriver() + err = d.setConfigFromFlagsImpl(&commandstest.FakeFlagger{ + Data: map[string]interface{}{ + flagAPIToken: "foo", + legacyFlagUserDataFromFile: true, + flagUserDataFile: file, + }, + }) + assertMutualExclusion(t, err, legacyFlagUserDataFromFile, flagUserDataFile) + + // inline user data + d = NewDriver() + err = d.setConfigFromFlagsImpl(makeFlags(map[string]interface{}{ + flagAPIToken: "foo", + flagUserData: inlineContents, + })) + if err != nil { + t.Fatalf("unexpected error, %v", err) + } + + data, err := d.getUserData() + if err != nil { + t.Fatalf("unexpected error, %v", err) + } + if data != inlineContents { + t.Error("content did not match (inline)") + } + + // file user data + d = NewDriver() + err = d.setConfigFromFlagsImpl(makeFlags(map[string]interface{}{ + flagAPIToken: "foo", + flagUserDataFile: file, + })) + if err != nil { + t.Fatalf("unexpected error, %v", err) + } + + data, err = d.getUserData() + if err != nil { + t.Fatalf("unexpected error, %v", err) + } + if data != fileContents { + t.Error("content did not match (file)") + } + + // legacy file user data + d = NewDriver() + err = d.setConfigFromFlagsImpl(makeFlags(map[string]interface{}{ + flagAPIToken: "foo", + flagUserData: file, + legacyFlagUserDataFromFile: true, + })) + if err != nil { + t.Fatalf("unexpected error, %v", err) + } + + data, err = d.getUserData() + if err != nil { + t.Fatalf("unexpected error, %v", err) + } + if data != fileContents { + t.Error("content did not match (legacy-file)") + } +} + +func assertMutualExclusion(t *testing.T, err error, flag1, flag2 string) { + if err == nil { + t.Errorf("expected mutually exclusive flags to fail, but no error was thrown: %v %v", flag1, flag2) + return + } + + errstr := err.Error() + if !(strings.Contains(errstr, flag1) && strings.Contains(errstr, flag2) && strings.Contains(errstr, "mutually exclusive")) { + t.Errorf("expected mutually exclusive flags to fail, but message differs: %v %v %v", flag1, flag2, errstr) + } +} diff --git a/go.mod b/go.mod index 4379c1f..0c3eb16 100644 --- a/go.mod +++ b/go.mod @@ -9,10 +9,14 @@ require ( golang.org/x/crypto v0.3.0 ) +replace github.com/codegangsta/cli v1.22.12 => github.com/urfave/cli v1.22.12 + require ( github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.1.2 // indirect + github.com/codegangsta/cli v1.22.12 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect github.com/docker/docker v20.10.21+incompatible // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect @@ -21,6 +25,7 @@ require ( github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect golang.org/x/net v0.2.0 // indirect golang.org/x/sys v0.2.0 // indirect golang.org/x/term v0.2.0 // indirect diff --git a/go.sum b/go.sum index e53cc4a..5c803d8 100644 --- a/go.sum +++ b/go.sum @@ -34,6 +34,7 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7 github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8= github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= 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= @@ -53,6 +54,8 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= +github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw= 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= @@ -197,15 +200,24 @@ github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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/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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= 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.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8= +github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -490,7 +502,9 @@ 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.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -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= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -- GitLab