diff --git a/.github/actions/setup-mdbook/action.yaml b/.github/actions/setup-mdbook/action.yaml new file mode 100644 index 0000000..23e0665 --- /dev/null +++ b/.github/actions/setup-mdbook/action.yaml @@ -0,0 +1,16 @@ +name: "Setup mdbook" +inputs: + version: + description: "mdbook version" + +runs: + using: composite + steps: + - name: Setup mdbook + shell: bash + env: + url: https://github.com/rust-lang/mdbook/releases/download/${{ inputs.version }}/mdbook-${{ inputs.version }}-x86_64-unknown-linux-gnu.tar.gz + run: | + mkdir mdbook + curl -sSL "$url" | tar -xz --directory=./mdbook + echo `pwd`/mdbook >> $GITHUB_PATH diff --git a/.github/release-please-config.json b/.github/release-please-config.json index 965c8a1..393f496 100644 --- a/.github/release-please-config.json +++ b/.github/release-please-config.json @@ -2,11 +2,19 @@ "$schema": "https://raw.githubusercontent.com/googleapis/release-please/main/schemas/config.json", "include-component-in-tag": false, "include-v-in-tag": true, + "release-type": "go", + "separate-pull-requests": true, "packages": { ".": { - "release-type": "go", + "component": "cli", "package-name": "hcloud-upload-image", "extra-files": ["internal/version/version.go"] + }, + "hcloudimages": { + "component": "hcloudimages", + "package-name": "hcloudimages", + "include-component-in-tag": true, + "tag-separator": "/" } } } diff --git a/.github/release-please-manifest.json b/.github/release-please-manifest.json index 305c150..0271bbd 100644 --- a/.github/release-please-manifest.json +++ b/.github/release-please-manifest.json @@ -1 +1 @@ -{".":"0.2.0"} +{".":"1.3.0","hcloudimages":"1.3.0"} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index e0191ca..2feae24 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,50 +10,48 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Run golangci-lint (CLI) - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v9 with: - version: v1.58.1 # renovate: datasource=github-releases depName=golangci/golangci-lint + version: v2.7.2 # renovate: datasource=github-releases depName=golangci/golangci-lint args: --timeout 5m - name: Run golangci-lint (Lib) - uses: golangci/golangci-lint-action@v6 + uses: golangci/golangci-lint-action@v9 with: - version: v1.58.1 # renovate: datasource=github-releases depName=golangci/golangci-lint + version: v2.7.2 # renovate: datasource=github-releases depName=golangci/golangci-lint args: --timeout 5m working-directory: hcloudimages - test: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Run tests run: go test -v -race -coverpkg=./...,./hcloudimages/... ./... ./hcloudimages/... - go-mod-tidy: runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: go.mod @@ -65,3 +63,23 @@ jobs: - if: failure() run: echo "::error::Check failed, please run 'go mod tidy' and commit the changes." + + cli-help-pages: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v6 + + - name: Set up Go + uses: actions/setup-go@v6 + with: + go-version-file: go.mod + + - name: Generate CLI help pages + run: go run ./scripts/cli-help-pages.go + + - name: Check uncommitted changes + run: git diff --exit-code + + - if: failure() + run: echo "::error::Check failed, please run 'go run ./scripts/cli-help-pages.go' and commit the changes." diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 0000000..aaf099d --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,39 @@ +name: docs +on: + push: + branches: + - main + +jobs: + deploy: + runs-on: ubuntu-latest + permissions: + contents: write # To push a branch + pages: write # To push to a GitHub Pages site + id-token: write # To update the deployment status + + steps: + - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6 + with: + lfs: "true" + + - uses: ./.github/actions/setup-mdbook + with: + version: v0.5.2 # renovate: datasource=github-releases depName=rust-lang/mdbook + + - name: Build Book + working-directory: docs + run: mdbook build + + - name: Setup Pages + uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5 + + - name: Upload artifact + uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4 + with: + # Upload entire repository + path: "docs/book" + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4 diff --git a/.github/workflows/release-please.yaml b/.github/workflows/release-please.yaml index 53838a9..666e83f 100644 --- a/.github/workflows/release-please.yaml +++ b/.github/workflows/release-please.yaml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: google-github-actions/release-please-action@v4 + - uses: googleapis/release-please-action@v4 with: token: ${{ secrets.RELEASE_GH_TOKEN }} config-file: .github/release-please-config.json diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 2a3fc50..ea4e440 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -7,20 +7,31 @@ on: jobs: release: + permissions: + contents: read + packages: write + runs-on: ubuntu-latest steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@v6 + + - name: Log in to the Container registry + uses: docker/login-action@6862ffc5ab2cdb4405cf318a62a6f4c066e2298b + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@v6 with: go-version-file: go.mod - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v5 + uses: goreleaser/goreleaser-action@v6 with: - version: latest + version: "~> v2" args: release --clean env: GITHUB_TOKEN: ${{ secrets.RELEASE_GH_TOKEN }} diff --git a/.goreleaser.yaml b/.goreleaser.yaml index ac3b3c2..474362f 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -1,13 +1,14 @@ # yaml-language-server: $schema=https://goreleaser.com/static/schema.json -version: 1 +version: 2 before: hooks: - ./scripts/completions.sh builds: - - env: + - id: default + env: - CGO_ENABLED=0 goos: - linux @@ -21,7 +22,7 @@ builds: - -X {{ .ModulePath }}/internal/version.versionPrerelease= archives: - - format: tar.gz + - formats: [ "tar.gz" ] # this name template makes the OS and Arch compatible with the results of `uname`. name_template: >- {{ .ProjectName }}_ @@ -33,7 +34,7 @@ archives: # use zip for windows archives format_overrides: - goos: windows - format: zip + formats: [ "zip" ] files: - README.md @@ -104,9 +105,23 @@ aurs: install -Dm644 "./completions/hcloud-upload-image.zsh" "${pkgdir}/usr/share/zsh/site-functions/_hcloud-upload-image" install -Dm644 "./completions/hcloud-upload-image.fish" "${pkgdir}/usr/share/fish/vendor_completions.d/hcloud-upload-image.fish" +kos: + - id: container-images + build: default + repositories: + - ghcr.io/apricote + platforms: + - linux/amd64 + - linux/arm64 + base_import_paths: true + labels: + org.opencontainers.image.source: https://github.com/apricote/hcloud-upload-image + tags: + - latest + - "{{.Tag}}" snapshot: - name_template: "{{ .Version }}-dev+{{ .ShortCommit }}" + version_template: "{{ .Version }}-dev+{{ .ShortCommit }}" changelog: # Generated by release-please diff --git a/CHANGELOG.md b/CHANGELOG.md index d636153..e87b3b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,77 @@ # Changelog +## [1.3.0](https://github.com/apricote/hcloud-upload-image/compare/v1.2.0...v1.3.0) (2025-12-22) + + +### Features + +* add --location flag to specify datacenter region ([#141](https://github.com/apricote/hcloud-upload-image/issues/141)) ([fcbc14a](https://github.com/apricote/hcloud-upload-image/commit/fcbc14aab6d495d2c67d653f9ea1ff56a39a8c2f)), closes [#142](https://github.com/apricote/hcloud-upload-image/issues/142) + +## [1.2.0](https://github.com/apricote/hcloud-upload-image/compare/v1.1.0...v1.2.0) (2025-11-06) + + +### Features + +* change minimum required Go version to 1.24 ([#130](https://github.com/apricote/hcloud-upload-image/issues/130)) ([5eba2d5](https://github.com/apricote/hcloud-upload-image/commit/5eba2d52fe3aafb4fd0d93403548f4c32bc2b5ac)) +* support zstd compression ([#125](https://github.com/apricote/hcloud-upload-image/issues/125)) ([37ebbce](https://github.com/apricote/hcloud-upload-image/commit/37ebbce5179997ac216af274055fc34c777b01e6)), closes [#122](https://github.com/apricote/hcloud-upload-image/issues/122) +* update default x86 server type to cx23 ([#129](https://github.com/apricote/hcloud-upload-image/issues/129)) ([a205619](https://github.com/apricote/hcloud-upload-image/commit/a20561944d0ba9485a6e10e99df15c56a688541d)) + +## [1.1.0](https://github.com/apricote/hcloud-upload-image/compare/v1.0.1...v1.1.0) (2025-05-10) + + +### Features + +* smaller snapshots by zeroing disk first ([#101](https://github.com/apricote/hcloud-upload-image/issues/101)) ([fdfb284](https://github.com/apricote/hcloud-upload-image/commit/fdfb284533d3154806b0936c08015fd5cc64b0fb)), closes [#96](https://github.com/apricote/hcloud-upload-image/issues/96) + + +### Bug Fixes + +* upload from local image generates broken command ([#98](https://github.com/apricote/hcloud-upload-image/issues/98)) ([420dcf9](https://github.com/apricote/hcloud-upload-image/commit/420dcf94c965ee470602db6c9c23c777fda91222)), closes [#97](https://github.com/apricote/hcloud-upload-image/issues/97) + +## [1.0.1](https://github.com/apricote/hcloud-upload-image/compare/v1.0.0...v1.0.1) (2025-05-09) + + +### Bug Fixes + +* timeout while waiting for SSH to become available ([#92](https://github.com/apricote/hcloud-upload-image/issues/92)) ([e490b9a](https://github.com/apricote/hcloud-upload-image/commit/e490b9a7f394e268fa1946ca51aa998c78c3d46a)) + +## [1.0.0](https://github.com/apricote/hcloud-upload-image/compare/v0.3.1...v1.0.0) (2025-05-04) + + +### Features + +* **deps:** require Go 1.23 ([#70](https://github.com/apricote/hcloud-upload-image/issues/70)) ([f3fcb62](https://github.com/apricote/hcloud-upload-image/commit/f3fcb623fc00095ab3806fa41dbcb7083c13c5df)) +* docs website ([#80](https://github.com/apricote/hcloud-upload-image/issues/80)) ([d144b85](https://github.com/apricote/hcloud-upload-image/commit/d144b85e3dfd933e8fbb09a0e6f5acacb4d05bea)) +* publish container image ([#82](https://github.com/apricote/hcloud-upload-image/issues/82)) ([91df729](https://github.com/apricote/hcloud-upload-image/commit/91df729f1cfd636355fc8338f47aefa4ab8b3b84)) +* upload qcow2 images ([#69](https://github.com/apricote/hcloud-upload-image/issues/69)) ([ac3e9dd](https://github.com/apricote/hcloud-upload-image/commit/ac3e9dd7ecd86d1538b6401c3073c7c078c40847)) + +## [0.3.1](https://github.com/apricote/hcloud-upload-image/compare/v0.3.0...v0.3.1) (2024-12-07) + + +### Bug Fixes + +* **cli:** local install fails because of go.mod replace ([#47](https://github.com/apricote/hcloud-upload-image/issues/47)) ([66dc5f7](https://github.com/apricote/hcloud-upload-image/commit/66dc5f70b604ed3ee964576d74f94bdcea710c95)) + +## [0.3.0](https://github.com/apricote/hcloud-upload-image/compare/v0.2.1...v0.3.0) (2024-06-23) + + +### Features + +* set server type explicitly ([#36](https://github.com/apricote/hcloud-upload-image/issues/36)) ([42eeb00](https://github.com/apricote/hcloud-upload-image/commit/42eeb00a0784e13a00a52cf15a8659b497d78d72)), closes [#30](https://github.com/apricote/hcloud-upload-image/issues/30) +* update default x86 server type to cx22 ([#38](https://github.com/apricote/hcloud-upload-image/issues/38)) ([ebe08b3](https://github.com/apricote/hcloud-upload-image/commit/ebe08b345c8f31df73087b091fa39f5fdc195156)) + + +### Bug Fixes + +* error early when the image write fails ([#34](https://github.com/apricote/hcloud-upload-image/issues/34)) ([256989f](https://github.com/apricote/hcloud-upload-image/commit/256989f4a37e7b124c0684aab0f34cf5e09559be)), closes [#33](https://github.com/apricote/hcloud-upload-image/issues/33) + +## [0.2.1](https://github.com/apricote/hcloud-upload-image/compare/v0.2.0...v0.2.1) (2024-05-10) + + +### Bug Fixes + +* **cli:** completion requires HCLOUD_TOKEN ([#19](https://github.com/apricote/hcloud-upload-image/issues/19)) ([bb2ca48](https://github.com/apricote/hcloud-upload-image/commit/bb2ca482000f5c780545edb9a03aa9f6bf93d906)) + ## [0.2.0](https://github.com/apricote/hcloud-upload-image/compare/v0.1.1...v0.2.0) (2024-05-09) diff --git a/README.md b/README.md index 26b998d..04a2999 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,34 @@ -# `hcloud-upload-image` +# hcloud-upload-image + +

+ Quickly upload any raw disk images into your Hetzner Cloud projects! +

+ +

+ Badge: Documentation + Badge: Stable Release + Badge: License MIT +

-Quickly upload any raw disk images into your [Hetzner Cloud](https://hetzner.com/cloud) projects! ## About -The [Hetzner Cloud API](https://docs.hetzner.cloud/) does not support uploading disk images directly, and it only -provides a limited set of default images. The only option for custom disk images that users have is by taking a -"snapshot" of an existing servers root disk. These can then be used to create new servers. +The [Hetzner Cloud API](https://docs.hetzner.cloud/) does not support uploading disk images directly and only provides a limited set of default images. The only option for custom disk images is to take a snapshot of an existing server’s root disk. These snapshots can then be used to create new servers. -To create a completely custom disk image, users have to follow these steps: +To create a completely custom disk image, users need to follow these steps: -1. Create server with the correct server type -2. Enable rescue system for the server +1. Create a server with the correct server type +2. Enable the rescue system for the server 3. Boot the server 4. Download the disk image from within the rescue system -5. Write disk image to servers root disk +5. Write the disk image to the server’s root disk 6. Shut down the server -7. Take a snapshot of the servers root disk +7. Take a snapshot of the server’s root disk 8. Delete the server -This is an annoyingly long process. Many users have automated this with [Packer](https://www.packer.io/) & -[`packer-plugin-hcloud`](https://github.com/hetznercloud/packer-plugin-hcloud/) before, but Packer offers a lot of -additional complexity to wrap your head around. +This is a frustratingly long process. Many users have automated it with [Packer](https://www.packer.io/) and [`packer-plugin-hcloud`](https://github.com/hetznercloud/packer-plugin-hcloud/), but Packer introduces additional complexity that can be difficult to manage. -This repository provides a simple CLI tool & Go library to do the above. +This repository provides a simple CLI tool and Go library to streamline the process. ## Getting Started @@ -31,14 +36,34 @@ This repository provides a simple CLI tool & Go library to do the above. #### Binary -> TODO +We provide pre-built `deb`, `rpm` and `apk` packages. Alternatively we also provide the binaries directly. + +Check out the [GitHub release artifacts](https://github.com/apricote/hcloud-upload-image/releases/latest) for all of these files and archives. + +#### Arch Linux + +You can get [`hcloud-upload-image-bin`](https://aur.archlinux.org/packages/hcloud-upload-image-bin) from the AUR. + +Use your preferred wrapper to install: + +```shell +yay -S hcloud-upload-image-bin +``` #### `go install` If you already have a recent Go toolchain installed, you can build & install the binary from source: ```shell -go install github.com/apricote/hcloud-upload-image +go install github.com/apricote/hcloud-upload-image@latest +``` + +#### Docker + +There is a docker image published at `ghcr.io/apricote/hcloud-upload-image`. + +```shell +docker run --rm -e HCLOUD_TOKEN="" ghcr.io/apricote/hcloud-upload-image:latest ``` #### Usage @@ -48,10 +73,11 @@ export HCLOUD_TOKEN="" hcloud-upload-image upload \ --image-url "https://example.com/disk-image-x86.raw.bz2" \ --architecture x86 \ - --compression bz2 + --compression bz2 \ + --location nbg1 # Optional: defaults to fsn1 ``` -To learn more, you can use the embedded help output: +To learn more, you can use the embedded help output or check out the [CLI help pages in this repository](docs/reference/cli/hcloud-upload-image.md).: ```shell hcloud-upload-image --help @@ -98,6 +124,7 @@ func main() { ImageURL: imageURL, ImageCompression: hcloudimages.CompressionBZ2, Architecture: hcloud.ArchitectureX86, + Location: &hcloud.Location{Name: "nbg1"}, // Optional: defaults to fsn1 }) if err != nil { panic(err) diff --git a/cmd/cleanup.go b/cmd/cleanup.go index c726267..a41e2e1 100644 --- a/cmd/cleanup.go +++ b/cmd/cleanup.go @@ -18,12 +18,17 @@ that match the label "apricote.de/created-by=hcloud-upload-image". If you want to see a preview of what would be removed, you can use the official hcloud CLI and run: -$ hcloud server list -l apricote.de/created-by=hcloud-upload-image -$ hcloud ssh-key list -l apricote.de/created-by=hcloud-upload-image + $ hcloud server list -l apricote.de/created-by=hcloud-upload-image + $ hcloud ssh-key list -l apricote.de/created-by=hcloud-upload-image This command does not handle any parallel executions of hcloud-upload-image and will remove in-use resources if called at the same time.`, + DisableAutoGenTag: true, + GroupID: "primary", + + PreRun: initClient, + RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() logger := contextlogger.From(ctx) @@ -40,5 +45,5 @@ and will remove in-use resources if called at the same time.`, } func init() { - rootCmd.AddCommand(cleanupCmd) + RootCmd.AddCommand(cleanupCmd) } diff --git a/cmd/root.go b/cmd/root.go index 3d0b247..98a25d3 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -1,7 +1,6 @@ package cmd import ( - "context" "log/slog" "os" "time" @@ -10,7 +9,6 @@ import ( "github.com/spf13/cobra" "github.com/apricote/hcloud-upload-image/hcloudimages" - "github.com/apricote/hcloud-upload-image/hcloudimages/backoff" "github.com/apricote/hcloud-upload-image/hcloudimages/contextlogger" "github.com/apricote/hcloud-upload-image/internal/ui" "github.com/apricote/hcloud-upload-image/internal/version" @@ -29,11 +27,13 @@ var ( // 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", - Long: `Manage custom OS images on Hetzner Cloud.`, - SilenceUsage: true, +// 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, @@ -46,8 +46,6 @@ var rootCmd = &cobra.Command{ logger := slog.Default() ctx = contextlogger.New(ctx, logger) cmd.SetContext(ctx) - - client = newClient(ctx) }, } @@ -71,7 +69,15 @@ func initLogger() *slog.Logger { } -func newClient(ctx context.Context) *hcloudimages.Client { +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") == "" { @@ -82,29 +88,29 @@ func newClient(ctx context.Context) *hcloudimages.Client { opts := []hcloud.ClientOption{ hcloud.WithToken(os.Getenv("HCLOUD_TOKEN")), hcloud.WithApplication("hcloud-upload-image", version.Version), - hcloud.WithPollBackoffFunc(backoff.ExponentialBackoffWithLimit(2, 1*time.Second, 30*time.Second)), + 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)) } - return hcloudimages.NewClient(hcloud.NewClient(opts...)) + client = hcloudimages.NewClient(hcloud.NewClient(opts...)) } func Execute() { - err := rootCmd.Execute() + err := RootCmd.Execute() if err != nil { os.Exit(1) } } func init() { - rootCmd.SetErrPrefix("\033[1;31mError:") + RootCmd.SetErrPrefix("\033[1;31mError:") - rootCmd.PersistentFlags().CountVarP(&verbose, flagVerbose, "v", "verbose debug output, can be specified up to 2 times") + RootCmd.PersistentFlags().CountVarP(&verbose, flagVerbose, "v", "verbose debug output, can be specified up to 2 times") - rootCmd.AddGroup(&cobra.Group{ + RootCmd.AddGroup(&cobra.Group{ ID: "primary", Title: "Primary Commands:", }) diff --git a/cmd/upload.go b/cmd/upload.go index 843791a..d66ff6c 100644 --- a/cmd/upload.go +++ b/cmd/upload.go @@ -1,7 +1,9 @@ package cmd import ( + _ "embed" "fmt" + "net/http" "net/url" "os" @@ -16,23 +18,31 @@ const ( uploadFlagImageURL = "image-url" uploadFlagImagePath = "image-path" uploadFlagCompression = "compression" + uploadFlagFormat = "format" uploadFlagArchitecture = "architecture" + uploadFlagServerType = "server-type" uploadFlagDescription = "description" uploadFlagLabels = "labels" + uploadFlagLocation = "location" ) +//go:embed upload.md +var longDescription string + // uploadCmd represents the upload command var uploadCmd = &cobra.Command{ Use: "upload (--image-path= | --image-url=) --architecture=", Short: "Upload the specified disk image into your Hetzner Cloud project.", - Long: `This command implements a fake "upload", by going through a real server and snapshots. -This does cost a bit of money for the server.`, + Long: longDescription, Example: ` hcloud-upload-image upload --image-path /home/you/images/custom-linux-image-x86.bz2 --architecture x86 --compression bz2 --description "My super duper custom linux" hcloud-upload-image upload --image-url https://examples.com/image-arm.raw --architecture arm --labels foo=bar,version=latest -`, + hcloud-upload-image upload --image-url https://examples.com/image-x86.qcow2 --architecture x86 --format qcow2`, + DisableAutoGenTag: true, GroupID: "primary", + PreRun: initClient, + RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() logger := contextlogger.From(ctx) @@ -40,13 +50,16 @@ This does cost a bit of money for the server.`, imageURLString, _ := cmd.Flags().GetString(uploadFlagImageURL) imagePathString, _ := cmd.Flags().GetString(uploadFlagImagePath) imageCompression, _ := cmd.Flags().GetString(uploadFlagCompression) + imageFormat, _ := cmd.Flags().GetString(uploadFlagFormat) architecture, _ := cmd.Flags().GetString(uploadFlagArchitecture) + serverType, _ := cmd.Flags().GetString(uploadFlagServerType) description, _ := cmd.Flags().GetString(uploadFlagDescription) labels, _ := cmd.Flags().GetStringToString(uploadFlagLabels) + location, _ := cmd.Flags().GetString(uploadFlagLocation) options := hcloudimages.UploadOptions{ ImageCompression: hcloudimages.Compression(imageCompression), - Architecture: hcloud.Architecture(architecture), + ImageFormat: hcloudimages.Format(imageFormat), Description: hcloud.Ptr(description), Labels: labels, } @@ -57,8 +70,26 @@ This does cost a bit of money for the server.`, return fmt.Errorf("unable to parse url from --%s=%q: %w", uploadFlagImageURL, imageURLString, err) } + // Check for image size + resp, err := http.Head(imageURL.String()) + switch { + case err != nil: + logger.DebugContext(ctx, "failed to check for file size, error on request", "err", err) + case resp.ContentLength == -1: + logger.DebugContext(ctx, "failed to check for file size, server did not set the Content-Length", "err", err) + default: + options.ImageSize = resp.ContentLength + } + options.ImageURL = imageURL } else if imagePathString != "" { + stat, err := os.Stat(imagePathString) + if err != nil { + logger.DebugContext(ctx, "failed to check for file size, error on stat", "err", err) + } else { + options.ImageSize = stat.Size() + } + imageFile, err := os.Open(imagePathString) if err != nil { return fmt.Errorf("unable to read file from --%s=%q: %w", uploadFlagImagePath, imagePathString, err) @@ -67,6 +98,16 @@ This does cost a bit of money for the server.`, options.ImageReader = imageFile } + if architecture != "" { + options.Architecture = hcloud.Architecture(architecture) + } else if serverType != "" { + options.ServerType = &hcloud.ServerType{Name: serverType} + } + + if location != "" { + options.Location = &hcloud.Location{Name: location} + } + image, err := client.Upload(ctx, options) if err != nil { return fmt.Errorf("failed to upload the image: %w", err) @@ -79,17 +120,23 @@ This does cost a bit of money for the server.`, } func init() { - rootCmd.AddCommand(uploadCmd) + RootCmd.AddCommand(uploadCmd) uploadCmd.Flags().String(uploadFlagImageURL, "", "Remote URL of the disk image that should be uploaded") uploadCmd.Flags().String(uploadFlagImagePath, "", "Local path to the disk image that should be uploaded") uploadCmd.MarkFlagsMutuallyExclusive(uploadFlagImageURL, uploadFlagImagePath) uploadCmd.MarkFlagsOneRequired(uploadFlagImageURL, uploadFlagImagePath) - uploadCmd.Flags().String(uploadFlagCompression, "", "Type of compression that was used on the disk image [choices: bz2, xz]") + uploadCmd.Flags().String(uploadFlagCompression, "", "Type of compression that was used on the disk image [choices: bz2, xz, zstd]") _ = uploadCmd.RegisterFlagCompletionFunc( uploadFlagCompression, - cobra.FixedCompletions([]string{string(hcloudimages.CompressionBZ2), string(hcloudimages.CompressionXZ)}, cobra.ShellCompDirectiveNoFileComp), + cobra.FixedCompletions([]string{string(hcloudimages.CompressionBZ2), string(hcloudimages.CompressionXZ), string(hcloudimages.CompressionZSTD)}, cobra.ShellCompDirectiveNoFileComp), + ) + + uploadCmd.Flags().String(uploadFlagFormat, "", "Format of the image. [default: raw, choices: qcow2]") + _ = uploadCmd.RegisterFlagCompletionFunc( + uploadFlagFormat, + cobra.FixedCompletions([]string{string(hcloudimages.FormatQCOW2)}, cobra.ShellCompDirectiveNoFileComp), ) uploadCmd.Flags().String(uploadFlagArchitecture, "", "CPU architecture of the disk image [choices: x86, arm]") @@ -97,9 +144,20 @@ func init() { uploadFlagArchitecture, cobra.FixedCompletions([]string{string(hcloud.ArchitectureX86), string(hcloud.ArchitectureARM)}, cobra.ShellCompDirectiveNoFileComp), ) - _ = uploadCmd.MarkFlagRequired(uploadFlagArchitecture) + + uploadCmd.Flags().String(uploadFlagServerType, "", "Explicitly use this server type to generate the image. Mutually exclusive with --architecture.") + + // Only one of them needs to be set + uploadCmd.MarkFlagsOneRequired(uploadFlagArchitecture, uploadFlagServerType) + uploadCmd.MarkFlagsMutuallyExclusive(uploadFlagArchitecture, uploadFlagServerType) uploadCmd.Flags().String(uploadFlagDescription, "", "Description for the resulting image") uploadCmd.Flags().StringToString(uploadFlagLabels, map[string]string{}, "Labels for the resulting image") + + uploadCmd.Flags().String(uploadFlagLocation, "", "Datacenter location for the temporary server [default: fsn1, choices: fsn1, nbg1, hel1, ash, hil, sin]") + _ = uploadCmd.RegisterFlagCompletionFunc( + uploadFlagLocation, + cobra.FixedCompletions([]string{"fsn1", "nbg1", "hel1", "ash", "hil", "sin"}, cobra.ShellCompDirectiveNoFileComp), + ) } diff --git a/cmd/upload.md b/cmd/upload.md new file mode 100644 index 0000000..0d8c43c --- /dev/null +++ b/cmd/upload.md @@ -0,0 +1,12 @@ +This command implements a fake "upload", by going through a real server and +snapshots. This does cost a bit of money for the server. + +#### Image Size + +The image size for raw disk images is only limited by the servers root disk. + +The image size for qcow2 images is limited to the rescue systems root disk. +This is currently a memory-backed file system with **960 MB** of space. A qcow2 +image not be larger than this size, or the process will error. There is a +warning being logged if hcloud-upload-image can detect that your file is larger +than this size. diff --git a/docs/.gitignore b/docs/.gitignore new file mode 100644 index 0000000..b7a1b8c --- /dev/null +++ b/docs/.gitignore @@ -0,0 +1,2 @@ +book +https: diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md new file mode 100644 index 0000000..8f188b4 --- /dev/null +++ b/docs/SUMMARY.md @@ -0,0 +1,22 @@ +# Summary + +[Introduction](introduction.md) + +# Guides + +- [Uploading Images](guides/README.md) + - [Fedora CoreOS ↗](https://docs.fedoraproject.org/en-US/fedora-coreos/provisioning-hetzner/#_creating_a_snapshot) + - [Flatcar Container Linux ↗](https://www.flatcar.org/docs/latest/installing/cloud/hetzner/#building-the-snapshots-1) + - [Talos Linux ↗](https://www.talos.dev/v1.10/talos-guides/install/cloud-platforms/hetzner/#hcloud-upload-image) + +# Reference + +- [CLI](reference/cli/hcloud-upload-image.md) + - [`upload`](reference/cli/hcloud-upload-image_upload.md) + - [`cleanup`](reference/cli/hcloud-upload-image_cleanup.md) +- [Go Library](reference/go-library.md) + +--- + +[Changelog CLI](changelog.md) +[Changelog Go Library](changelog-hcloudimages.md) diff --git a/docs/book.toml b/docs/book.toml new file mode 100644 index 0000000..fe0f7e1 --- /dev/null +++ b/docs/book.toml @@ -0,0 +1,7 @@ +[book] +language = "en" +src = "." +title = "hcloud-upload-image" + +[output.html] +git-repository-url = "https://github.com/apricote/hcloud-upload-image" diff --git a/docs/changelog-hcloudimages.md b/docs/changelog-hcloudimages.md new file mode 100644 index 0000000..d492f13 --- /dev/null +++ b/docs/changelog-hcloudimages.md @@ -0,0 +1,3 @@ +# Changelog Library + +{{#include ../hcloudimages/CHANGELOG.md:2: }} diff --git a/docs/changelog.md b/docs/changelog.md new file mode 100644 index 0000000..76ff939 --- /dev/null +++ b/docs/changelog.md @@ -0,0 +1,3 @@ +# Changelog CLI + +{{#include ../CHANGELOG.md:2: }} diff --git a/docs/guides/README.md b/docs/guides/README.md new file mode 100644 index 0000000..9fc9435 --- /dev/null +++ b/docs/guides/README.md @@ -0,0 +1,7 @@ +# Uploading Images + +Check out these docs from other projects to learn how to use `hcloud-upload-image`: + +- [Fedora CoreOS ↗](https://docs.fedoraproject.org/en-US/fedora-coreos/provisioning-hetzner/#_creating_a_snapshot) +- [Flatcar Container Linux ↗](https://www.flatcar.org/docs/latest/installing/cloud/hetzner/#building-the-snapshots-1) +- [Talos Linux ↗](https://www.talos.dev/v1.10/talos-guides/install/cloud-platforms/hetzner/#hcloud-upload-image) diff --git a/docs/introduction.md b/docs/introduction.md new file mode 100644 index 0000000..71809aa --- /dev/null +++ b/docs/introduction.md @@ -0,0 +1,3 @@ +# Introduction + +{{#include ../README.md:2:}} \ No newline at end of file diff --git a/docs/reference/cli/hcloud-upload-image.md b/docs/reference/cli/hcloud-upload-image.md new file mode 100644 index 0000000..1e01ef0 --- /dev/null +++ b/docs/reference/cli/hcloud-upload-image.md @@ -0,0 +1,20 @@ +## hcloud-upload-image + +Manage custom OS images on Hetzner Cloud. + +### Synopsis + +Manage custom OS images on Hetzner Cloud. + +### Options + +``` + -h, --help help for hcloud-upload-image + -v, --verbose count verbose debug output, can be specified up to 2 times +``` + +### SEE ALSO + +* [hcloud-upload-image cleanup](hcloud-upload-image_cleanup.md) - Remove any temporary resources that were left over +* [hcloud-upload-image upload](hcloud-upload-image_upload.md) - Upload the specified disk image into your Hetzner Cloud project. + diff --git a/docs/reference/cli/hcloud-upload-image_cleanup.md b/docs/reference/cli/hcloud-upload-image_cleanup.md new file mode 100644 index 0000000..5e848a4 --- /dev/null +++ b/docs/reference/cli/hcloud-upload-image_cleanup.md @@ -0,0 +1,38 @@ +## hcloud-upload-image cleanup + +Remove any temporary resources that were left over + +### Synopsis + +If the upload fails at any point, there might still exist a server or +ssh key in your Hetzner Cloud project. This command cleans up any resources +that match the label "apricote.de/created-by=hcloud-upload-image". + +If you want to see a preview of what would be removed, you can use the official hcloud CLI and run: + + $ hcloud server list -l apricote.de/created-by=hcloud-upload-image + $ hcloud ssh-key list -l apricote.de/created-by=hcloud-upload-image + +This command does not handle any parallel executions of hcloud-upload-image +and will remove in-use resources if called at the same time. + +``` +hcloud-upload-image cleanup [flags] +``` + +### Options + +``` + -h, --help help for cleanup +``` + +### Options inherited from parent commands + +``` + -v, --verbose count verbose debug output, can be specified up to 2 times +``` + +### SEE ALSO + +* [hcloud-upload-image](hcloud-upload-image.md) - Manage custom OS images on Hetzner Cloud. + diff --git a/docs/reference/cli/hcloud-upload-image_upload.md b/docs/reference/cli/hcloud-upload-image_upload.md new file mode 100644 index 0000000..7ca3f31 --- /dev/null +++ b/docs/reference/cli/hcloud-upload-image_upload.md @@ -0,0 +1,57 @@ +## hcloud-upload-image upload + +Upload the specified disk image into your Hetzner Cloud project. + +### Synopsis + +This command implements a fake "upload", by going through a real server and +snapshots. This does cost a bit of money for the server. + +#### Image Size + +The image size for raw disk images is only limited by the servers root disk. + +The image size for qcow2 images is limited to the rescue systems root disk. +This is currently a memory-backed file system with **960 MB** of space. A qcow2 +image not be larger than this size, or the process will error. There is a +warning being logged if hcloud-upload-image can detect that your file is larger +than this size. + + +``` +hcloud-upload-image upload (--image-path= | --image-url=) --architecture= [flags] +``` + +### Examples + +``` + hcloud-upload-image upload --image-path /home/you/images/custom-linux-image-x86.bz2 --architecture x86 --compression bz2 --description "My super duper custom linux" + hcloud-upload-image upload --image-url https://examples.com/image-arm.raw --architecture arm --labels foo=bar,version=latest + hcloud-upload-image upload --image-url https://examples.com/image-x86.qcow2 --architecture x86 --format qcow2 +``` + +### Options + +``` + --architecture string CPU architecture of the disk image [choices: x86, arm] + --compression string Type of compression that was used on the disk image [choices: bz2, xz, zstd] + --description string Description for the resulting image + --format string Format of the image. [default: raw, choices: qcow2] + -h, --help help for upload + --image-path string Local path to the disk image that should be uploaded + --image-url string Remote URL of the disk image that should be uploaded + --labels stringToString Labels for the resulting image (default []) + --location string Datacenter location for the temporary server [default: fsn1, choices: fsn1, nbg1, hel1, ash, hil, sin] + --server-type string Explicitly use this server type to generate the image. Mutually exclusive with --architecture. +``` + +### Options inherited from parent commands + +``` + -v, --verbose count verbose debug output, can be specified up to 2 times +``` + +### SEE ALSO + +* [hcloud-upload-image](hcloud-upload-image.md) - Manage custom OS images on Hetzner Cloud. + diff --git a/docs/reference/go-library.md b/docs/reference/go-library.md new file mode 100644 index 0000000..46edc04 --- /dev/null +++ b/docs/reference/go-library.md @@ -0,0 +1,3 @@ +# Go Library + +You can find the documentation at [pkg.go.dev/github.com/apricote/hcloud-upload-image/hcloudimages ↗](https://pkg.go.dev/github.com/apricote/hcloud-upload-image/hcloudimages). diff --git a/go.mod b/go.mod index d00ea5c..dfe7bed 100644 --- a/go.mod +++ b/go.mod @@ -1,27 +1,32 @@ module github.com/apricote/hcloud-upload-image -go 1.22.2 +go 1.24.0 + +toolchain go1.25.5 require ( - github.com/apricote/hcloud-upload-image/hcloudimages v0.0.0 - github.com/hetznercloud/hcloud-go/v2 v2.8.0 - github.com/spf13/cobra v1.8.0 + github.com/apricote/hcloud-upload-image/hcloudimages v1.3.0 + github.com/hetznercloud/hcloud-go/v2 v2.34.0 + github.com/spf13/cobra v1.10.2 ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.6 // 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/crypto v0.23.0 // indirect - golang.org/x/net v0.24.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect - google.golang.org/protobuf v1.33.0 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + github.com/russross/blackfriday/v2 v2.1.0 // indirect + github.com/spf13/pflag v1.0.9 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/crypto v0.46.0 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect ) - -replace github.com/apricote/hcloud-upload-image/hcloudimages => ./hcloudimages diff --git a/go.sum b/go.sum index 48ec8d5..7a40be6 100644 --- a/go.sum +++ b/go.sum @@ -1,45 +1,69 @@ +github.com/apricote/hcloud-upload-image/hcloudimages v1.3.0 h1:FVIKGSqpxdkO4+t1N8a8xu/xxc+13vHy2QaTAWICuQo= +github.com/apricote/hcloud-upload-image/hcloudimages v1.3.0/go.mod h1:NiCZ7xGoYNbWeK9L083leB7/g5oa7SAOZq405XkUSeQ= 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 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.8.0 h1:vfbfL/JfV8dIZUX7ANHWEbKNqgFWsETqvt/EctvoFJ0= -github.com/hetznercloud/hcloud-go/v2 v2.8.0/go.mod h1:jvpP3qAWMIZ3WQwQLYa97ia6t98iPCgsJNwRts+Jnrk= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/hetznercloud/hcloud-go/v2 v2.34.0 h1:mxasKipFPDPzni85xcMgwYci2PH8TalLgyPJoTlhWDA= +github.com/hetznercloud/hcloud-go/v2 v2.34.0/go.mod h1:aF9x0XYNRRQ7N4gux/4cEJeGAHcggu7sX+BBA1rv8ks= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= +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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +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 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= 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.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -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.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= +github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 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 1b4ed3c..0e2f596 100644 --- a/go.work +++ b/go.work @@ -1,4 +1,6 @@ -go 1.22.2 +go 1.24.0 + +toolchain go1.25.4 use ( . diff --git a/go.work.sum b/go.work.sum index 3b3d09d..453c608 100644 --- a/go.work.sum +++ b/go.work.sum @@ -1,7 +1,9 @@ +cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjHpqDjYY= github.com/alecthomas/kingpin/v2 v2.4.0/go.mod h1:0gyi0zQnjuFk8xrkNKamJoyUo382HRL7ATRpFZCw6tE= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc= github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE= +github.com/apricote/hcloud-upload-image/hcloudimages v1.1.0/go.mod h1:iJ95BaLfISZBY9X8K2Y2A5a49dI0RLjAuq+4BqlOSgA= github.com/cpuguy83/go-md2man/v2 v2.0.3 h1:qMCsGGgs+MAzDFyp9LpAe1Lqy/fY/qCovCm0qnXZOBM= github.com/creack/pty v1.1.9 h1:uDmaGzcdjhF4i/plgjmEsriH11Y0o7RKapEf/LDaM3w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= @@ -21,52 +23,131 @@ github.com/dave/patsy v0.0.0-20210517141501-957256f50cba h1:1o36L4EKbZzazMk8iGC4 github.com/dave/patsy v0.0.0-20210517141501-957256f50cba/go.mod h1:qfR88CgEGLoiqDaE+xxDCi5QA5v4vUoW0UCX2Nd5Tlc= github.com/dave/rebecca v0.9.1 h1:jxVfdOxRirbXL28vXMvUvJ1in3djwkVKXCq339qhBL0= github.com/dave/rebecca v0.9.1/go.mod h1:N6XYdMD/OKw3lkF3ywh8Z6wPGuwNFDNtWYEMFWEmXBA= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/go-kit/log v0.2.1 h1:MRVx0/zhvdseW+Gza6N9rVzU/IVzaeE1SFI4raAhmBU= github.com/go-kit/log v0.2.1/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= github.com/go-logfmt/logfmt v0.5.1 h1:otpy5pqBCBZ1ng9RQ0dPu4PN7ba75Y/aA+UpowDyNVA= github.com/go-logfmt/logfmt v0.5.1/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs= +github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/jessevdk/go-flags v1.4.1-0.20181029123624-5de817a9aa20 h1:dAOsPLhnBzIyxu0VvmnKjlNcIlgMK+erD6VRHDtweMI= github.com/jessevdk/go-flags v1.4.1-0.20181029123624-5de817a9aa20/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/jmattheis/goverter v1.4.0 h1:SrboBYMpGkj1XSgFhWwqzdP024zIa1+58YzUm+0jcBE= github.com/jmattheis/goverter v1.4.0/go.mod h1:iVIl/4qItWjWj2g3vjouGoYensJbRqDHpzlEVMHHFeY= +github.com/jmattheis/goverter v1.5.1 h1:NdBYrF1V1EFQbAA1M/ZR4YVbQjxVl3L6Xupn7moF3LU= +github.com/jmattheis/goverter v1.5.1/go.mod h1:iVIl/4qItWjWj2g3vjouGoYensJbRqDHpzlEVMHHFeY= +github.com/jmattheis/goverter v1.8.0 h1:P8GQ/uJEzCwpNdm5vKxaAjDDMxTpsAJZxgrXegicAW8= +github.com/jmattheis/goverter v1.8.0/go.mod h1:c8TVzpum2NThy2eJ/Wz3tyqRxzpElP2xDfoHOIDrNSQ= +github.com/jmattheis/goverter v1.9.2 h1:pBjvkhJ0F3PKMqGyHPL0yqnbTe08jjZqt/Z9ZmNKtTQ= +github.com/jmattheis/goverter v1.9.2/go.mod h1:1n3q6zf7j58tXcRWHbLFxK2Jk8WQVzr0d3nuaCcRqeg= github.com/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/julienschmidt/httprouter v1.3.0 h1:U0609e9tgbseu3rBINet9P48AI/D3oJs4dN7jwJOQ1U= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= +github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +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/vburenin/ifacemaker v1.2.1 h1:3Vq8B/bfBgjWTkv+jDg4dVL1KHt3k1K4lO7XRxYA2sk= github.com/vburenin/ifacemaker v1.2.1/go.mod h1:5WqrzX2aD7/hi+okBjcaEQJMg4lDGrpuEX3B8L4Wgrs= +github.com/vburenin/ifacemaker v1.3.0 h1:X5//v/1tyORf5157wLATgP1wgquW3FUW91/OGHLRqGo= +github.com/vburenin/ifacemaker v1.3.0/go.mod h1:SxTD9m+6uBQyhd0aohV7R4iirO+l9mEoTn4nSe67vMs= github.com/xhit/go-str2duration/v2 v2.1.0 h1:lxklc02Drh6ynqX+DdPyp5pCKLUQpRT8bp8Ydu2Bstc= github.com/xhit/go-str2duration/v2 v2.1.0/go.mod h1:ohY8p+0f07DiV6Em5LKB0s2YpLtXVyJfNt1+BlmyAsU= github.com/yuin/goldmark v1.4.13 h1:fVcFKWvrslecOb/tg+Cc05dkeYx540o0FuFt3nUVDoE= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0 h1:vvrHzRwRfVKSiLrG+d4FMl/Qi4ukBCE6kZlTUkDYRT0= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= +golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= +golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= +golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0= golang.org/x/oauth2 v0.16.0 h1:aDkGMBSYxElaoP81NpoUoz2oo2R2wHdZpGToUxfyQrQ= golang.org/x/oauth2 v0.16.0/go.mod h1:hqZ+0LWXsiVoZpeld6jVt06P3adbS2Uu911W1SsJv2o= +golang.org/x/oauth2 v0.21.0 h1:tsimM75w1tF/uws5rbeHzIWxEqElMehnc+iW793zsZs= +golang.org/x/oauth2 v0.21.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.24.0 h1:KTBBxWqUa0ykRPLtV69rRto9TLXcqYkeswu48x/gvNE= +golang.org/x/oauth2 v0.24.0/go.mod h1:XYTD2NtWslqkgxebSiOHnXEap4TF09sJSc7H1sXbhtI= +golang.org/x/oauth2 v0.30.0 h1:dnDm7JmhM45NNpd8FDDeLhK6FwqbOf4MLCM9zb1BOHI= +golang.org/x/oauth2 v0.30.0/go.mod h1:B++QgG3ZKulg6sRPGD/mqlHQs5rB3Ml9erfeDY7xKlU= golang.org/x/sync v0.3.0 h1:ftCYgMx6zT/asHUrPw8BLLscYtGznsLAnjq5RH9P66E= golang.org/x/sync v0.3.0/go.mod h1:FU7BRWz2tNW+3quACPkgCx/L+uEAv1htQ0V83Z9Rj+Y= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= +golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= +golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= +golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2 h1:IRJeR9r1pYWsHKTRe/IInb7lYvbBVIqOgsX/u0mbOWY= +golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE= +golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457 h1:zf5N6UOrA487eEFacMePxjXAJctxKmyjKUsjA11Uzuk= +golang.org/x/telemetry v0.0.0-20240521205824-bda55230c457/go.mod h1:pRgIJT+bRLFKnoM1ldnzKoxTIn14Yxz928LQRYYgIN0= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= +golang.org/x/tools v0.25.0 h1:oFU9pkj/iJgs+0DT+VMHrx+oBKs/LJMV+Uvg78sl+fE= +golang.org/x/tools v0.25.0/go.mod h1:/vtpO8WL1N9cQC3FN5zPqb//fRXskFHbLKk4OW1Q7rg= +golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= +golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= diff --git a/hcloudimages/CHANGELOG.md b/hcloudimages/CHANGELOG.md new file mode 100644 index 0000000..e5d6069 --- /dev/null +++ b/hcloudimages/CHANGELOG.md @@ -0,0 +1,63 @@ +# Changelog + +## [1.3.0](https://github.com/apricote/hcloud-upload-image/compare/hcloudimages/v1.2.0...hcloudimages/v1.3.0) (2025-12-22) + + +### Features + +* add --location flag to specify datacenter region ([#141](https://github.com/apricote/hcloud-upload-image/issues/141)) ([fcbc14a](https://github.com/apricote/hcloud-upload-image/commit/fcbc14aab6d495d2c67d653f9ea1ff56a39a8c2f)), closes [#142](https://github.com/apricote/hcloud-upload-image/issues/142) + +## [1.2.0](https://github.com/apricote/hcloud-upload-image/compare/hcloudimages/v1.1.0...hcloudimages/v1.2.0) (2025-11-06) + + +### Features + +* change minimum required Go version to 1.24 ([#130](https://github.com/apricote/hcloud-upload-image/issues/130)) ([5eba2d5](https://github.com/apricote/hcloud-upload-image/commit/5eba2d52fe3aafb4fd0d93403548f4c32bc2b5ac)) +* support zstd compression ([#125](https://github.com/apricote/hcloud-upload-image/issues/125)) ([37ebbce](https://github.com/apricote/hcloud-upload-image/commit/37ebbce5179997ac216af274055fc34c777b01e6)), closes [#122](https://github.com/apricote/hcloud-upload-image/issues/122) +* update default x86 server type to cx23 ([#129](https://github.com/apricote/hcloud-upload-image/issues/129)) ([a205619](https://github.com/apricote/hcloud-upload-image/commit/a20561944d0ba9485a6e10e99df15c56a688541d)) + +## [1.1.0](https://github.com/apricote/hcloud-upload-image/compare/hcloudimages/v1.0.1...hcloudimages/v1.1.0) (2025-05-10) + + +### Features + +* smaller snapshots by zeroing disk first ([#101](https://github.com/apricote/hcloud-upload-image/issues/101)) ([fdfb284](https://github.com/apricote/hcloud-upload-image/commit/fdfb284533d3154806b0936c08015fd5cc64b0fb)), closes [#96](https://github.com/apricote/hcloud-upload-image/issues/96) + + +### Bug Fixes + +* upload from local image generates broken command ([#98](https://github.com/apricote/hcloud-upload-image/issues/98)) ([420dcf9](https://github.com/apricote/hcloud-upload-image/commit/420dcf94c965ee470602db6c9c23c777fda91222)), closes [#97](https://github.com/apricote/hcloud-upload-image/issues/97) + +## [1.0.1](https://github.com/apricote/hcloud-upload-image/compare/hcloudimages/v1.0.0...hcloudimages/v1.0.1) (2025-05-09) + + +### Bug Fixes + +* timeout while waiting for SSH to become available ([#92](https://github.com/apricote/hcloud-upload-image/issues/92)) ([e490b9a](https://github.com/apricote/hcloud-upload-image/commit/e490b9a7f394e268fa1946ca51aa998c78c3d46a)) + +## [1.0.0](https://github.com/apricote/hcloud-upload-image/compare/hcloudimages/v0.3.1...hcloudimages/v1.0.0) (2025-05-04) + + +### Features + +* upload qcow2 images ([#69](https://github.com/apricote/hcloud-upload-image/issues/69)) ([ac3e9dd](https://github.com/apricote/hcloud-upload-image/commit/ac3e9dd7ecd86d1538b6401c3073c7c078c40847)) + +## [0.3.1](https://github.com/apricote/hcloud-upload-image/compare/hcloudimages/v0.3.0...hcloudimages/v0.3.1) (2024-12-07) + + +### Bug Fixes + +* **cli:** local install fails because of go.mod replace ([#47](https://github.com/apricote/hcloud-upload-image/issues/47)) ([66dc5f7](https://github.com/apricote/hcloud-upload-image/commit/66dc5f70b604ed3ee964576d74f94bdcea710c95)) + +## [0.3.0](https://github.com/apricote/hcloud-upload-image/compare/hcloudimages/v0.2.0...hcloudimages/v0.3.0) (2024-06-23) + + +### Features + +* set server type explicitly ([#36](https://github.com/apricote/hcloud-upload-image/issues/36)) ([42eeb00](https://github.com/apricote/hcloud-upload-image/commit/42eeb00a0784e13a00a52cf15a8659b497d78d72)), closes [#30](https://github.com/apricote/hcloud-upload-image/issues/30) +* update default x86 server type to cx22 ([#38](https://github.com/apricote/hcloud-upload-image/issues/38)) ([ebe08b3](https://github.com/apricote/hcloud-upload-image/commit/ebe08b345c8f31df73087b091fa39f5fdc195156)) + + +### Bug Fixes + +* error early when the image write fails ([#34](https://github.com/apricote/hcloud-upload-image/issues/34)) ([256989f](https://github.com/apricote/hcloud-upload-image/commit/256989f4a37e7b124c0684aab0f34cf5e09559be)), closes [#33](https://github.com/apricote/hcloud-upload-image/issues/33) diff --git a/hcloudimages/backoff/backoff.go b/hcloudimages/backoff/backoff.go index 310312a..346fb8f 100644 --- a/hcloudimages/backoff/backoff.go +++ b/hcloudimages/backoff/backoff.go @@ -16,6 +16,10 @@ import ( // It uses the formula: // // min(b^retries * d, limit) +// +// This function has a known overflow issue and should not be used anymore. +// +// Deprecated: Use BackoffFuncWithOpts from github.com/hetznercloud/hcloud-go/v2/hcloud instead. 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 diff --git a/hcloudimages/client.go b/hcloudimages/client.go index 5205b77..cf2f4e7 100644 --- a/hcloudimages/client.go +++ b/hcloudimages/client.go @@ -10,6 +10,7 @@ import ( "time" "github.com/hetznercloud/hcloud-go/v2/hcloud" + "github.com/hetznercloud/hcloud-go/v2/hcloud/exp/kit/sshutil" "golang.org/x/crypto/ssh" "github.com/apricote/hcloud-upload-image/hcloudimages/contextlogger" @@ -17,7 +18,6 @@ import ( "github.com/apricote/hcloud-upload-image/hcloudimages/internal/control" "github.com/apricote/hcloud-upload-image/hcloudimages/internal/labelutil" "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" ) @@ -34,15 +34,19 @@ var ( } serverTypePerArchitecture = map[hcloud.Architecture]*hcloud.ServerType{ - hcloud.ArchitectureX86: {Name: "cx11"}, + hcloud.ArchitectureX86: {Name: "cx23"}, hcloud.ArchitectureARM: {Name: "cax11"}, } - defaultImage = &hcloud.Image{Name: "ubuntu-22.04"} + defaultImage = &hcloud.Image{Name: "ubuntu-24.04"} defaultLocation = &hcloud.Location{Name: "fsn1"} defaultRescueType = hcloud.ServerRescueTypeLinux64 defaultSSHDialTimeout = 1 * time.Minute + + // Size observed on x86, 2025-05-03, no idea if that changes. + // Might be able to extends this to more of the available memory. + rescueSystemRootDiskSizeMB int64 = 960 ) type UploadOptions struct { @@ -56,17 +60,30 @@ type UploadOptions struct { // set to anything else, the file will be decompressed before written to the disk. ImageCompression Compression + ImageFormat Format + + // Can be optionally set to make the client validate that the image can be written to the server. + ImageSize int64 + // Possible future additions: // ImageSignatureVerification // ImageLocalPath - // ImageType (RawDiskImage, ISO, qcow2, ...) // Architecture should match the architecture of the Image. This decides if the Snapshot can later be // used with [hcloud.ArchitectureX86] or [hcloud.ArchitectureARM] servers. // // Internally this decides what server type is used for the temporary server. + // + // Optional if [UploadOptions.ServerType] is set. Architecture hcloud.Architecture + // ServerType can be optionally set to override the default server type for the architecture. + // Situations where this makes sense: + // + // - Your image is larger than the root disk of the default server types. + // - The default server type is no longer available, or not temporarily out of stock. + ServerType *hcloud.ServerType + // Description is an optional description that the resulting image (snapshot) will have. There is no way to // select images by its description, you should use Labels if you need to identify your image later. Description *string @@ -77,6 +94,10 @@ type UploadOptions struct { // We also always add a label `apricote.de/created-by=hcloud-image-upload` ([CreatedByLabel], [CreatedByValue]). Labels map[string]string + // Location is the datacenter location for the temporary server. + // Defaults to fsn1 if not specified. + Location *hcloud.Location + // DebugSkipResourceCleanup will skip the cleanup of the temporary SSH Key and Server. DebugSkipResourceCleanup bool } @@ -87,9 +108,23 @@ const ( CompressionNone Compression = "" CompressionBZ2 Compression = "bz2" CompressionXZ Compression = "xz" + CompressionZSTD Compression = "zstd" // Possible future additions: - // zip,zstd + // zip +) + +type Format string + +const ( + FormatRaw Format = "" + + // FormatQCOW2 allows to upload images in the qcow2 format directly. + // + // The qcow2 image must fit on the disk available in the rescue system. "qemu-img dd", which is used to convert + // qcow2 to raw, requires a file as an input. If [UploadOption.ImageSize] is set and FormatQCOW2 is used, there is a + // warning message displayed if there is a high probability of issues. + FormatQCOW2 Format = "qcow2" ) // NewClient instantiates a new client. It requires a working [*hcloud.Client] to interact with the Hetzner Cloud API. @@ -125,9 +160,22 @@ func (s *Client) Upload(ctx context.Context, options UploadOptions) (*hcloud.Ima resourceName := resourcePrefix + id labels := labelutil.Merge(DefaultLabels, options.Labels) + // 0. Validations + if options.ImageFormat == FormatQCOW2 && options.ImageSize > 0 { + if options.ImageSize > rescueSystemRootDiskSizeMB*1024*1024 { + // Just a warning, because the size might change with time. + // Alternatively one could add an override flag for the check and make this an error. + logger.WarnContext(ctx, + fmt.Sprintf("image must be smaller than %d MB (rescue system root disk) for qcow2", rescueSystemRootDiskSizeMB), + "maximum-size", rescueSystemRootDiskSizeMB, + "actual-size", options.ImageSize/(1024*1024), + ) + } + } + // 1. Create SSH Key logger.InfoContext(ctx, "# Step 1: Generating SSH Key") - publicKey, privateKey, err := sshkey.GenerateKeyPair() + privateKey, publicKey, err := sshutil.GenerateKeyPair() if err != nil { return nil, fmt.Errorf("failed to generate temporary ssh key pair: %w", err) } @@ -159,14 +207,25 @@ func (s *Client) Upload(ctx context.Context, options UploadOptions) (*hcloud.Ima // 2. Create Server logger.InfoContext(ctx, "# Step 2: Creating Server") - serverType, ok := serverTypePerArchitecture[options.Architecture] - if !ok { - return nil, fmt.Errorf("unknown architecture %q, valid options: %q, %q", options.Architecture, hcloud.ArchitectureX86, hcloud.ArchitectureARM) + var serverType *hcloud.ServerType + if options.ServerType != nil { + serverType = options.ServerType + } else { + var ok bool + serverType, ok = serverTypePerArchitecture[options.Architecture] + if !ok { + return nil, fmt.Errorf("unknown architecture %q, valid options: %q, %q", options.Architecture, hcloud.ArchitectureX86, hcloud.ArchitectureARM) + } + } + + location := defaultLocation + if options.Location != nil { + location = options.Location } logger.DebugContext(ctx, "creating server with config", "image", defaultImage.Name, - "location", defaultLocation.Name, + "location", location.Name, "serverType", serverType.Name, ) serverCreateResult, _, err := s.c.Server.Create(ctx, hcloud.ServerCreateOpts{ @@ -180,7 +239,7 @@ func (s *Client) Upload(ctx context.Context, options UploadOptions) (*hcloud.Ima StartAfterCreate: hcloud.Ptr(false), // Image will never be booted, we only boot into rescue system Image: defaultImage, - Location: defaultLocation, + Location: location, Labels: labels, }) if err != nil { @@ -267,7 +326,7 @@ func (s *Client) Upload(ctx context.Context, options UploadOptions) (*hcloud.Ima err = control.Retry( contextlogger.New(ctx, logger.With("operation", "ssh")), - 10, + 100, // ~ 3 minutes func() error { var err error logger.DebugContext(ctx, "trying to connect to server", "ip", server.PublicNet.IPv4.IP) @@ -278,46 +337,44 @@ func (s *Client) Upload(ctx context.Context, options UploadOptions) (*hcloud.Ima if err != nil { return nil, fmt.Errorf("failed to ssh into temporary server: %w", err) } - defer sshClient.Close() + defer func() { _ = sshClient.Close() }() - // 6. SSH On Server: Download Image, Decompress, Write to Root Disk - logger.InfoContext(ctx, "# Step 6: Downloading image and writing to disk") - cmd := "" - if options.ImageURL != nil { - cmd += fmt.Sprintf("wget --no-verbose -O - %q | ", options.ImageURL.String()) + // 6. Wipe existing disk, to avoid storing any bytes from it in the snapshot + logger.InfoContext(ctx, "# Step 6: Cleaning existing disk") + + output, err := sshsession.Run(sshClient, "blkdiscard /dev/sda", nil) + logger.DebugContext(ctx, string(output)) + if err != nil { + return nil, fmt.Errorf("failed to clean existing disk: %w", err) } - if options.ImageCompression != CompressionNone { - switch options.ImageCompression { - case CompressionBZ2: - cmd += "bzip2 -cd | " - case CompressionXZ: - cmd += "xz -cd | " - default: - return nil, fmt.Errorf("unknown compression: %q", options.ImageCompression) - } + // 7. SSH On Server: Download Image, Decompress, Write to Root Disk + logger.InfoContext(ctx, "# Step 7: Downloading image and writing to disk") + + cmd, err := assembleCommand(options) + if err != nil { + return nil, err } - cmd += "dd of=/dev/sda bs=4M && sync" logger.DebugContext(ctx, "running download, decompress and write to disk command", "cmd", cmd) - output, err := sshsession.Run(sshClient, cmd, options.ImageReader) - logger.InfoContext(ctx, "# Step 6: Finished writing image to disk") + output, err = sshsession.Run(sshClient, cmd, options.ImageReader) + logger.InfoContext(ctx, "# Step 7: Finished writing image to disk") logger.DebugContext(ctx, string(output)) if err != nil { return nil, fmt.Errorf("failed to download and write the image: %w", err) } - // 7. SSH On Server: Shutdown - logger.InfoContext(ctx, "# Step 7: Shutting down server") + // 8. SSH On Server: Shutdown + logger.InfoContext(ctx, "# Step 8: Shutting down server") _, err = sshsession.Run(sshClient, "shutdown now", nil) if err != nil { // TODO Verify if shutdown error, otherwise return logger.WarnContext(ctx, "shutdown returned error", "err", err) } - // 8. Create Image from Server - logger.InfoContext(ctx, "# Step 8: Creating Image") + // 9. Create Image from Server + logger.InfoContext(ctx, "# Step 9: Creating Image") createImageResult, _, err := s.c.Server.CreateImage(ctx, server, &hcloud.ServerCreateImageOpts{ Type: hcloud.ImageTypeSnapshot, Description: options.Description, @@ -462,3 +519,41 @@ func (s *Client) cleanupTempSSHKeys(ctx context.Context, logger *slog.Logger, se return nil } + +func assembleCommand(options UploadOptions) (string, error) { + // Make sure that we fail early, ie. if the image url does not work + cmd := "set -euo pipefail && " + + if options.ImageURL != nil { + cmd += fmt.Sprintf("wget --no-verbose -O - %q | ", options.ImageURL.String()) + } + + if options.ImageCompression != CompressionNone { + switch options.ImageCompression { + case CompressionBZ2: + cmd += "bzip2 -cd | " + case CompressionXZ: + cmd += "xz -cd | " + case CompressionZSTD: + cmd += "zstd -cd | " + default: + return "", fmt.Errorf("unknown compression: %q", options.ImageCompression) + } + } + + switch options.ImageFormat { + case FormatRaw: + cmd += "dd of=/dev/sda bs=4M" + case FormatQCOW2: + cmd += "tee image.qcow2 > /dev/null && qemu-img dd -f qcow2 -O raw if=image.qcow2 of=/dev/sda bs=4M" + default: + return "", fmt.Errorf("unknown format: %q", options.ImageFormat) + } + + cmd += " && sync" + + // the pipefail does not work correctly without wrapping in bash. + cmd = fmt.Sprintf("bash -c '%s'", cmd) + + return cmd, nil +} diff --git a/hcloudimages/client_test.go b/hcloudimages/client_test.go index 4905bdf..d1d1d33 100644 --- a/hcloudimages/client_test.go +++ b/hcloudimages/client_test.go @@ -1,33 +1,125 @@ -package hcloudimages_test +package hcloudimages import ( - "context" - "fmt" "net/url" - - "github.com/hetznercloud/hcloud-go/v2/hcloud" - - "github.com/apricote/hcloud-upload-image/hcloudimages" + "testing" ) -func ExampleClient_Upload() { - client := hcloudimages.NewClient( - hcloud.NewClient(hcloud.WithToken("")), - ) - - imageURL, err := url.Parse("https://example.com/disk-image.raw.bz2") +func mustParseURL(s string) *url.URL { + u, err := url.Parse(s) if err != nil { panic(err) } - image, err := client.Upload(context.TODO(), hcloudimages.UploadOptions{ - ImageURL: imageURL, - ImageCompression: hcloudimages.CompressionBZ2, - Architecture: hcloud.ArchitectureX86, - }) - if err != nil { - panic(err) - } - - fmt.Printf("Uploaded Image: %d", image.ID) + return u +} + +func TestAssembleCommand(t *testing.T) { + tests := []struct { + name string + options UploadOptions + want string + wantErr bool + }{ + { + name: "local raw", + options: UploadOptions{}, + want: "bash -c 'set -euo pipefail && dd of=/dev/sda bs=4M && sync'", + }, + { + name: "remote raw", + options: UploadOptions{ + ImageURL: mustParseURL("https://example.com/image.xz"), + }, + want: "bash -c 'set -euo pipefail && wget --no-verbose -O - \"https://example.com/image.xz\" | dd of=/dev/sda bs=4M && sync'", + }, + { + name: "local xz", + options: UploadOptions{ + ImageCompression: CompressionXZ, + }, + want: "bash -c 'set -euo pipefail && xz -cd | dd of=/dev/sda bs=4M && sync'", + }, + { + name: "remote xz", + options: UploadOptions{ + ImageURL: mustParseURL("https://example.com/image.xz"), + ImageCompression: CompressionXZ, + }, + want: "bash -c 'set -euo pipefail && wget --no-verbose -O - \"https://example.com/image.xz\" | xz -cd | dd of=/dev/sda bs=4M && sync'", + }, + { + name: "local zstd", + options: UploadOptions{ + ImageCompression: CompressionZSTD, + }, + want: "bash -c 'set -euo pipefail && zstd -cd | dd of=/dev/sda bs=4M && sync'", + }, + { + name: "remote zstd", + options: UploadOptions{ + ImageURL: mustParseURL("https://example.com/image.zst"), + ImageCompression: CompressionZSTD, + }, + want: "bash -c 'set -euo pipefail && wget --no-verbose -O - \"https://example.com/image.zst\" | zstd -cd | dd of=/dev/sda bs=4M && sync'", + }, + { + name: "local bz2", + options: UploadOptions{ + ImageCompression: CompressionBZ2, + }, + want: "bash -c 'set -euo pipefail && bzip2 -cd | dd of=/dev/sda bs=4M && sync'", + }, + { + name: "remote bz2", + options: UploadOptions{ + ImageURL: mustParseURL("https://example.com/image.bz2"), + ImageCompression: CompressionBZ2, + }, + want: "bash -c 'set -euo pipefail && wget --no-verbose -O - \"https://example.com/image.bz2\" | bzip2 -cd | dd of=/dev/sda bs=4M && sync'", + }, + { + name: "local qcow2", + options: UploadOptions{ + ImageFormat: FormatQCOW2, + }, + want: "bash -c 'set -euo pipefail && tee image.qcow2 > /dev/null && qemu-img dd -f qcow2 -O raw if=image.qcow2 of=/dev/sda bs=4M && sync'", + }, + { + name: "remote qcow2", + options: UploadOptions{ + ImageURL: mustParseURL("https://example.com/image.qcow2"), + ImageFormat: FormatQCOW2, + }, + want: "bash -c 'set -euo pipefail && wget --no-verbose -O - \"https://example.com/image.qcow2\" | tee image.qcow2 > /dev/null && qemu-img dd -f qcow2 -O raw if=image.qcow2 of=/dev/sda bs=4M && sync'", + }, + + { + name: "unknown compression", + options: UploadOptions{ + ImageCompression: "noodle", + }, + wantErr: true, + }, + + { + name: "unknown format", + options: UploadOptions{ + ImageFormat: "poodle", + }, + wantErr: true, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := assembleCommand(tt.options) + if (err != nil) != tt.wantErr { + t.Errorf("assembleCommand() error = %v, wantErr %v", err, tt.wantErr) + return + } + if got != tt.want { + t.Errorf("assembleCommand() got = %v, want %v", got, tt.want) + } + }) + } } diff --git a/hcloudimages/doc_test.go b/hcloudimages/doc_test.go new file mode 100644 index 0000000..e07c720 --- /dev/null +++ b/hcloudimages/doc_test.go @@ -0,0 +1,34 @@ +package hcloudimages_test + +import ( + "context" + "fmt" + "net/url" + + "github.com/hetznercloud/hcloud-go/v2/hcloud" + + "github.com/apricote/hcloud-upload-image/hcloudimages" +) + +func ExampleClient_Upload() { + client := hcloudimages.NewClient( + hcloud.NewClient(hcloud.WithToken("")), + ) + + imageURL, err := url.Parse("https://example.com/disk-image.raw.bz2") + if err != nil { + panic(err) + } + + image, err := client.Upload(context.TODO(), hcloudimages.UploadOptions{ + ImageURL: imageURL, + ImageCompression: hcloudimages.CompressionBZ2, + Architecture: hcloud.ArchitectureX86, + Location: &hcloud.Location{Name: "nbg1"}, // Optional: defaults to fsn1 + }) + if err != nil { + panic(err) + } + + fmt.Printf("Uploaded Image: %d", image.ID) +} diff --git a/hcloudimages/go.mod b/hcloudimages/go.mod index 621cdac..27e05fd 100644 --- a/hcloudimages/go.mod +++ b/hcloudimages/go.mod @@ -1,25 +1,29 @@ module github.com/apricote/hcloud-upload-image/hcloudimages -go 1.22.2 +go 1.24.0 + +toolchain go1.25.5 require ( - github.com/hetznercloud/hcloud-go/v2 v2.8.0 - github.com/stretchr/testify v1.9.0 - golang.org/x/crypto v0.23.0 + github.com/hetznercloud/hcloud-go/v2 v2.34.0 + github.com/stretchr/testify v1.11.1 + golang.org/x/crypto v0.46.0 ) require ( github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // 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 - golang.org/x/net v0.24.0 // indirect - golang.org/x/sys v0.20.0 // indirect - golang.org/x/text v0.15.0 // indirect - google.golang.org/protobuf v1.33.0 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.66.1 // indirect + github.com/prometheus/procfs v0.16.1 // indirect + go.yaml.in/yaml/v2 v2.4.2 // indirect + golang.org/x/net v0.48.0 // indirect + golang.org/x/sys v0.39.0 // indirect + golang.org/x/text v0.32.0 // indirect + google.golang.org/protobuf v1.36.8 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/hcloudimages/go.sum b/hcloudimages/go.sum index 40c4e72..231bc23 100644 --- a/hcloudimages/go.sum +++ b/hcloudimages/go.sum @@ -1,43 +1,53 @@ 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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.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.8.0 h1:vfbfL/JfV8dIZUX7ANHWEbKNqgFWsETqvt/EctvoFJ0= -github.com/hetznercloud/hcloud-go/v2 v2.8.0/go.mod h1:jvpP3qAWMIZ3WQwQLYa97ia6t98iPCgsJNwRts+Jnrk= +github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= +github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/hetznercloud/hcloud-go/v2 v2.34.0 h1:mxasKipFPDPzni85xcMgwYci2PH8TalLgyPJoTlhWDA= +github.com/hetznercloud/hcloud-go/v2 v2.34.0/go.mod h1:aF9x0XYNRRQ7N4gux/4cEJeGAHcggu7sX+BBA1rv8ks= +github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= +github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 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/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= +github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 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/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 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.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= -golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= -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.20.0 h1:Od9JTbYCk261bKm4M/mw7AklTlFYIa0bIp9BgSm1S8Y= -golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.20.0 h1:VnkxpohqXaOBYJtBmEppKUG6mXpi+4O6purfc2+sMhw= -golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY= -golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= -golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= +go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= +golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= +golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= +golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= +golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= +golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= +golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= +google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 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= diff --git a/hcloudimages/internal/control/retry.go b/hcloudimages/internal/control/retry.go index d053aee..44cfe55 100644 --- a/hcloudimages/internal/control/retry.go +++ b/hcloudimages/internal/control/retry.go @@ -8,7 +8,8 @@ import ( "context" "time" - "github.com/apricote/hcloud-upload-image/hcloudimages/backoff" + "github.com/hetznercloud/hcloud-go/v2/hcloud" + "github.com/apricote/hcloud-upload-image/hcloudimages/contextlogger" ) @@ -18,7 +19,7 @@ func Retry(ctx context.Context, maxTries int, f func() error) error { var err error - backoffFunc := backoff.ExponentialBackoffWithLimit(2, 1*time.Second, 30*time.Second) + backoffFunc := hcloud.ExponentialBackoffWithOpts(hcloud.ExponentialBackoffOpts{Multiplier: 2, Base: 200 * time.Millisecond, Cap: 2 * time.Second}) for try := 0; try < maxTries; try++ { if ctx.Err() != nil { diff --git a/hcloudimages/internal/sshkey/ssh_key.go b/hcloudimages/internal/sshkey/ssh_key.go deleted file mode 100644 index e5c497f..0000000 --- a/hcloudimages/internal/sshkey/ssh_key.go +++ /dev/null @@ -1,49 +0,0 @@ -// SPDX-License-Identifier: MIT -// From https://gitlab.com/hetznercloud/fleeting-plugin-hetzner/-/blob/0f60204582289c243599f8ca0f5be4822789131d/internal/utils/ssh_key.go -// Copyright (c) 2024 Hetzner Cloud GmbH - -package sshkey - -import ( - "crypto/ed25519" - "encoding/pem" - - "golang.org/x/crypto/ssh" -) - -func GenerateKeyPair() ([]byte, []byte, error) { - pub, priv, err := ed25519.GenerateKey(nil) - if err != nil { - return nil, nil, err - } - - pubBytes, err := encodePublicKey(pub) - if err != nil { - return nil, nil, err - } - - privBytes, err := encodePrivateKey(priv) - if err != nil { - return nil, nil, err - } - - return pubBytes, privBytes, nil -} - -func encodePublicKey(pub ed25519.PublicKey) ([]byte, error) { - sshPub, err := ssh.NewPublicKey(pub) - if err != nil { - return nil, err - } - - return ssh.MarshalAuthorizedKey(sshPub), nil -} - -func encodePrivateKey(priv ed25519.PrivateKey) ([]byte, error) { - privPem, err := ssh.MarshalPrivateKey(priv, "") - if err != nil { - return nil, err - } - - return pem.EncodeToMemory(privPem), nil -} diff --git a/hcloudimages/internal/sshkey/ssh_key_test.go b/hcloudimages/internal/sshkey/ssh_key_test.go deleted file mode 100644 index 926f4ed..0000000 --- a/hcloudimages/internal/sshkey/ssh_key_test.go +++ /dev/null @@ -1,29 +0,0 @@ -// SPDX-License-Identifier: MIT -// From https://gitlab.com/hetznercloud/fleeting-plugin-hetzner/-/blob/0f60204582289c243599f8ca0f5be4822789131d/internal/utils/ssh_key_test.go -// Copyright (c) 2024 Hetzner Cloud GmbH - -package sshkey - -import ( - "strings" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestGenerateSSHKeyPair(t *testing.T) { - pubBytes, privBytes, err := GenerateKeyPair() - assert.Nil(t, err) - - pub := string(pubBytes) - priv := string(privBytes) - - if !(strings.HasPrefix(priv, "-----BEGIN OPENSSH PRIVATE KEY-----\n") && - strings.HasSuffix(priv, "-----END OPENSSH PRIVATE KEY-----\n")) { - assert.Fail(t, "private key is invalid", priv) - } - - if !strings.HasPrefix(pub, "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA") { - assert.Fail(t, "public key is invalid", pub) - } -} diff --git a/hcloudimages/internal/sshsession/session.go b/hcloudimages/internal/sshsession/session.go index 4306ce1..f725251 100644 --- a/hcloudimages/internal/sshsession/session.go +++ b/hcloudimages/internal/sshsession/session.go @@ -12,7 +12,7 @@ func Run(client *ssh.Client, cmd string, stdin io.Reader) ([]byte, error) { if err != nil { return nil, err } - defer sess.Close() + defer func() { _ = sess.Close() }() if stdin != nil { sess.Stdin = stdin diff --git a/internal/version/version.go b/internal/version/version.go index dccf51c..a4f584f 100644 --- a/internal/version/version.go +++ b/internal/version/version.go @@ -2,7 +2,7 @@ package version var ( // version is a semver version (https://semver.org). - version = "0.2.0" // x-release-please-version + version = "1.3.0" // x-release-please-version // versionPrerelease is a semver version pre-release identifier (https://semver.org). // diff --git a/renovate.json b/renovate.json index d970352..ada450e 100644 --- a/renovate.json +++ b/renovate.json @@ -11,12 +11,18 @@ "gomodTidy", "gomodUpdateImportPaths" ], + "goGetDirs": [ + "./...", + "./hcloudimages/..." + ], "customManagers": [ { "customType": "regex", - "fileMatch": ["^\\.github\\/(?:workflows|actions)\\/.+\\.ya?ml$"], + "managerFilePatterns": [ + "/^\\.github\\/(?:workflows|actions)\\/.+\\.ya?ml$/" + ], "matchStrings": [ - "(?:version|VERSION): (?.+) # renovate: datasource=(?[a-z-]+) depName=(?.+)(?: lookupName=(?.+))?(?: versioning=(?[a-z-]+))?" + "(?:version|VERSION): (?.+) # renovate: datasource=(?[a-z-]+) depName=(?.+)(?: packageName=(?.+))?(?: versioning=(?[a-z-]+))?" ] } ] diff --git a/scripts/cli-help-pages.go b/scripts/cli-help-pages.go new file mode 100644 index 0000000..527b04a --- /dev/null +++ b/scripts/cli-help-pages.go @@ -0,0 +1,35 @@ +package main + +import ( + "fmt" + "os" + + "github.com/spf13/cobra/doc" + + "github.com/apricote/hcloud-upload-image/cmd" +) + +func run() error { + // Define the directory where the docs will be generated + dir := "docs/reference/cli" + + // Ensure the directory exists + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("error creating docs directory: %v", err) + } + + // Generate the docs + if err := doc.GenMarkdownTree(cmd.RootCmd, dir); err != nil { + return fmt.Errorf("error generating docs: %v", err) + } + + fmt.Println("Docs generated successfully in", dir) + return nil +} + +func main() { + if err := run(); err != nil { + fmt.Printf("Error: %v\n", err) + os.Exit(1) + } +}