Skip to content

Commit 13bf4f8

Browse files
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)
1 parent 1b3281e commit 13bf4f8

File tree

5 files changed

+205
-19
lines changed

5 files changed

+205
-19
lines changed

README.md

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@ You can find sources and pre-compiled binaries [here](https://github.com/JonasPr
1515

1616
```bash
1717
# Download the binary (this example downloads the binary for linux amd64)
18-
$ 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
19-
$ tar -xvf docker-machine-driver-hetzner_3.11.0_linux_amd64.tar.gz
18+
$ 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
19+
$ tar -xvf docker-machine-driver-hetzner_3.12.0_linux_amd64.tar.gz
2020

2121
# Make it executable and copy the binary in a directory accessible with your $PATH
2222
$ chmod +x docker-machine-driver-hetzner
@@ -99,8 +99,9 @@ $ docker-machine create \
9999
- `--hetzner-existing-key-id`: **requires `--hetzner-existing-key-path`**. Use an existing (remote) SSH key instead of uploading the imported key pair,
100100
see [SSH Keys API](https://docs.hetzner.cloud/#resources-ssh-keys-get) for how to get a list
101101
- `--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.
102-
- `--hetzner-user-data`: Cloud-init based User data
103-
- `--hetzner-user-data-from-file`: Use Cloud-init based User data as file, `--hetzner-user-data` as file name
102+
- `--hetzner-user-data`: Cloud-init based data, passed inline as-is.
103+
- `--hetzner-user-data-file`: Cloud-init based data, read from passed file.
104+
- `--hetzner-user-data-from-file`: DEPRECATED, use `--hetzner-user-data-file`. Read `--hetzner-user-data` as file name and use contents as user-data.
104105
- `--hetzner-volumes`: Volume IDs or names which should be attached to the server
105106
- `--hetzner-networks`: Network IDs or names which should be attached to the server private network interface
106107
- `--hetzner-use-private-network`: Use private network
@@ -142,6 +143,7 @@ was used during creation.
142143
| `--hetzner-existing-key-id` | `HETZNER_EXISTING_KEY_ID` | 0 *(upload new key)* |
143144
| `--hetzner-additional-key` | `HETZNER_ADDITIONAL_KEYS` | |
144145
| `--hetzner-user-data` | `HETZNER_USER_DATA` | |
146+
| `--hetzner-user-data-file` | `HETZNER_USER_DATA_FILE` | |
145147
| `--hetzner-networks` | `HETZNER_NETWORKS` | |
146148
| `--hetzner-firewalls` | `HETZNER_FIREWALLS` | |
147149
| `--hetzner-volumes` | `HETZNER_VOLUMES` | |
@@ -229,3 +231,13 @@ $ export PATH="$PATH:$GOBIN"
229231
# Make docker-machine output help including hetzner-specific options
230232
$ docker-machine create --driver hetzner
231233
```
234+
235+
## Upcoming breaking changes
236+
237+
### 4.0.0
238+
239+
* `--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.
240+
241+
### 5.0.0
242+
243+
* `--hetzner-user-data-from-file` will be removed entirely, including its fallback behavior

driver.go

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ type Driver struct {
3939
ServerID int
4040
cachedServer *hcloud.Server
4141
userData string
42-
userDataFromFile bool
42+
userDataFile string
4343
Volumes []string
4444
Networks []string
4545
UsePrivateNetwork bool
@@ -74,7 +74,7 @@ const (
7474
flagExKeyID = "hetzner-existing-key-id"
7575
flagExKeyPath = "hetzner-existing-key-path"
7676
flagUserData = "hetzner-user-data"
77-
flagUserDataFromFile = "hetzner-user-data-from-file"
77+
flagUserDataFile = "hetzner-user-data-file"
7878
flagVolumes = "hetzner-volumes"
7979
flagNetworks = "hetzner-networks"
8080
flagUsePrivateNetwork = "hetzner-use-private-network"
@@ -104,6 +104,8 @@ const (
104104

105105
flagWaitOnError = "wait-on-error"
106106
defaultWaitOnError = 0
107+
108+
legacyFlagUserDataFromFile = "hetzner-user-data-from-file"
107109
)
108110

109111
// NewDriver initializes a new driver instance; see [drivers.Driver.NewDriver]
@@ -167,13 +169,19 @@ func (d *Driver) GetCreateFlags() []mcnflag.Flag {
167169
mcnflag.StringFlag{
168170
EnvVar: "HETZNER_USER_DATA",
169171
Name: flagUserData,
170-
Usage: "Cloud-init based User data",
172+
Usage: "Cloud-init based user data (inline).",
171173
Value: "",
172174
},
173175
mcnflag.BoolFlag{
174176
EnvVar: "HETZNER_USER_DATA_FROM_FILE",
175-
Name: flagUserDataFromFile,
176-
Usage: "Cloud-init based User data is file",
177+
Name: legacyFlagUserDataFromFile,
178+
Usage: "DEPRECATED, use --hetzner-user-data-file. Treat --hetzner-user-data argument as filename.",
179+
},
180+
mcnflag.StringFlag{
181+
EnvVar: "HETZNER_USER_DATA_FILE",
182+
Name: flagUserDataFile,
183+
Usage: "Cloud-init based user data (read from file)",
184+
Value: "",
177185
},
178186
mcnflag.StringSliceFlag{
179187
EnvVar: "HETZNER_VOLUMES",
@@ -290,8 +298,10 @@ func (d *Driver) setConfigFromFlagsImpl(opts drivers.DriverOptions) error {
290298
d.KeyID = opts.Int(flagExKeyID)
291299
d.IsExistingKey = d.KeyID != 0
292300
d.originalKey = opts.String(flagExKeyPath)
293-
d.userData = opts.String(flagUserData)
294-
d.userDataFromFile = opts.Bool(flagUserDataFromFile)
301+
err := d.setUserDataFlags(opts)
302+
if err != nil {
303+
return err
304+
}
295305
d.Volumes = opts.StringSlice(flagVolumes)
296306
d.Networks = opts.StringSlice(flagNetworks)
297307
disablePublic := opts.Bool(flagDisablePublic)
@@ -316,7 +326,7 @@ func (d *Driver) setConfigFromFlagsImpl(opts drivers.DriverOptions) error {
316326
d.placementGroup = autoSpreadPgName
317327
}
318328

319-
err := d.setLabelsFromFlags(opts)
329+
err = d.setLabelsFromFlags(opts)
320330
if err != nil {
321331
return err
322332
}
@@ -351,6 +361,30 @@ func (d *Driver) setConfigFromFlagsImpl(opts drivers.DriverOptions) error {
351361
return nil
352362
}
353363

364+
func (d *Driver) setUserDataFlags(opts drivers.DriverOptions) error {
365+
userData := opts.String(flagUserData)
366+
userDataFile := opts.String(flagUserDataFile)
367+
368+
if opts.Bool(legacyFlagUserDataFromFile) {
369+
if userDataFile != "" {
370+
return d.flagFailure("--%v and --%v are mutually exclusive", flagUserDataFile, legacyFlagUserDataFromFile)
371+
}
372+
373+
log.Warnf("--%v is deprecated, pass '--%v \"%v\"'", legacyFlagUserDataFromFile, flagUserDataFile, userData)
374+
d.userDataFile = userData
375+
return nil
376+
}
377+
378+
d.userData = userData
379+
d.userDataFile = userDataFile
380+
381+
if d.userData != "" && d.userDataFile != "" {
382+
return d.flagFailure("--%v and --%v are mutually exclusive", flagUserData, flagUserDataFile)
383+
}
384+
385+
return nil
386+
}
387+
354388
// GetSSHUsername retrieves the SSH username used to connect to the server during provisioning
355389
func (d *Driver) GetSSHUsername() string {
356390
return d.SSHUser
@@ -601,13 +635,12 @@ func (d *Driver) makeCreateServerOptions() (*hcloud.ServerCreateOpts, error) {
601635
}
602636

603637
func (d *Driver) getUserData() (string, error) {
604-
userData := d.userData
605-
606-
if !d.userDataFromFile {
607-
return userData, nil
638+
file := d.userDataFile
639+
if file == "" {
640+
return d.userData, nil
608641
}
609642

610-
readUserData, err := os.ReadFile(d.userData)
643+
readUserData, err := os.ReadFile(file)
611644
if err != nil {
612645
return "", err
613646
}

driver_test.go

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
package main
2+
3+
import (
4+
"github.com/docker/machine/commands/commandstest"
5+
"github.com/docker/machine/libmachine/drivers"
6+
"os"
7+
"strings"
8+
"testing"
9+
)
10+
11+
var defaultFlags = map[string]interface{}{
12+
flagAPIToken: "foo",
13+
}
14+
15+
func makeFlags(args map[string]interface{}) drivers.DriverOptions {
16+
combined := make(map[string]interface{}, len(defaultFlags)+len(args))
17+
for k, v := range defaultFlags {
18+
combined[k] = v
19+
}
20+
for k, v := range args {
21+
combined[k] = v
22+
}
23+
24+
return &commandstest.FakeFlagger{Data: combined}
25+
}
26+
27+
func TestUserData(t *testing.T) {
28+
const fileContents = "User data from file"
29+
const inlineContents = "User data"
30+
31+
file := t.TempDir() + string(os.PathSeparator) + "userData"
32+
err := os.WriteFile(file, []byte(fileContents), 0644)
33+
if err != nil {
34+
t.Fatal(err)
35+
}
36+
37+
// mutual exclusion data <=> data file
38+
d := NewDriver()
39+
err = d.setConfigFromFlagsImpl(makeFlags(map[string]interface{}{
40+
flagUserData: inlineContents,
41+
flagUserDataFile: file,
42+
}))
43+
assertMutualExclusion(t, err, flagUserData, flagUserDataFile)
44+
45+
// mutual exclusion data file <=> legacy flag
46+
d = NewDriver()
47+
err = d.setConfigFromFlagsImpl(&commandstest.FakeFlagger{
48+
Data: map[string]interface{}{
49+
flagAPIToken: "foo",
50+
legacyFlagUserDataFromFile: true,
51+
flagUserDataFile: file,
52+
},
53+
})
54+
assertMutualExclusion(t, err, legacyFlagUserDataFromFile, flagUserDataFile)
55+
56+
// inline user data
57+
d = NewDriver()
58+
err = d.setConfigFromFlagsImpl(makeFlags(map[string]interface{}{
59+
flagAPIToken: "foo",
60+
flagUserData: inlineContents,
61+
}))
62+
if err != nil {
63+
t.Fatalf("unexpected error, %v", err)
64+
}
65+
66+
data, err := d.getUserData()
67+
if err != nil {
68+
t.Fatalf("unexpected error, %v", err)
69+
}
70+
if data != inlineContents {
71+
t.Error("content did not match (inline)")
72+
}
73+
74+
// file user data
75+
d = NewDriver()
76+
err = d.setConfigFromFlagsImpl(makeFlags(map[string]interface{}{
77+
flagAPIToken: "foo",
78+
flagUserDataFile: file,
79+
}))
80+
if err != nil {
81+
t.Fatalf("unexpected error, %v", err)
82+
}
83+
84+
data, err = d.getUserData()
85+
if err != nil {
86+
t.Fatalf("unexpected error, %v", err)
87+
}
88+
if data != fileContents {
89+
t.Error("content did not match (file)")
90+
}
91+
92+
// legacy file user data
93+
d = NewDriver()
94+
err = d.setConfigFromFlagsImpl(makeFlags(map[string]interface{}{
95+
flagAPIToken: "foo",
96+
flagUserData: file,
97+
legacyFlagUserDataFromFile: true,
98+
}))
99+
if err != nil {
100+
t.Fatalf("unexpected error, %v", err)
101+
}
102+
103+
data, err = d.getUserData()
104+
if err != nil {
105+
t.Fatalf("unexpected error, %v", err)
106+
}
107+
if data != fileContents {
108+
t.Error("content did not match (legacy-file)")
109+
}
110+
}
111+
112+
func assertMutualExclusion(t *testing.T, err error, flag1, flag2 string) {
113+
if err == nil {
114+
t.Errorf("expected mutually exclusive flags to fail, but no error was thrown: %v %v", flag1, flag2)
115+
return
116+
}
117+
118+
errstr := err.Error()
119+
if !(strings.Contains(errstr, flag1) && strings.Contains(errstr, flag2) && strings.Contains(errstr, "mutually exclusive")) {
120+
t.Errorf("expected mutually exclusive flags to fail, but message differs: %v %v %v", flag1, flag2, errstr)
121+
}
122+
}

go.mod

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,14 @@ require (
99
golang.org/x/crypto v0.3.0
1010
)
1111

12+
replace github.com/codegangsta/cli v1.22.12 => github.com/urfave/cli v1.22.12
13+
1214
require (
1315
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 // indirect
1416
github.com/beorn7/perks v1.0.1 // indirect
1517
github.com/cespare/xxhash/v2 v2.1.2 // indirect
18+
github.com/codegangsta/cli v1.22.12 // indirect
19+
github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect
1620
github.com/docker/docker v20.10.21+incompatible // indirect
1721
github.com/golang/protobuf v1.5.2 // indirect
1822
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
@@ -21,6 +25,7 @@ require (
2125
github.com/prometheus/client_model v0.3.0 // indirect
2226
github.com/prometheus/common v0.37.0 // indirect
2327
github.com/prometheus/procfs v0.8.0 // indirect
28+
github.com/russross/blackfriday/v2 v2.1.0 // indirect
2429
golang.org/x/net v0.2.0 // indirect
2530
golang.org/x/sys v0.2.0 // indirect
2631
golang.org/x/term v0.2.0 // indirect

go.sum

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7
3434
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1 h1:UQHMgLO+TxOElx5B5HZ4hJQsoJ/PvUvKRhJHDQXO8P8=
3535
github.com/Azure/go-ansiterm v0.0.0-20210617225240-d185dfc1b5a1/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
3636
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
37+
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
3738
github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo=
3839
github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc=
3940
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
5354
github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU=
5455
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
5556
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
57+
github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w=
58+
github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
5659
github.com/creack/pty v1.1.11 h1:07n33Z8lZxZ2qwegKbObQohDhXDQxiMMz1NOUGYlesw=
5760
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
5861
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
197200
github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo=
198201
github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4=
199202
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
203+
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
204+
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
200205
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=
201206
github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE=
202207
github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88=
203208
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
204209
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
210+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
211+
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
205212
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
206213
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
207214
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
208-
github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
215+
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
216+
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
217+
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
218+
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
219+
github.com/urfave/cli v1.22.12 h1:igJgVw1JdKH+trcLWLeLwZjU9fEfPesQ+9/e4MQ44S8=
220+
github.com/urfave/cli v1.22.12/go.mod h1:sSBEIC79qR6OvcmsD4U3KABeOTxDqQtdDnaFuUN30b8=
209221
github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
210222
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
211223
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=
490502
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
491503
gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
492504
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
493-
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
505+
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
506+
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
507+
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
494508
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
495509
honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
496510
honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=

0 commit comments

Comments
 (0)