hcloud-upload-image/cmd/root.go
Julian Tölle e490b9a7f3
fix: timeout while waiting for SSH to become available (#92)
In #68 I reduced the general limits for the back off, thinking that it
would speed up the upload on average because it was retrying faster. But
because it was retrying faster, the 10 available retries were used up
before SSH became available.

The new 100 retries match the 3 minutes of total timeout that the
previous solution had, and should fix all issues.

In addition, I discovered that my implementation in
`hcloudimages/backoff.ExponentialBackoffWithLimit` has a bug where the
calculated offset could overflow before the limit was applied, resulting
in negative durations. I did not fix the issue because `hcloud-go`
provides such a method natively nowadays. Instead, I marked the method
as deprecated, to be removed in a later release.
2025-05-09 16:15:07 +02:00

118 lines
2.9 KiB
Go

package cmd
import (
"log/slog"
"os"
"time"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
"github.com/spf13/cobra"
"github.com/apricote/hcloud-upload-image/hcloudimages"
"github.com/apricote/hcloud-upload-image/hcloudimages/contextlogger"
"github.com/apricote/hcloud-upload-image/internal/ui"
"github.com/apricote/hcloud-upload-image/internal/version"
)
const (
flagVerbose = "verbose"
)
var (
// 1 activates slog debug output
// 2 activates hcloud-go debug output
verbose int
)
// The pre-authenticated client. Set in the root command PersistentPreRun
var client *hcloudimages.Client
// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "hcloud-upload-image",
Short: `Manage custom OS images on Hetzner Cloud.`,
Long: `Manage custom OS images on Hetzner Cloud.`,
SilenceUsage: true,
DisableAutoGenTag: true,
Version: version.Version,
PersistentPreRun: func(cmd *cobra.Command, _ []string) {
ctx := cmd.Context()
slog.SetDefault(initLogger())
// Add logger to command context
logger := slog.Default()
ctx = contextlogger.New(ctx, logger)
cmd.SetContext(ctx)
},
}
func initLogger() *slog.Logger {
logLevel := slog.LevelInfo
if verbose >= 1 {
logLevel = slog.LevelDebug
}
return slog.New(ui.NewHandler(os.Stdout, &ui.HandlerOptions{
Level: logLevel,
ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr {
// Remove attributes that are unnecessary for the cli context
if a.Key == "library" || a.Key == "method" {
return slog.Attr{}
}
return a
},
}))
}
func initClient(cmd *cobra.Command, _ []string) {
if client != nil {
// Only init if not set.
// Theoretically this is not safe against data races and should use [sync.Once], but :shrug:
return
}
ctx := cmd.Context()
logger := contextlogger.From(ctx)
// Build hcloud-go client
if os.Getenv("HCLOUD_TOKEN") == "" {
logger.ErrorContext(ctx, "You need to set the HCLOUD_TOKEN environment variable to your Hetzner Cloud API Token.")
os.Exit(1)
}
opts := []hcloud.ClientOption{
hcloud.WithToken(os.Getenv("HCLOUD_TOKEN")),
hcloud.WithApplication("hcloud-upload-image", version.Version),
hcloud.WithPollOpts(hcloud.PollOpts{BackoffFunc: hcloud.ExponentialBackoffWithOpts(hcloud.ExponentialBackoffOpts{Multiplier: 2, Base: 1 * time.Second, Cap: 30 * time.Second})}),
}
if os.Getenv("HCLOUD_DEBUG") != "" || verbose >= 2 {
opts = append(opts, hcloud.WithDebugWriter(os.Stderr))
}
client = hcloudimages.NewClient(hcloud.NewClient(opts...))
}
func Execute() {
err := RootCmd.Execute()
if err != nil {
os.Exit(1)
}
}
func init() {
RootCmd.SetErrPrefix("\033[1;31mError:")
RootCmd.PersistentFlags().CountVarP(&verbose, flagVerbose, "v", "verbose debug output, can be specified up to 2 times")
RootCmd.AddGroup(&cobra.Group{
ID: "primary",
Title: "Primary Commands:",
})
}