Skip to content

Commit 0399b7d

Browse files
swalkinshawclaude
andauthored
Add Hetzner Cloud as second server provider (#620)
Adds Hetzner Cloud support to the server commands alongside the existing DigitalOcean provider: - Add hetzner package with Provider implementation using hcloud-go SDK - Implement server creation, listing, regions, sizes, SSH keys - Implement DNS management (zones and records) - Add ProviderHetzner constant and factory support - Update command help text and autocomplete for hetzner provider - Token sourced from HCLOUD_TOKEN environment variable 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Claude Opus 4.5 <[email protected]>
1 parent c256dc5 commit 0399b7d

File tree

9 files changed

+516
-23
lines changed

9 files changed

+516
-23
lines changed

cli_config/cli_config.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,8 @@ func (c *Config) LoadFile(path string) error {
7878
return fmt.Errorf("%w: unsupported value for `database_app`. Must be one of: tableplus, sequel-ace", InvalidConfigErr)
7979
}
8080

81-
if c.Server.Provider != "" && c.Server.Provider != "digitalocean" {
82-
return fmt.Errorf("%w: unsupported value for `server.provider`. Must be one of: digitalocean", InvalidConfigErr)
81+
if c.Server.Provider != "" && c.Server.Provider != "digitalocean" && c.Server.Provider != "hetzner" {
82+
return fmt.Errorf("%w: unsupported value for `server.provider`. Must be one of: digitalocean, hetzner", InvalidConfigErr)
8383
}
8484

8585
return nil

cmd/server_create.go

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ type ServerCreateCommand struct {
3838
func (c *ServerCreateCommand) init() {
3939
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
4040
c.flags.Usage = func() { c.UI.Info(c.Help()) }
41-
c.flags.StringVar(&c.providerFlag, "provider", "", "Cloud provider (digitalocean)")
41+
c.flags.StringVar(&c.providerFlag, "provider", "", "Cloud provider (digitalocean, hetzner)")
4242
c.flags.StringVar(&c.sshKey, "ssh-key", "", "Path to SSH public key to automatically add to new server")
4343
c.flags.StringVar(&c.region, "region", "", "Region to create the server in")
4444
c.flags.StringVar(&c.image, "image", "", "Server image (default: Ubuntu 24.04)")
@@ -251,6 +251,7 @@ Development should be managed separately through the VM commands.
251251
252252
Supported providers:
253253
- digitalocean (default)
254+
- hetzner
254255
255256
The provider can be configured via:
256257
1. --provider flag
@@ -273,7 +274,7 @@ Arguments:
273274
ENVIRONMENT Name of environment (ie: production)
274275
275276
Options:
276-
--provider Cloud provider (digitalocean)
277+
--provider Cloud provider (digitalocean, hetzner)
277278
--region Region to create the server in
278279
--image Server image (default: Ubuntu 24.04)
279280
--size Server size/type
@@ -291,7 +292,7 @@ func (c *ServerCreateCommand) AutocompleteArgs() complete.Predictor {
291292

292293
func (c *ServerCreateCommand) AutocompleteFlags() complete.Flags {
293294
return complete.Flags{
294-
"--provider": complete.PredictSet("digitalocean"),
295+
"--provider": complete.PredictSet("digitalocean", "hetzner"),
295296
"--region": complete.PredictNothing,
296297
"--size": complete.PredictNothing,
297298
"--skip--provision": complete.PredictNothing,
@@ -374,7 +375,9 @@ func (c *ServerCreateCommand) createServer(ctx context.Context, provider server.
374375
return nil, err
375376
}
376377

377-
c.UI.Info(fmt.Sprintf("\n%s Server created => %s", color.GreenString("[✓]"), srv.DashboardURL))
378+
if srv.DashboardURL != "" {
379+
c.UI.Info(fmt.Sprintf("\n%s Server created => %s", color.GreenString("[✓]"), srv.DashboardURL))
380+
}
378381

379382
return srv, nil
380383
}

cmd/server_dns.go

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ type ServerDnsCommand struct {
3434
func (c *ServerDnsCommand) init() {
3535
c.flags = flag.NewFlagSet("", flag.ContinueOnError)
3636
c.flags.Usage = func() { c.UI.Info(c.Help()) }
37-
c.flags.StringVar(&c.providerFlag, "provider", "", "Cloud provider (digitalocean)")
37+
c.flags.StringVar(&c.providerFlag, "provider", "", "Cloud provider (digitalocean, hetzner)")
3838
c.flags.BoolVar(&c.force, "force", false, "Force update of DNS records even if they exist")
3939
c.flags.StringVar(&c.ip, "ip", "", "Host IP of DNS records")
4040
}
@@ -206,6 +206,7 @@ server IP; the host IP can be manually overridden if need be.
206206
207207
Supported providers:
208208
- digitalocean (default)
209+
- hetzner
209210
210211
The provider can be configured via:
211212
1. --provider flag
@@ -235,7 +236,7 @@ Arguments:
235236
ENVIRONMENT Name of environment (ie: production)
236237
237238
Options:
238-
--provider Cloud provider (digitalocean)
239+
--provider Cloud provider (digitalocean, hetzner)
239240
--force Force updating DNS records even if they already exist
240241
--ip Host IP of DNS records
241242
-h, --help Show this help
@@ -250,7 +251,7 @@ func (c *ServerDnsCommand) AutocompleteArgs() complete.Predictor {
250251

251252
func (c *ServerDnsCommand) AutocompleteFlags() complete.Flags {
252253
return complete.Flags{
253-
"--provider": complete.PredictSet("digitalocean"),
254+
"--provider": complete.PredictSet("digitalocean", "hetzner"),
254255
"--ip": complete.PredictNothing,
255256
"--force": complete.PredictNothing,
256257
}

go.mod

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ require (
1010
github.com/google/go-cmp v0.7.0
1111
github.com/hashicorp/cli v1.1.7
1212
github.com/hashicorp/go-version v1.8.0
13+
github.com/hetznercloud/hcloud-go/v2 v2.33.0
1314
github.com/manifoldco/promptui v0.9.0
1415
github.com/mattn/go-isatty v0.0.20
1516
github.com/mcuadros/go-version v0.0.0-20190830083331-035f6764e8d2
@@ -33,10 +34,12 @@ require (
3334
github.com/alessio/shellescape v1.4.1 // indirect
3435
github.com/andybalholm/brotli v1.2.0 // indirect
3536
github.com/armon/go-radix v1.0.0 // indirect
37+
github.com/beorn7/perks v1.0.1 // indirect
3638
github.com/bgentry/speakeasy v0.1.0 // indirect
3739
github.com/bodgit/plumbing v1.3.0 // indirect
3840
github.com/bodgit/sevenzip v1.6.1 // indirect
3941
github.com/bodgit/windows v1.0.1 // indirect
42+
github.com/cespare/xxhash/v2 v2.3.0 // indirect
4043
github.com/chzyer/readline v1.5.0 // indirect
4144
github.com/chzyer/test v1.0.0 // indirect
4245
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
@@ -57,19 +60,24 @@ require (
5760
github.com/minio/minlz v1.0.1 // indirect
5861
github.com/mitchellh/copystructure v1.2.0 // indirect
5962
github.com/mitchellh/reflectwalk v1.0.2 // indirect
63+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
6064
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
6165
github.com/pierrec/lz4/v4 v4.1.22 // indirect
66+
github.com/prometheus/client_golang v1.23.2 // indirect
67+
github.com/prometheus/client_model v0.6.2 // indirect
68+
github.com/prometheus/common v0.66.1 // indirect
69+
github.com/prometheus/procfs v0.16.1 // indirect
6270
github.com/rivo/uniseg v0.2.0 // indirect
63-
github.com/rogpeppe/go-internal v1.8.1 // indirect
6471
github.com/shopspring/decimal v1.3.1 // indirect
6572
github.com/sorairolake/lzip-go v0.3.8 // indirect
6673
github.com/spf13/afero v1.15.0 // indirect
6774
github.com/spf13/cast v1.5.0 // indirect
6875
github.com/ulikunitz/xz v0.5.15 // indirect
76+
go.yaml.in/yaml/v2 v2.4.2 // indirect
6977
go4.org v0.0.0-20230225012048-214862532bf5 // indirect
70-
golang.org/x/net v0.47.0 // indirect
78+
golang.org/x/net v0.48.0 // indirect
7179
golang.org/x/sys v0.39.0 // indirect
7280
golang.org/x/text v0.32.0 // indirect
7381
golang.org/x/time v0.6.0 // indirect
74-
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
82+
google.golang.org/protobuf v1.36.8 // indirect
7583
)

go.sum

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwTo
3131
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
3232
github.com/armon/go-radix v1.0.0 h1:F4z6KzEeeQIMeLFa97iZU6vupzoecKdU5TX24SNppXI=
3333
github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8=
34+
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
35+
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
3436
github.com/bgentry/speakeasy v0.1.0 h1:ByYyxL9InA1OWqxJqqp2A5pYHUrCiAL6K3J+LKSsQkY=
3537
github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs=
3638
github.com/bodgit/plumbing v1.3.0 h1:pf9Itz1JOQgn7vEOE7v7nlEfBykYqvUYioC61TwWCFU=
@@ -40,6 +42,8 @@ github.com/bodgit/sevenzip v1.6.1/go.mod h1:GVoYQbEVbOGT8n2pfqCIMRUaRjQ8F9oSqoBE
4042
github.com/bodgit/windows v1.0.1 h1:tF7K6KOluPYygXa3Z2594zxlkbKPAOvqr97etrGNIz4=
4143
github.com/bodgit/windows v1.0.1/go.mod h1:a6JLwrB4KrTR5hBpp8FI9/9W9jJfeQ2h4XDXU74ZCdM=
4244
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
45+
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
46+
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
4347
github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI=
4448
github.com/chzyer/logex v1.2.0/go.mod h1:9+9sk7u7pGNWYMkh0hdiL++6OeibzJccyQU4p4MedaY=
4549
github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM=
@@ -122,6 +126,8 @@ github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ
122126
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
123127
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
124128
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
129+
github.com/hetznercloud/hcloud-go/v2 v2.33.0 h1:g9hwuo60IXbupXJCYMlO4xDXgxxMPuFk31iOpLXDCV4=
130+
github.com/hetznercloud/hcloud-go/v2 v2.33.0/go.mod h1:GzYEl7slIGKc6Ttt08hjiJvGj8/PbWzcQf6IUi02dIs=
125131
github.com/huandu/xstrings v1.3.3 h1:/Gcsuc1x8JVbJ9/rlye4xZnVAbEkGauT8lbebqcQws4=
126132
github.com/huandu/xstrings v1.3.3/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE=
127133
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
@@ -138,13 +144,14 @@ github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgo
138144
github.com/klauspost/pgzip v1.2.6 h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
139145
github.com/klauspost/pgzip v1.2.6/go.mod h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
140146
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
141-
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
142-
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
143-
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
147+
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
148+
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
144149
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
145150
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
146151
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
147152
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
153+
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
154+
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
148155
github.com/manifoldco/promptui v0.9.0 h1:3V4HzJk1TtXW1MTZMP7mdlwbBpIinw3HztaIlYthEiA=
149156
github.com/manifoldco/promptui v0.9.0/go.mod h1:ka04sppxSGFAtxX0qhlYQjISsg9mR4GWtQEhdbn6Pgg=
150157
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
@@ -170,21 +177,30 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
170177
github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
171178
github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ=
172179
github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw=
180+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
181+
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
173182
github.com/nwaples/rardecode/v2 v2.2.0 h1:4ufPGHiNe1rYJxYfehALLjup4Ls3ck42CWwjKiOqu0A=
174183
github.com/nwaples/rardecode/v2 v2.2.0/go.mod h1:7uz379lSxPe6j9nvzxUZ+n7mnJNgjsRNb6IbvGVHRmw=
175184
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
176185
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
177-
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
178186
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
179187
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
180188
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
181189
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
190+
github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o=
191+
github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg=
182192
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
193+
github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk=
194+
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
195+
github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs=
196+
github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA=
197+
github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg=
198+
github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is=
183199
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
184200
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
185201
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
186-
github.com/rogpeppe/go-internal v1.8.1 h1:geMPLpDpQOgVyCg5z5GoRwLHepNdb71NXb67XFkP+Eg=
187-
github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4nPKWu0nJ5d+o=
202+
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
203+
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
188204
github.com/rwcarlsen/goexif v0.0.0-20190401172101-9e8deecbddbd/go.mod h1:hPqNNc0+uJM6H+SuU8sEs5K5IQeKccPqeSjfgcKGgPk=
189205
github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o=
190206
github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8=
@@ -207,8 +223,8 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5
207223
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
208224
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
209225
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
210-
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
211-
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
226+
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
227+
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
212228
github.com/theckman/yacspin v0.13.12 h1:CdZ57+n0U6JMuh2xqjnjRq5Haj6v1ner2djtLQRzJr4=
213229
github.com/theckman/yacspin v0.13.12/go.mod h1:Rd2+oG2LmQi5f3zC3yeZAOl245z8QOvrH4OPOJNZxLg=
214230
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
@@ -223,6 +239,10 @@ go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU=
223239
go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8=
224240
go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
225241
go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw=
242+
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
243+
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
244+
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
245+
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
226246
go4.org v0.0.0-20230225012048-214862532bf5 h1:nifaUDeh+rPaBCMPMQHZmvJf+QdpLFnuQPwx+LxVmtc=
227247
go4.org v0.0.0-20230225012048-214862532bf5/go.mod h1:F57wTi5Lrj6WLyswp5EYV1ncrEbFGHD4hhz6S1ZYeaU=
228248
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
@@ -276,8 +296,8 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
276296
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
277297
golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY=
278298
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
279-
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
280-
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
299+
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
300+
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
281301
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
282302
golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
283303
golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
@@ -399,6 +419,8 @@ google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyac
399419
google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
400420
google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
401421
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
422+
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
423+
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
402424
gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61 h1:8ajkpB4hXVftY5ko905id+dOnmorcS2CHNxxHLLDcFM=
403425
gopkg.in/alessio/shellescape.v1 v1.0.0-20170105083845-52074bc9df61/go.mod h1:IfMagxm39Ys4ybJrDb7W3Ob8RwxftP0Yy+or/NVz1O8=
404426
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

pkg/server/hetzner/dns.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
package hetzner
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strconv"
7+
8+
"github.com/hetznercloud/hcloud-go/v2/hcloud"
9+
"github.com/roots/trellis-cli/pkg/server/types"
10+
)
11+
12+
const defaultTTL = 300
13+
14+
func (p *Provider) CreateZone(ctx context.Context, domain string) error {
15+
_, _, err := p.client.Zone.Create(ctx, hcloud.ZoneCreateOpts{
16+
Name: domain,
17+
})
18+
return err
19+
}
20+
21+
func (p *Provider) GetZone(ctx context.Context, domain string) (*types.Zone, bool, error) {
22+
zone, _, err := p.client.Zone.GetByName(ctx, domain)
23+
if err != nil {
24+
return nil, false, err
25+
}
26+
if zone == nil {
27+
return nil, false, nil
28+
}
29+
30+
return &types.Zone{
31+
ID: strconv.FormatInt(zone.ID, 10),
32+
Name: zone.Name,
33+
TTL: zone.TTL,
34+
}, true, nil
35+
}
36+
37+
func (p *Provider) CreateRecord(ctx context.Context, domain string, record types.DNSRecord) (*types.DNSRecord, error) {
38+
zone, _, err := p.client.Zone.GetByName(ctx, domain)
39+
if err != nil {
40+
return nil, err
41+
}
42+
if zone == nil {
43+
return nil, fmt.Errorf("zone %s not found", domain)
44+
}
45+
46+
ttl := record.TTL
47+
if ttl == 0 {
48+
ttl = defaultTTL
49+
}
50+
51+
result, _, err := p.client.Zone.CreateRRSet(ctx, zone, hcloud.ZoneRRSetCreateOpts{
52+
Name: record.Name,
53+
Type: hcloud.ZoneRRSetType(record.Type),
54+
TTL: &ttl,
55+
Records: []hcloud.ZoneRRSetRecord{
56+
{Value: record.Value},
57+
},
58+
})
59+
if err != nil {
60+
return nil, err
61+
}
62+
63+
rrset := result.RRSet
64+
var rrsetTTL int
65+
if rrset.TTL != nil {
66+
rrsetTTL = *rrset.TTL
67+
}
68+
69+
return &types.DNSRecord{
70+
ID: rrset.ID,
71+
Type: string(rrset.Type),
72+
Name: rrset.Name,
73+
Value: record.Value,
74+
TTL: rrsetTTL,
75+
}, nil
76+
}
77+
78+
func (p *Provider) DeleteRecord(ctx context.Context, domain string, recordID string) error {
79+
zone, _, err := p.client.Zone.GetByName(ctx, domain)
80+
if err != nil {
81+
return err
82+
}
83+
if zone == nil {
84+
return fmt.Errorf("zone %s not found", domain)
85+
}
86+
87+
rrset, _, err := p.client.Zone.GetRRSetByID(ctx, zone, recordID)
88+
if err != nil {
89+
return err
90+
}
91+
if rrset == nil {
92+
return fmt.Errorf("record %s not found", recordID)
93+
}
94+
95+
_, _, err = p.client.Zone.DeleteRRSet(ctx, rrset)
96+
return err
97+
}
98+
99+
func (p *Provider) ListRecords(ctx context.Context, domain string) ([]types.DNSRecord, error) {
100+
zone, _, err := p.client.Zone.GetByName(ctx, domain)
101+
if err != nil {
102+
return nil, err
103+
}
104+
if zone == nil {
105+
return []types.DNSRecord{}, nil
106+
}
107+
108+
rrsets, err := p.client.Zone.AllRRSets(ctx, zone)
109+
if err != nil {
110+
return nil, err
111+
}
112+
113+
var result []types.DNSRecord
114+
for _, rrset := range rrsets {
115+
var ttl int
116+
if rrset.TTL != nil {
117+
ttl = *rrset.TTL
118+
}
119+
for _, record := range rrset.Records {
120+
result = append(result, types.DNSRecord{
121+
ID: rrset.ID,
122+
Type: string(rrset.Type),
123+
Name: rrset.Name,
124+
Value: record.Value,
125+
TTL: ttl,
126+
})
127+
}
128+
}
129+
130+
return result, nil
131+
}

0 commit comments

Comments
 (0)