diff --git a/cmd/hcloud-image/cmd/cleanup.go b/cmd/cleanup.go similarity index 100% rename from cmd/hcloud-image/cmd/cleanup.go rename to cmd/cleanup.go diff --git a/cmd/hcloud-image/go.sum b/cmd/hcloud-image/go.sum deleted file mode 100644 index ba0f2a4..0000000 --- a/cmd/hcloud-image/go.sum +++ /dev/null @@ -1,26 +0,0 @@ -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/hetznercloud/hcloud-go/v2 v2.7.3-0.20240430130644-7bb1a7b9ae5f h1:c1ahn6OKXkSqwOfCoqyFrjVh14BEC9rD3ok0dehbCno= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= -github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/cmd/hcloud-image/cmd/list.go b/cmd/list.go similarity index 100% rename from cmd/hcloud-image/cmd/list.go rename to cmd/list.go diff --git a/cmd/hcloud-image/cmd/root.go b/cmd/root.go similarity index 81% rename from cmd/hcloud-image/cmd/root.go rename to cmd/root.go index 61537f2..8946e15 100644 --- a/cmd/hcloud-image/cmd/root.go +++ b/cmd/root.go @@ -10,13 +10,13 @@ import ( "github.com/hetznercloud/hcloud-go/v2/hcloud" "github.com/spf13/cobra" - hcloud_upload_image "github.com/apricote/hcloud-upload-image" - "github.com/apricote/hcloud-upload-image/contextlogger" - "github.com/apricote/hcloud-upload-image/control" + "github.com/apricote/hcloud-upload-image/hcloudimages" + "github.com/apricote/hcloud-upload-image/hcloudimages/backoff" + "github.com/apricote/hcloud-upload-image/hcloudimages/contextlogger" ) // The pre-authenticated client. Set in the root command PersistentPreRun -var client hcloud_upload_image.SnapshotClient +var client hcloudimages.Client // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ @@ -44,7 +44,7 @@ to quickly create a Cobra application.`, }, } -func newClient(ctx context.Context) hcloud_upload_image.SnapshotClient { +func newClient(ctx context.Context) hcloudimages.Client { logger := contextlogger.From(ctx) // Build hcloud-go client if os.Getenv("HCLOUD_TOKEN") == "" { @@ -54,11 +54,11 @@ func newClient(ctx context.Context) hcloud_upload_image.SnapshotClient { hcloudClient := hcloud.NewClient( hcloud.WithToken(os.Getenv("HCLOUD_TOKEN")), hcloud.WithApplication("hcloud-image", ""), - hcloud.WithPollBackoffFunc(control.ExponentialBackoffWithLimit(2, 1*time.Second, 30*time.Second)), + hcloud.WithPollBackoffFunc(backoff.ExponentialBackoffWithLimit(2, 1*time.Second, 30*time.Second)), // hcloud.WithDebugWriter(os.Stderr), ) - return hcloud_upload_image.New(hcloudClient) + return hcloudimages.New(hcloudClient) } func Execute() { diff --git a/cmd/hcloud-image/cmd/upload.go b/cmd/upload.go similarity index 95% rename from cmd/hcloud-image/cmd/upload.go rename to cmd/upload.go index f34fc1b..629f77d 100644 --- a/cmd/hcloud-image/cmd/upload.go +++ b/cmd/upload.go @@ -7,8 +7,8 @@ import ( "github.com/hetznercloud/hcloud-go/v2/hcloud" "github.com/spf13/cobra" - hcloud_upload_image "github.com/apricote/hcloud-upload-image" - "github.com/apricote/hcloud-upload-image/contextlogger" + hcloud_upload_image "github.com/apricote/hcloud-upload-image/hcloudimages" + "github.com/apricote/hcloud-upload-image/hcloudimages/contextlogger" ) const ( diff --git a/control/retry.go b/control/retry.go deleted file mode 100644 index 61eb4dd..0000000 --- a/control/retry.go +++ /dev/null @@ -1,57 +0,0 @@ -package control - -import ( - "context" - "math" - "time" - - "github.com/hetznercloud/hcloud-go/v2/hcloud" - - "github.com/apricote/hcloud-upload-image/contextlogger" -) - -// From https://github.com/hetznercloud/terraform-provider-hcloud/blob/v1.46.1/internal/control/retry.go - -// ExponentialBackoffWithLimit returns a [hcloud.BackoffFunc] which implements an exponential -// backoff. -// It uses the formula: -// -// min(b^retries * d, limit) -func ExponentialBackoffWithLimit(b float64, d time.Duration, limit time.Duration) hcloud.BackoffFunc { - return func(retries int) time.Duration { - current := time.Duration(math.Pow(b, float64(retries))) * d - - if current > limit { - return limit - } else { - return current - } - } -} - -// Retry executes f at most maxTries times. -func Retry(ctx context.Context, maxTries int, f func() error) error { - logger := contextlogger.From(ctx) - - var err error - - backoff := ExponentialBackoffWithLimit(2, 1*time.Second, 30*time.Second) - - for try := 0; try < maxTries; try++ { - if ctx.Err() != nil { - return ctx.Err() - } - - err = f() - if err != nil { - sleep := backoff(try) - logger.DebugContext(ctx, "operation failed, waiting before trying again", "try", try, "backoff", sleep) - time.Sleep(sleep) - continue - } - - return nil - } - - return err -} diff --git a/go.mod b/go.mod index f8b2496..e80eb95 100644 --- a/go.mod +++ b/go.mod @@ -4,22 +4,20 @@ go 1.22.2 require ( github.com/hetznercloud/hcloud-go/v2 v2.7.3-0.20240430130644-7bb1a7b9ae5f - github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.22.0 + github.com/spf13/cobra v1.8.0 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/prometheus/client_golang v1.19.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect + github.com/spf13/pflag v1.0.5 // indirect golang.org/x/net v0.24.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.32.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 938dc0d..ba0f2a4 100644 --- a/go.sum +++ b/go.sum @@ -1,45 +1,26 @@ 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.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= 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= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hetznercloud/hcloud-go/v2 v2.7.3-0.20240430130644-7bb1a7b9ae5f h1:c1ahn6OKXkSqwOfCoqyFrjVh14BEC9rD3ok0dehbCno= -github.com/hetznercloud/hcloud-go/v2 v2.7.3-0.20240430130644-7bb1a7b9ae5f/go.mod h1:jvpP3qAWMIZ3WQwQLYa97ia6t98iPCgsJNwRts+Jnrk= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 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/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= -github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= -github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= +github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= +github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= +github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= -github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= -golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= -golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= -golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= -golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= -golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go.work b/go.work index f8e580f..1b4ed3c 100644 --- a/go.work +++ b/go.work @@ -2,5 +2,5 @@ go 1.22.2 use ( . - ./cmd/hcloud-image + ./hcloudimages ) diff --git a/hcloudimages/backoff/backoff.go b/hcloudimages/backoff/backoff.go new file mode 100644 index 0000000..9f73d91 --- /dev/null +++ b/hcloudimages/backoff/backoff.go @@ -0,0 +1,28 @@ +package backoff + +import ( + "math" + "time" + + "github.com/hetznercloud/hcloud-go/v2/hcloud" +) + +// From https://github.com/hetznercloud/terraform-provider-hcloud/blob/v1.46.1/internal/control/retry.go +// Copyright (c) Hetzner Cloud GmbH + +// ExponentialBackoffWithLimit returns a [hcloud.BackoffFunc] which implements an exponential +// backoff. +// It uses the formula: +// +// min(b^retries * d, limit) +func ExponentialBackoffWithLimit(b float64, d time.Duration, limit time.Duration) hcloud.BackoffFunc { + return func(retries int) time.Duration { + current := time.Duration(math.Pow(b, float64(retries))) * d + + if current > limit { + return limit + } else { + return current + } + } +} diff --git a/contextlogger/context.go b/hcloudimages/contextlogger/context.go similarity index 100% rename from contextlogger/context.go rename to hcloudimages/contextlogger/context.go diff --git a/contextlogger/discard.go b/hcloudimages/contextlogger/discard.go similarity index 100% rename from contextlogger/discard.go rename to hcloudimages/contextlogger/discard.go diff --git a/cmd/hcloud-image/go.mod b/hcloudimages/go.mod similarity index 69% rename from cmd/hcloud-image/go.mod rename to hcloudimages/go.mod index 54b0261..4a8dbb4 100644 --- a/cmd/hcloud-image/go.mod +++ b/hcloudimages/go.mod @@ -1,23 +1,25 @@ -module github.com/apricote/hcloud-upload-image/cmd/hcloud-image +module github.com/apricote/hcloud-upload-image/hcloudimages go 1.22.2 require ( github.com/hetznercloud/hcloud-go/v2 v2.7.3-0.20240430130644-7bb1a7b9ae5f - github.com/spf13/cobra v1.8.0 + github.com/stretchr/testify v1.9.0 + golang.org/x/crypto v0.22.0 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.19.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/common v0.48.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect golang.org/x/net v0.24.0 // indirect golang.org/x/sys v0.19.0 // indirect golang.org/x/text v0.14.0 // indirect google.golang.org/protobuf v1.32.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/hcloudimages/go.sum b/hcloudimages/go.sum new file mode 100644 index 0000000..938dc0d --- /dev/null +++ b/hcloudimages/go.sum @@ -0,0 +1,45 @@ +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.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= +github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +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= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/hetznercloud/hcloud-go/v2 v2.7.3-0.20240430130644-7bb1a7b9ae5f h1:c1ahn6OKXkSqwOfCoqyFrjVh14BEC9rD3ok0dehbCno= +github.com/hetznercloud/hcloud-go/v2 v2.7.3-0.20240430130644-7bb1a7b9ae5f/go.mod h1:jvpP3qAWMIZ3WQwQLYa97ia6t98iPCgsJNwRts+Jnrk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU= +github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k= +github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= +github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= +github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= +github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= +github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= +github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= +github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= +github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30= +golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M= +golang.org/x/net v0.24.0 h1:1PcaxkF854Fu3+lvBIx5SYn9wRlBzzcnHZSiaFFAb0w= +golang.org/x/net v0.24.0/go.mod h1:2Q7sJY5mzlzWjKtYUEXSlBWCdyaioyXzRB2RtU8KVE8= +golang.org/x/sys v0.19.0 h1:q5f1RH2jigJ1MoAWp2KTp3gm5zAGFUTarQZ5U386+4o= +golang.org/x/sys v0.19.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.19.0 h1:+ThwsDv+tYfnJFhF4L8jITxu1tdTWRTZpdsWgEgjL6Q= +golang.org/x/term v0.19.0/go.mod h1:2CuTdWZ7KHSQwUzKva0cbMg6q2DMI3Mmxp+gKJbskEk= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= +google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/interface.go b/hcloudimages/interface.go similarity index 96% rename from interface.go rename to hcloudimages/interface.go index 9479315..60bfe0d 100644 --- a/interface.go +++ b/hcloudimages/interface.go @@ -1,4 +1,4 @@ -package hcloud_upload_image +package hcloudimages import ( "context" @@ -7,7 +7,7 @@ import ( "github.com/hetznercloud/hcloud-go/v2/hcloud" ) -type SnapshotClient interface { +type Client interface { // Upload the specified image into a snapshot on Hetzner Cloud. // // As the Hetzner Cloud API has no direct way to upload images, we create a temporary server, diff --git a/hcloudimages/internal/control/retry.go b/hcloudimages/internal/control/retry.go new file mode 100644 index 0000000..a63f0ad --- /dev/null +++ b/hcloudimages/internal/control/retry.go @@ -0,0 +1,39 @@ +package control + +import ( + "context" + "time" + + "github.com/apricote/hcloud-upload-image/hcloudimages/backoff" + "github.com/apricote/hcloud-upload-image/hcloudimages/contextlogger" +) + +// From https://github.com/hetznercloud/terraform-provider-hcloud/blob/v1.46.1/internal/control/retry.go +// Copyright (c) Hetzner Cloud GmbH + +// Retry executes f at most maxTries times. +func Retry(ctx context.Context, maxTries int, f func() error) error { + logger := contextlogger.From(ctx) + + var err error + + backoffFunc := backoff.ExponentialBackoffWithLimit(2, 1*time.Second, 30*time.Second) + + for try := 0; try < maxTries; try++ { + if ctx.Err() != nil { + return ctx.Err() + } + + err = f() + if err != nil { + sleep := backoffFunc(try) + logger.DebugContext(ctx, "operation failed, waiting before trying again", "try", try, "backoff", sleep) + time.Sleep(sleep) + continue + } + + return nil + } + + return err +} diff --git a/randomid/randomid.go b/hcloudimages/internal/randomid/randomid.go similarity index 90% rename from randomid/randomid.go rename to hcloudimages/internal/randomid/randomid.go index 5f19cd9..bdc42c5 100644 --- a/randomid/randomid.go +++ b/hcloudimages/internal/randomid/randomid.go @@ -7,6 +7,7 @@ import ( ) // From https://gitlab.com/hetznercloud/fleeting-plugin-hetzner/-/blob/0f60204582289c243599f8ca0f5be4822789131d/internal/utils/random.go +// Copyright (c) 2024 Hetzner Cloud GmbH func Generate() (string, error) { b := make([]byte, 4) diff --git a/randomid/randomid_test.go b/hcloudimages/internal/randomid/randomid_test.go similarity index 100% rename from randomid/randomid_test.go rename to hcloudimages/internal/randomid/randomid_test.go diff --git a/sshkey/ssh_key.go b/hcloudimages/internal/sshkey/ssh_key.go similarity index 95% rename from sshkey/ssh_key.go rename to hcloudimages/internal/sshkey/ssh_key.go index 45c1de1..eea0e02 100644 --- a/sshkey/ssh_key.go +++ b/hcloudimages/internal/sshkey/ssh_key.go @@ -8,6 +8,7 @@ import ( ) // From https://gitlab.com/hetznercloud/fleeting-plugin-hetzner/-/blob/0f60204582289c243599f8ca0f5be4822789131d/internal/utils/ssh_key.go +// Copyright (c) 2024 Hetzner Cloud GmbH func GenerateKeyPair() ([]byte, []byte, error) { pub, priv, err := ed25519.GenerateKey(nil) diff --git a/sshkey/ssh_key_test.go b/hcloudimages/internal/sshkey/ssh_key_test.go similarity index 100% rename from sshkey/ssh_key_test.go rename to hcloudimages/internal/sshkey/ssh_key_test.go diff --git a/sshsession/session.go b/hcloudimages/internal/sshsession/session.go similarity index 100% rename from sshsession/session.go rename to hcloudimages/internal/sshsession/session.go diff --git a/snapshot.go b/hcloudimages/snapshot.go similarity index 85% rename from snapshot.go rename to hcloudimages/snapshot.go index 6ff7970..8bbea12 100644 --- a/snapshot.go +++ b/hcloudimages/snapshot.go @@ -1,4 +1,4 @@ -package hcloud_upload_image +package hcloudimages import ( "context" @@ -8,11 +8,11 @@ import ( "github.com/hetznercloud/hcloud-go/v2/hcloud" "golang.org/x/crypto/ssh" - "github.com/apricote/hcloud-upload-image/contextlogger" - "github.com/apricote/hcloud-upload-image/control" - "github.com/apricote/hcloud-upload-image/randomid" - "github.com/apricote/hcloud-upload-image/sshkey" - "github.com/apricote/hcloud-upload-image/sshsession" + "github.com/apricote/hcloud-upload-image/hcloudimages/contextlogger" + "github.com/apricote/hcloud-upload-image/hcloudimages/internal/control" + "github.com/apricote/hcloud-upload-image/hcloudimages/internal/randomid" + "github.com/apricote/hcloud-upload-image/hcloudimages/internal/sshkey" + "github.com/apricote/hcloud-upload-image/hcloudimages/internal/sshsession" ) const ( @@ -39,17 +39,17 @@ var ( defaultSSHDialTimeout = 1 * time.Minute ) -func New(client *hcloud.Client) SnapshotClient { - return &snapshotClient{ - client: client, +func New(c *hcloud.Client) Client { + return &client{ + c: c, } } -type snapshotClient struct { - client *hcloud.Client +type client struct { + c *hcloud.Client } -func (s snapshotClient) Upload(ctx context.Context, options UploadOptions) (*hcloud.Image, error) { +func (s client) Upload(ctx context.Context, options UploadOptions) (*hcloud.Image, error) { logger := contextlogger.From(ctx).With( "library", "hcloud-upload-image", "method", "upload", @@ -70,7 +70,7 @@ func (s snapshotClient) Upload(ctx context.Context, options UploadOptions) (*hcl return nil, fmt.Errorf("failed to generate temporary ssh key pair: %w", err) } - key, _, err := s.client.SSHKey.Create(ctx, hcloud.SSHKeyCreateOpts{ + key, _, err := s.c.SSHKey.Create(ctx, hcloud.SSHKeyCreateOpts{ Name: resourceName, PublicKey: string(publicKey), Labels: fullLabels(options.Labels), @@ -88,7 +88,7 @@ func (s snapshotClient) Upload(ctx context.Context, options UploadOptions) (*hcl logger.InfoContext(ctx, "Cleanup: Deleting temporary ssh key") - _, err := s.client.SSHKey.Delete(ctx, key) + _, err := s.c.SSHKey.Delete(ctx, key) if err != nil { logger.WarnContext(ctx, "Cleanup: ssh key could not be deleted", "error", err) // TODO @@ -107,7 +107,7 @@ func (s snapshotClient) Upload(ctx context.Context, options UploadOptions) (*hcl "location", defaultLocation.Name, "serverType", serverType.Name, ) - serverCreateResult, _, err := s.client.Server.Create(ctx, hcloud.ServerCreateOpts{ + serverCreateResult, _, err := s.c.Server.Create(ctx, hcloud.ServerCreateOpts{ Name: resourceName, ServerType: serverType, @@ -128,7 +128,7 @@ func (s snapshotClient) Upload(ctx context.Context, options UploadOptions) (*hcl logger.DebugContext(ctx, "Created Server") logger.DebugContext(ctx, "waiting on actions") - err = s.client.Action.WaitFor(ctx, append(serverCreateResult.NextActions, serverCreateResult.Action)...) + err = s.c.Action.WaitFor(ctx, append(serverCreateResult.NextActions, serverCreateResult.Action)...) if err != nil { return nil, fmt.Errorf("creating the temporary server failed: %w", err) } @@ -144,7 +144,7 @@ func (s snapshotClient) Upload(ctx context.Context, options UploadOptions) (*hcl logger.InfoContext(ctx, "Cleanup: Deleting temporary server") - _, _, err := s.client.Server.DeleteWithResult(ctx, server) + _, _, err := s.c.Server.DeleteWithResult(ctx, server) if err != nil { logger.WarnContext(ctx, "Cleanup: server could not be deleted", "error", err) } @@ -152,7 +152,7 @@ func (s snapshotClient) Upload(ctx context.Context, options UploadOptions) (*hcl // 3. Activate Rescue System logger.InfoContext(ctx, "# Step 4: Activating Rescue System") - enableRescueResult, _, err := s.client.Server.EnableRescue(ctx, server, hcloud.ServerEnableRescueOpts{ + enableRescueResult, _, err := s.c.Server.EnableRescue(ctx, server, hcloud.ServerEnableRescueOpts{ Type: defaultRescueType, SSHKeys: []*hcloud.SSHKey{key}, }) @@ -162,7 +162,7 @@ func (s snapshotClient) Upload(ctx context.Context, options UploadOptions) (*hcl logger.DebugContext(ctx, "rescue system requested, waiting on action") - err = s.client.Action.WaitFor(ctx, enableRescueResult.Action) + err = s.c.Action.WaitFor(ctx, enableRescueResult.Action) if err != nil { return nil, fmt.Errorf("enabling the rescue system on the temporary server failed: %w", err) } @@ -170,14 +170,14 @@ func (s snapshotClient) Upload(ctx context.Context, options UploadOptions) (*hcl // 4. Boot Server logger.InfoContext(ctx, "# Step 4: Booting Server") - powerOnAction, _, err := s.client.Server.Poweron(ctx, server) + powerOnAction, _, err := s.c.Server.Poweron(ctx, server) if err != nil { return nil, fmt.Errorf("starting the temporary server failed: %w", err) } logger.DebugContext(ctx, "boot requested, waiting on action") - err = s.client.Action.WaitFor(ctx, powerOnAction) + err = s.c.Action.WaitFor(ctx, powerOnAction) if err != nil { return nil, fmt.Errorf("starting the temporary server failed: %w", err) } @@ -250,7 +250,7 @@ func (s snapshotClient) Upload(ctx context.Context, options UploadOptions) (*hcl // 8. Create Image from Server logger.InfoContext(ctx, "# Step 8: Creating Image") - createImageResult, _, err := s.client.Server.CreateImage(ctx, server, &hcloud.ServerCreateImageOpts{ + createImageResult, _, err := s.c.Server.CreateImage(ctx, server, &hcloud.ServerCreateImageOpts{ Type: hcloud.ImageTypeSnapshot, Description: options.Description, Labels: fullLabels(options.Labels), @@ -260,7 +260,7 @@ func (s snapshotClient) Upload(ctx context.Context, options UploadOptions) (*hcl } logger.DebugContext(ctx, "image creation requested, waiting on action") - err = s.client.Action.WaitFor(ctx, createImageResult.Action) + err = s.c.Action.WaitFor(ctx, createImageResult.Action) if err != nil { return nil, fmt.Errorf("failed to create snapshot: %w", err) } diff --git a/cmd/hcloud-image/ui/slog_handler.go b/internal/ui/slog_handler.go similarity index 100% rename from cmd/hcloud-image/ui/slog_handler.go rename to internal/ui/slog_handler.go diff --git a/cmd/hcloud-image/main.go b/main.go similarity index 77% rename from cmd/hcloud-image/main.go rename to main.go index b8e7011..671f489 100644 --- a/cmd/hcloud-image/main.go +++ b/main.go @@ -5,7 +5,7 @@ import ( "os" "github.com/apricote/hcloud-upload-image/cmd/hcloud-image/cmd" - "github.com/apricote/hcloud-upload-image/cmd/hcloud-image/ui" + "github.com/apricote/hcloud-upload-image/cmd/hcloud-image/internal/ui" ) func init() {