Sign Up
Log In
Log In
or
Sign Up
Places
All Projects
Status Monitor
Collapse sidebar
openSUSE:Factory:Rebuild
hcloud-upload-image
hcloud-upload-image-0.3.0.obscpio
Overview
Repositories
Revisions
Requests
Users
Attributes
Meta
File hcloud-upload-image-0.3.0.obscpio of Package hcloud-upload-image
07070100000000000041ED000000000000000000000002667830F700000000000000000000000000000000000000000000002200000000hcloud-upload-image-0.3.0/.github07070100000001000081A4000000000000000000000001667830F7000002F2000000000000000000000000000000000000003D00000000hcloud-upload-image-0.3.0/.github/release-please-config.json{ "$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", "group-pull-request-title-pattern": "chore(${branch}): release ${version}", "packages": { ".": { "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": "/" } }, "plugins": [ { "type": "linked-versions", "groupName": "repo", "components": [ "cli", "hcloudimages" ] } ] } 07070100000002000081A4000000000000000000000001667830F700000025000000000000000000000000000000000000003F00000000hcloud-upload-image-0.3.0/.github/release-please-manifest.json{".":"0.3.0","hcloudimages":"0.3.0"} 07070100000003000041ED000000000000000000000002667830F700000000000000000000000000000000000000000000002C00000000hcloud-upload-image-0.3.0/.github/workflows07070100000004000081A4000000000000000000000001667830F70000061B000000000000000000000000000000000000003400000000hcloud-upload-image-0.3.0/.github/workflows/ci.yamlname: ci on: push: branches: [main] pull_request: jobs: lint: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Run golangci-lint (CLI) uses: golangci/golangci-lint-action@v6 with: version: v1.59.1 # renovate: datasource=github-releases depName=golangci/golangci-lint args: --timeout 5m - name: Run golangci-lint (Lib) uses: golangci/golangci-lint-action@v6 with: version: v1.59.1 # 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 - name: Set up Go uses: actions/setup-go@v5 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 - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Run go mod tidy run: go mod tidy - name: Check uncommitted changes run: git diff --exit-code - if: failure() run: echo "::error::Check failed, please run 'go mod tidy' and commit the changes." 07070100000005000081A4000000000000000000000001667830F7000001B7000000000000000000000000000000000000004000000000hcloud-upload-image-0.3.0/.github/workflows/release-please.yamlname: release-please on: push: branches: [main] jobs: release-please: # Do not run on forks. if: github.repository == 'apricote/hcloud-upload-image' runs-on: ubuntu-latest steps: - uses: googleapis/release-please-action@v4 with: token: ${{ secrets.RELEASE_GH_TOKEN }} config-file: .github/release-please-config.json manifest-file: .github/release-please-manifest.json 07070100000006000081A4000000000000000000000001667830F700000226000000000000000000000000000000000000003900000000hcloud-upload-image-0.3.0/.github/workflows/release.yamlname: release on: push: tags: - "v*.*.*" jobs: release: runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v5 with: go-version-file: go.mod - name: Run GoReleaser uses: goreleaser/goreleaser-action@v6 with: version: "~> v2" args: release --clean env: GITHUB_TOKEN: ${{ secrets.RELEASE_GH_TOKEN }} AUR_SSH_KEY: ${{ secrets.RELEASE_AUR_SSH_KEY }} 07070100000007000081A4000000000000000000000001667830F700000014000000000000000000000000000000000000002500000000hcloud-upload-image-0.3.0/.gitignore dist/ completions/ 07070100000008000081A4000000000000000000000001667830F700000D3D000000000000000000000000000000000000002B00000000hcloud-upload-image-0.3.0/.goreleaser.yaml# yaml-language-server: $schema=https://goreleaser.com/static/schema.json version: 2 before: hooks: - ./scripts/completions.sh builds: - env: - CGO_ENABLED=0 goos: - linux - windows - darwin mod_timestamp: "{{ .CommitTimestamp }}" flags: - -trimpath ldflags: - -X {{ .ModulePath }}/internal/version.version={{ .Version }} - -X {{ .ModulePath }}/internal/version.versionPrerelease= archives: - format: tar.gz # this name template makes the OS and Arch compatible with the results of `uname`. name_template: >- {{ .ProjectName }}_ {{- title .Os }}_ {{- if eq .Arch "amd64" }}x86_64 {{- else if eq .Arch "386" }}i386 {{- else }}{{ .Arch }}{{ end }} {{- if .Arm }}v{{ .Arm }}{{ end }} # use zip for windows archives format_overrides: - goos: windows format: zip files: - README.md - LICENSE - completions/* nfpms: - id: default file_name_template: "{{ .ConventionalFileName }}" package_name: hcloud-upload-image vendor: Julian Tölle homepage: https://github.com/apricote/hcloud-upload-image maintainer: Julian Tölle <julian.toelle97@gmail.com> formats: - deb - rpm - apk description: Manage custom OS images on Hetzner Cloud. license: MIT dependencies: - openssh recommends: - hcloud-cli contents: - src: ./completions/hcloud-upload-image.bash dst: /usr/share/bash-completion/completions/hcloud-upload-image file_info: mode: 0644 - src: ./completions/hcloud-upload-image.fish dst: /usr/share/fish/vendor_completions.d/hcloud-upload-image.fish file_info: mode: 0644 - src: ./completions/hcloud-upload-image.zsh dst: /usr/share/zsh/vendor-completions/_hcloud-upload-image file_info: mode: 0644 - src: ./LICENSE dst: /usr/share/doc/hcloud-upload-image/license file_info: mode: 0644 aurs: - name: hcloud-upload-image-bin homepage: "https://github.com/apricote/hcloud-upload-image" description: Manage custom OS images on Hetzner Cloud. maintainers: - "Julian Tölle <julian.toelle97@gmail.com>" license: MIT private_key: "{{ .Env.AUR_SSH_KEY }}" git_url: "ssh://aur@aur.archlinux.org/hcloud-upload-image-bin.git" depends: - openssh package: |- # bin install -Dm755 "./hcloud-upload-image" "${pkgdir}/usr/bin/hcloud-upload-image" # license install -Dm644 "./LICENSE" "${pkgdir}/usr/share/licenses/hcloud-upload-image/LICENSE" # completions mkdir -p "${pkgdir}/usr/share/bash-completion/completions/" mkdir -p "${pkgdir}/usr/share/zsh/site-functions/" mkdir -p "${pkgdir}/usr/share/fish/vendor_completions.d/" install -Dm644 "./completions/hcloud-upload-image.bash" "${pkgdir}/usr/share/bash-completion/completions/hcloud-upload-image" 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" snapshot: name_template: "{{ .Version }}-dev+{{ .ShortCommit }}" changelog: # Generated by release-please disable: true 07070100000009000081A4000000000000000000000001667830F700000DA0000000000000000000000000000000000000002700000000hcloud-upload-image-0.3.0/CHANGELOG.md# Changelog ## [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) ### Features * packaging for deb, rpm, apk, aur ([#17](https://github.com/apricote/hcloud-upload-image/issues/17)) ([139761c](https://github.com/apricote/hcloud-upload-image/commit/139761cc28050b00bca22573d765f2b94af89bac)) * upload local disk images ([#15](https://github.com/apricote/hcloud-upload-image/issues/15)) ([fcea3e3](https://github.com/apricote/hcloud-upload-image/commit/fcea3e3c6e5ba7383aa69838401903e3f54f910c)) * upload xz compressed images ([#16](https://github.com/apricote/hcloud-upload-image/issues/16)) ([1c943e4](https://github.com/apricote/hcloud-upload-image/commit/1c943e4480ba2042fc3feabf363ec88eb2efbaee)) ### Bug Fixes * update user-agent in CLI ([#5](https://github.com/apricote/hcloud-upload-image/issues/5)) ([b17857c](https://github.com/apricote/hcloud-upload-image/commit/b17857c1fefc0b09da2ed2711b20ba76930dd365)) ## [0.1.1](https://github.com/apricote/hcloud-upload-image/compare/v0.1.0...v0.1.1) (2024-05-04) ### Bug Fixes * CLI does not produce release binaries ([#3](https://github.com/apricote/hcloud-upload-image/issues/3)) ([f373d4c](https://github.com/apricote/hcloud-upload-image/commit/f373d4c2baca9ccc892e6b6abff6dd217f2fdbeb)) ## [0.1.0](https://github.com/apricote/hcloud-upload-image/compare/v0.0.1...v0.1.0) (2024-05-04) ### Features * **cli:** docs grouping and version ([847b696](https://github.com/apricote/hcloud-upload-image/commit/847b696c74ce67c2f18aaa69af60f6c0c5b736c4)) * **cli:** hide redundant log attributes ([9e65452](https://github.com/apricote/hcloud-upload-image/commit/9e654521ae12debf40f181dfe291ad4ded0f7524)) * **cli:** upload command ([b6ae95f](https://github.com/apricote/hcloud-upload-image/commit/b6ae95f55ba134f5ef124d377ed3ad0a556b8cf4)) * documentation and cleanup command ([c9ab40b](https://github.com/apricote/hcloud-upload-image/commit/c9ab40b539bc51ea2611bb0b58ab8aef4ec06eea)) * initial library code ([4f57df5](https://github.com/apricote/hcloud-upload-image/commit/4f57df5b66ed1391155792758737b8f54b7ef2ab)) * log output ([904e5e0](https://github.com/apricote/hcloud-upload-image/commit/904e5e0bed6ba87e0f4063c27a0678a9c85b7371)) 0707010000000A000081A4000000000000000000000001667830F700000421000000000000000000000000000000000000002200000000hcloud-upload-image-0.3.0/LICENSECopyright (c) 2024 Julian Tölle Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 0707010000000B000081A4000000000000000000000001667830F700000DF7000000000000000000000000000000000000002400000000hcloud-upload-image-0.3.0/README.md# `hcloud-upload-image` 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. To create a completely custom disk image, users have to follow these steps: 1. Create server with the correct server type 2. Enable 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 6. Shut down the server 7. Take a snapshot of the servers 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 repository provides a simple CLI tool & Go library to do the above. ## Getting Started ### CLI #### Binary 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 ``` #### Usage ```shell export HCLOUD_TOKEN="<your token>" hcloud-upload-image upload \ --image-url "https://example.com/disk-image-x86.raw.bz2" \ --architecture x86 \ --compression bz2 ``` To learn more, you can use the embedded help output: ```shell hcloud-upload-image --help hcloud-upload-image upload --help hcloud-upload-image cleanup --help ``` ### Go Library The functionality to upload images is also exposed in the library `hcloudimages`! Check out the [reference documentation](https://pkg.go.dev/github.com/apricote/hcloud-upload-image/hcloudimages) for more details. #### Install ```shell go get github.com/apricote/hcloud-upload-image/hcloudimages ``` #### Usages ```go package main import ( "context" "fmt" "net/url" "github.com/hetznercloud/hcloud-go/v2/hcloud" "github.com/apricote/hcloud-upload-image/hcloudimages" ) func main() { client := hcloudimages.NewClient( hcloud.NewClient(hcloud.WithToken("<your token>")), ) imageURL, err := url.Parse("https://example.com/disk-image-x86.raw.bz2") 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) } ``` ## Contributing If you have any questions, feedback or ideas, feel free to open an issue or pull request. ## License This project is licensed under the MIT license, unless the file explicitly specifies another license. ## Support Disclaimer This is not an official Hetzner Cloud product in any way and Hetzner Cloud does not provide support for this. 0707010000000C000041ED000000000000000000000002667830F700000000000000000000000000000000000000000000001E00000000hcloud-upload-image-0.3.0/cmd0707010000000D000081A4000000000000000000000001667830F700000533000000000000000000000000000000000000002900000000hcloud-upload-image-0.3.0/cmd/cleanup.gopackage cmd import ( "fmt" "github.com/spf13/cobra" "github.com/apricote/hcloud-upload-image/hcloudimages/contextlogger" ) // cleanupCmd represents the cleanup command var cleanupCmd = &cobra.Command{ Use: "cleanup", Short: "Remove any temporary resources that were left over", Long: `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.`, GroupID: "primary", PreRun: initClient, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() logger := contextlogger.From(ctx) err := client.CleanupTempResources(ctx) if err != nil { return fmt.Errorf("failed to clean up temporary resources: %w", err) } logger.InfoContext(ctx, "Successfully cleaned up all temporary resources!") return nil }, } func init() { rootCmd.AddCommand(cleanupCmd) } 0707010000000E000081A4000000000000000000000001667830F700000B0B000000000000000000000000000000000000002600000000hcloud-upload-image-0.3.0/cmd/root.gopackage cmd import ( "log/slog" "os" "time" "github.com/hetznercloud/hcloud-go/v2/hcloud" "github.com/spf13/cobra" "github.com/apricote/hcloud-upload-image/hcloudimages" "github.com/apricote/hcloud-upload-image/hcloudimages/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" ) const ( flagVerbose = "verbose" ) var ( // 1 activates slog debug output // 2 activates hcloud-go debug output verbose int ) // The pre-authenticated client. Set in the root command PersistentPreRun var client *hcloudimages.Client // rootCmd represents the base command when called without any subcommands var rootCmd = &cobra.Command{ Use: "hcloud-upload-image", Long: `Manage custom OS images on Hetzner Cloud.`, SilenceUsage: true, Version: version.Version, PersistentPreRun: func(cmd *cobra.Command, _ []string) { ctx := cmd.Context() slog.SetDefault(initLogger()) // Add logger to command context logger := slog.Default() ctx = contextlogger.New(ctx, logger) cmd.SetContext(ctx) }, } func initLogger() *slog.Logger { logLevel := slog.LevelInfo if verbose >= 1 { logLevel = slog.LevelDebug } return slog.New(ui.NewHandler(os.Stdout, &ui.HandlerOptions{ Level: logLevel, ReplaceAttr: func(groups []string, a slog.Attr) slog.Attr { // Remove attributes that are unnecessary for the cli context if a.Key == "library" || a.Key == "method" { return slog.Attr{} } return a }, })) } func initClient(cmd *cobra.Command, _ []string) { if client != nil { // Only init if not set. // Theoretically this is not safe against data races and should use [sync.Once], but :shrug: return } ctx := cmd.Context() logger := contextlogger.From(ctx) // Build hcloud-go client if os.Getenv("HCLOUD_TOKEN") == "" { logger.ErrorContext(ctx, "You need to set the HCLOUD_TOKEN environment variable to your Hetzner Cloud API Token.") os.Exit(1) } opts := []hcloud.ClientOption{ hcloud.WithToken(os.Getenv("HCLOUD_TOKEN")), hcloud.WithApplication("hcloud-upload-image", version.Version), hcloud.WithPollBackoffFunc(backoff.ExponentialBackoffWithLimit(2, 1*time.Second, 30*time.Second)), } if os.Getenv("HCLOUD_DEBUG") != "" || verbose >= 2 { opts = append(opts, hcloud.WithDebugWriter(os.Stderr)) } client = hcloudimages.NewClient(hcloud.NewClient(opts...)) } func Execute() { err := rootCmd.Execute() if err != nil { os.Exit(1) } } func init() { rootCmd.SetErrPrefix("\033[1;31mError:") rootCmd.PersistentFlags().CountVarP(&verbose, flagVerbose, "v", "verbose debug output, can be specified up to 2 times") rootCmd.AddGroup(&cobra.Group{ ID: "primary", Title: "Primary Commands:", }) } 0707010000000F000081A4000000000000000000000001667830F7000011D1000000000000000000000000000000000000002800000000hcloud-upload-image-0.3.0/cmd/upload.gopackage cmd import ( "fmt" "net/url" "os" "github.com/hetznercloud/hcloud-go/v2/hcloud" "github.com/spf13/cobra" "github.com/apricote/hcloud-upload-image/hcloudimages" "github.com/apricote/hcloud-upload-image/hcloudimages/contextlogger" ) const ( uploadFlagImageURL = "image-url" uploadFlagImagePath = "image-path" uploadFlagCompression = "compression" uploadFlagArchitecture = "architecture" uploadFlagServerType = "server-type" uploadFlagDescription = "description" uploadFlagLabels = "labels" ) // uploadCmd represents the upload command var uploadCmd = &cobra.Command{ Use: "upload (--image-path=<local-path> | --image-url=<url>) --architecture=<x86|arm>", 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.`, 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 `, GroupID: "primary", PreRun: initClient, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() logger := contextlogger.From(ctx) imageURLString, _ := cmd.Flags().GetString(uploadFlagImageURL) imagePathString, _ := cmd.Flags().GetString(uploadFlagImagePath) imageCompression, _ := cmd.Flags().GetString(uploadFlagCompression) architecture, _ := cmd.Flags().GetString(uploadFlagArchitecture) serverType, _ := cmd.Flags().GetString(uploadFlagServerType) description, _ := cmd.Flags().GetString(uploadFlagDescription) labels, _ := cmd.Flags().GetStringToString(uploadFlagLabels) options := hcloudimages.UploadOptions{ ImageCompression: hcloudimages.Compression(imageCompression), Description: hcloud.Ptr(description), Labels: labels, } if imageURLString != "" { imageURL, err := url.Parse(imageURLString) if err != nil { return fmt.Errorf("unable to parse url from --%s=%q: %w", uploadFlagImageURL, imageURLString, err) } options.ImageURL = imageURL } else if imagePathString != "" { imageFile, err := os.Open(imagePathString) if err != nil { return fmt.Errorf("unable to read file from --%s=%q: %w", uploadFlagImagePath, imagePathString, err) } options.ImageReader = imageFile } if architecture != "" { options.Architecture = hcloud.Architecture(architecture) } else if serverType != "" { options.ServerType = &hcloud.ServerType{Name: serverType} } image, err := client.Upload(ctx, options) if err != nil { return fmt.Errorf("failed to upload the image: %w", err) } logger.InfoContext(ctx, "Successfully uploaded the image!", "image", image.ID) return nil }, } func init() { 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.RegisterFlagCompletionFunc( uploadFlagCompression, cobra.FixedCompletions([]string{string(hcloudimages.CompressionBZ2), string(hcloudimages.CompressionXZ)}, cobra.ShellCompDirectiveNoFileComp), ) uploadCmd.Flags().String(uploadFlagArchitecture, "", "CPU architecture of the disk image [choices: x86, arm]") _ = uploadCmd.RegisterFlagCompletionFunc( uploadFlagArchitecture, cobra.FixedCompletions([]string{string(hcloud.ArchitectureX86), string(hcloud.ArchitectureARM)}, cobra.ShellCompDirectiveNoFileComp), ) 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") } 07070100000010000081A4000000000000000000000001667830F70000038E000000000000000000000000000000000000002100000000hcloud-upload-image-0.3.0/go.modmodule github.com/apricote/hcloud-upload-image go 1.22.2 require ( github.com/apricote/hcloud-upload-image/hcloudimages v0.2.0 github.com/hetznercloud/hcloud-go/v2 v2.9.0 github.com/spf13/cobra v1.8.1 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/prometheus/client_golang v1.19.1 // 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.24.0 // indirect golang.org/x/net v0.25.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect google.golang.org/protobuf v1.33.0 // indirect ) replace github.com/apricote/hcloud-upload-image/hcloudimages => ./hcloudimages 07070100000011000081A4000000000000000000000001667830F700000F1B000000000000000000000000000000000000002100000000hcloud-upload-image-0.3.0/go.sumgithub.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.4/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hetznercloud/hcloud-go/v2 v2.9.0 h1:s0N6R7Zoi2DPfMtUF5o9VeUBzTtHVY6MIkHOQnfu/AY= github.com/hetznercloud/hcloud-go/v2 v2.9.0/go.mod h1:qtW/TuU7Bs16ibXl/ktJarWqU2LwHr7eGlwoilHxtgg= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= 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/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/spf13/cobra v1.8.1 h1:e5/vxKd/rZsfSJMUX1agtjeTDf+qv1/JdBF8gg5k9ZM= github.com/spf13/cobra v1.8.1/go.mod h1:wHxEcudfqmLYa8iTfL+OuZPbBZkmvliBWKIezN3kD9Y= 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.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 07070100000012000041ED000000000000000000000002667830F700000000000000000000000000000000000000000000002700000000hcloud-upload-image-0.3.0/hcloudimages07070100000013000081A4000000000000000000000001667830F7000003BA000000000000000000000000000000000000003400000000hcloud-upload-image-0.3.0/hcloudimages/CHANGELOG.md# Changelog ## [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) 07070100000014000041ED000000000000000000000002667830F700000000000000000000000000000000000000000000002F00000000hcloud-upload-image-0.3.0/hcloudimages/backoff07070100000015000081A4000000000000000000000001667830F7000002CE000000000000000000000000000000000000003A00000000hcloud-upload-image-0.3.0/hcloudimages/backoff/backoff.go// SPDX-License-Identifier: MPL-2.0 // From https://github.com/hetznercloud/terraform-provider-hcloud/blob/v1.46.1/internal/control/retry.go // Copyright (c) Hetzner Cloud GmbH package backoff import ( "math" "time" "github.com/hetznercloud/hcloud-go/v2/hcloud" ) // ExponentialBackoffWithLimit returns a [hcloud.BackoffFunc] which implements an exponential // backoff. // It uses the formula: // // min(b^retries * d, limit) func ExponentialBackoffWithLimit(b float64, d time.Duration, limit time.Duration) hcloud.BackoffFunc { return func(retries int) time.Duration { current := time.Duration(math.Pow(b, float64(retries))) * d if current > limit { return limit } else { return current } } } 07070100000016000081A4000000000000000000000001667830F700003CD4000000000000000000000000000000000000003100000000hcloud-upload-image-0.3.0/hcloudimages/client.gopackage hcloudimages import ( "context" "errors" "fmt" "io" "log/slog" "net/url" "time" "github.com/hetznercloud/hcloud-go/v2/hcloud" "golang.org/x/crypto/ssh" "github.com/apricote/hcloud-upload-image/hcloudimages/contextlogger" "github.com/apricote/hcloud-upload-image/hcloudimages/internal/actionutil" "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" ) const ( CreatedByLabel = "apricote.de/created-by" CreatedByValue = "hcloud-upload-image" resourcePrefix = "hcloud-upload-image-" ) var ( DefaultLabels = map[string]string{ CreatedByLabel: CreatedByValue, } serverTypePerArchitecture = map[hcloud.Architecture]*hcloud.ServerType{ hcloud.ArchitectureX86: {Name: "cx22"}, hcloud.ArchitectureARM: {Name: "cax11"}, } defaultImage = &hcloud.Image{Name: "ubuntu-24.04"} defaultLocation = &hcloud.Location{Name: "fsn1"} defaultRescueType = hcloud.ServerRescueTypeLinux64 defaultSSHDialTimeout = 1 * time.Minute ) type UploadOptions struct { // ImageURL must be publicly available. The instance will download the image from this endpoint. ImageURL *url.URL // ImageReader ImageReader io.Reader // ImageCompression describes the compression of the referenced image file. It defaults to [CompressionNone]. If // set to anything else, the file will be decompressed before written to the disk. ImageCompression Compression // 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 // Labels will be added to the resulting image (snapshot). Use these to filter the image list if you // need to identify the image later on. // // We also always add a label `apricote.de/created-by=hcloud-image-upload` ([CreatedByLabel], [CreatedByValue]). Labels map[string]string // DebugSkipResourceCleanup will skip the cleanup of the temporary SSH Key and Server. DebugSkipResourceCleanup bool } type Compression string const ( CompressionNone Compression = "" CompressionBZ2 Compression = "bz2" CompressionXZ Compression = "xz" // Possible future additions: // zip,zstd ) // NewClient instantiates a new client. It requires a working [*hcloud.Client] to interact with the Hetzner Cloud API. func NewClient(c *hcloud.Client) *Client { return &Client{ c: c, } } type Client struct { c *hcloud.Client } // Upload the specified image into a snapshot on Hetzner Cloud. // // As the Hetzner Cloud API has no direct way to upload images, we create a temporary server, // overwrite the root disk and take a snapshot of that disk instead. // // The temporary server costs money. If the upload fails, we might be unable to delete the server. Check out // CleanupTempResources for a helper in this case. func (s *Client) Upload(ctx context.Context, options UploadOptions) (*hcloud.Image, error) { logger := contextlogger.From(ctx).With( "library", "hcloudimages", "method", "upload", ) id, err := randomid.Generate() if err != nil { return nil, err } logger = logger.With("run-id", id) // For simplicity, we use the name random name for SSH Key + Server resourceName := resourcePrefix + id labels := labelutil.Merge(DefaultLabels, options.Labels) // 1. Create SSH Key logger.InfoContext(ctx, "# Step 1: Generating SSH Key") publicKey, privateKey, err := sshkey.GenerateKeyPair() if err != nil { return nil, fmt.Errorf("failed to generate temporary ssh key pair: %w", err) } key, _, err := s.c.SSHKey.Create(ctx, hcloud.SSHKeyCreateOpts{ Name: resourceName, PublicKey: string(publicKey), Labels: labels, }) if err != nil { return nil, fmt.Errorf("failed to submit temporary ssh key to API: %w", err) } logger.DebugContext(ctx, "Uploaded ssh key", "ssh-key-id", key.ID) defer func() { // Cleanup SSH Key if options.DebugSkipResourceCleanup { logger.InfoContext(ctx, "Cleanup: Skipping cleanup of temporary ssh key") return } logger.InfoContext(ctx, "Cleanup: Deleting temporary ssh key") _, err := s.c.SSHKey.Delete(ctx, key) if err != nil { logger.WarnContext(ctx, "Cleanup: ssh key could not be deleted", "error", err) // TODO } }() // 2. Create Server logger.InfoContext(ctx, "# Step 2: Creating Server") 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) } } logger.DebugContext(ctx, "creating server with config", "image", defaultImage.Name, "location", defaultLocation.Name, "serverType", serverType.Name, ) serverCreateResult, _, err := s.c.Server.Create(ctx, hcloud.ServerCreateOpts{ Name: resourceName, ServerType: serverType, // Not used, but without this the user receives an email with a password for every created server SSHKeys: []*hcloud.SSHKey{key}, // We need to enable rescue system first StartAfterCreate: hcloud.Ptr(false), // Image will never be booted, we only boot into rescue system Image: defaultImage, Location: defaultLocation, Labels: labels, }) if err != nil { return nil, fmt.Errorf("creating the temporary server failed: %w", err) } logger = logger.With("server", serverCreateResult.Server.ID) logger.DebugContext(ctx, "Created Server") logger.DebugContext(ctx, "waiting on actions") err = s.c.Action.WaitFor(ctx, append(serverCreateResult.NextActions, serverCreateResult.Action)...) if err != nil { return nil, fmt.Errorf("creating the temporary server failed: %w", err) } logger.DebugContext(ctx, "actions finished") server := serverCreateResult.Server defer func() { // Cleanup Server if options.DebugSkipResourceCleanup { logger.InfoContext(ctx, "Cleanup: Skipping cleanup of temporary server") return } logger.InfoContext(ctx, "Cleanup: Deleting temporary server") _, _, err := s.c.Server.DeleteWithResult(ctx, server) if err != nil { logger.WarnContext(ctx, "Cleanup: server could not be deleted", "error", err) } }() // 3. Activate Rescue System logger.InfoContext(ctx, "# Step 3: Activating Rescue System") enableRescueResult, _, err := s.c.Server.EnableRescue(ctx, server, hcloud.ServerEnableRescueOpts{ Type: defaultRescueType, SSHKeys: []*hcloud.SSHKey{key}, }) if err != nil { return nil, fmt.Errorf("enabling the rescue system on the temporary server failed: %w", err) } logger.DebugContext(ctx, "rescue system requested, waiting on action") err = s.c.Action.WaitFor(ctx, enableRescueResult.Action) if err != nil { return nil, fmt.Errorf("enabling the rescue system on the temporary server failed: %w", err) } logger.DebugContext(ctx, "action finished, rescue system enabled") // 4. Boot Server logger.InfoContext(ctx, "# Step 4: Booting Server") powerOnAction, _, err := s.c.Server.Poweron(ctx, server) if err != nil { return nil, fmt.Errorf("starting the temporary server failed: %w", err) } logger.DebugContext(ctx, "boot requested, waiting on action") err = s.c.Action.WaitFor(ctx, powerOnAction) if err != nil { return nil, fmt.Errorf("starting the temporary server failed: %w", err) } logger.DebugContext(ctx, "action finished, server is booting") // 5. Open SSH Session logger.InfoContext(ctx, "# Step 5: Opening SSH Connection") signer, err := ssh.ParsePrivateKey(privateKey) if err != nil { return nil, fmt.Errorf("parsing the automatically generated temporary private key failed: %w", err) } sshClientConfig := &ssh.ClientConfig{ User: "root", Auth: []ssh.AuthMethod{ ssh.PublicKeys(signer), }, // There is no way to get the host key of the rescue system beforehand HostKeyCallback: ssh.InsecureIgnoreHostKey(), Timeout: defaultSSHDialTimeout, } // the server needs some time until its properly started and ssh is available var sshClient *ssh.Client err = control.Retry( contextlogger.New(ctx, logger.With("operation", "ssh")), 10, func() error { var err error logger.DebugContext(ctx, "trying to connect to server", "ip", server.PublicNet.IPv4.IP) sshClient, err = ssh.Dial("tcp", server.PublicNet.IPv4.IP.String()+":ssh", sshClientConfig) return err }, ) if err != nil { return nil, fmt.Errorf("failed to ssh into temporary server: %w", err) } defer 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()) } 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) } } cmd += "dd of=/dev/sda bs=4M && sync" // Make sure that we fail early, ie. if the image url does not work. // the pipefail does not work correctly without wrapping in bash. cmd = fmt.Sprintf("bash -c 'set -euo pipefail && %s'", cmd) 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") 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") _, 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") createImageResult, _, err := s.c.Server.CreateImage(ctx, server, &hcloud.ServerCreateImageOpts{ Type: hcloud.ImageTypeSnapshot, Description: options.Description, Labels: labels, }) if err != nil { return nil, fmt.Errorf("failed to create snapshot: %w", err) } logger.DebugContext(ctx, "image creation requested, waiting on action") err = s.c.Action.WaitFor(ctx, createImageResult.Action) if err != nil { return nil, fmt.Errorf("failed to create snapshot: %w", err) } logger.DebugContext(ctx, "action finished, image was created") image := createImageResult.Image logger.InfoContext(ctx, "# Image was created", "image", image.ID) // Resource cleanup is happening in `defer` return image, nil } // CleanupTempResources tries to delete any resources that were left over from previous calls to [Client.Upload]. // Upload tries to clean up any temporary resources it created at runtime, but might fail at any point. // You can then use this command to make sure that all temporary resources are removed from your project. // // This method tries to delete any server or ssh keys that match the [DefaultLabels] func (s *Client) CleanupTempResources(ctx context.Context) error { logger := contextlogger.From(ctx).With( "library", "hcloudimages", "method", "cleanup", ) selector := labelutil.Selector(DefaultLabels) logger = logger.With("selector", selector) logger.InfoContext(ctx, "# Cleaning up Servers") err := s.cleanupTempServers(ctx, logger, selector) if err != nil { return fmt.Errorf("failed to clean up all servers: %w", err) } logger.DebugContext(ctx, "cleaned up all servers") logger.InfoContext(ctx, "# Cleaning up SSH Keys") err = s.cleanupTempSSHKeys(ctx, logger, selector) if err != nil { return fmt.Errorf("failed to clean up all ssh keys: %w", err) } logger.DebugContext(ctx, "cleaned up all ssh keys") return nil } func (s *Client) cleanupTempServers(ctx context.Context, logger *slog.Logger, selector string) error { servers, err := s.c.Server.AllWithOpts(ctx, hcloud.ServerListOpts{ListOpts: hcloud.ListOpts{ LabelSelector: selector, }}) if err != nil { return fmt.Errorf("failed to list servers: %w", err) } if len(servers) == 0 { logger.InfoContext(ctx, "No servers found") return nil } logger.InfoContext(ctx, "removing servers", "count", len(servers)) errs := []error{} actions := make([]*hcloud.Action, 0, len(servers)) for _, server := range servers { result, _, err := s.c.Server.DeleteWithResult(ctx, server) if err != nil { errs = append(errs, err) logger.WarnContext(ctx, "failed to delete server", "server", server.ID, "error", err) continue } actions = append(actions, result.Action) } successActions, errorActions, err := actionutil.Settle(ctx, &s.c.Action, actions...) if err != nil { return fmt.Errorf("failed to wait for server delete: %w", err) } if len(successActions) > 0 { ids := make([]int64, 0, len(successActions)) for _, action := range successActions { for _, resource := range action.Resources { if resource.Type == hcloud.ActionResourceTypeServer { ids = append(ids, resource.ID) } } } logger.InfoContext(ctx, "successfully deleted servers", "servers", ids) } if len(errorActions) > 0 { for _, action := range errorActions { errs = append(errs, action.Error()) } } if len(errs) > 0 { // The returned message contains no info about the server IDs which failed return fmt.Errorf("failed to delete some of the servers: %w", errors.Join(errs...)) } return nil } func (s *Client) cleanupTempSSHKeys(ctx context.Context, logger *slog.Logger, selector string) error { keys, _, err := s.c.SSHKey.List(ctx, hcloud.SSHKeyListOpts{ListOpts: hcloud.ListOpts{ LabelSelector: selector, }}) if err != nil { return fmt.Errorf("failed to list keys: %w", err) } if len(keys) == 0 { logger.InfoContext(ctx, "No ssh keys found") return nil } errs := []error{} for _, key := range keys { _, err := s.c.SSHKey.Delete(ctx, key) if err != nil { errs = append(errs, err) logger.WarnContext(ctx, "failed to delete ssh key", "ssh-key", key.ID, "error", err) continue } } if len(errs) > 0 { // The returned message contains no info about the server IDs which failed return fmt.Errorf("failed to delete some of the ssh keys: %w", errors.Join(errs...)) } return nil } 07070100000017000081A4000000000000000000000001667830F7000002A8000000000000000000000000000000000000003600000000hcloud-upload-image-0.3.0/hcloudimages/client_test.gopackage 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("<your token>")), ) 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, }) if err != nil { panic(err) } fmt.Printf("Uploaded Image: %d", image.ID) } 07070100000018000041ED000000000000000000000002667830F700000000000000000000000000000000000000000000003500000000hcloud-upload-image-0.3.0/hcloudimages/contextlogger07070100000019000081A4000000000000000000000001667830F7000002A0000000000000000000000000000000000000004000000000hcloud-upload-image-0.3.0/hcloudimages/contextlogger/context.gopackage contextlogger import ( "context" "log/slog" ) type key int var loggerKey key // New saves the logger as a value to the context. This can then be retrieved through [From]. func New(ctx context.Context, logger *slog.Logger) context.Context { return context.WithValue(ctx, loggerKey, logger) } // From returns the [*slog.Logger] set on the context by [New]. If there is none, // it returns a no-op logger that discards any output it receives. func From(ctx context.Context) *slog.Logger { if ctxLogger := ctx.Value(loggerKey); ctxLogger != nil { if logger, ok := ctxLogger.(*slog.Logger); ok { return logger } } return slog.New(discardHandler{}) } 0707010000001A000081A4000000000000000000000001667830F70000024A000000000000000000000000000000000000004000000000hcloud-upload-image-0.3.0/hcloudimages/contextlogger/discard.gopackage contextlogger import ( "context" "log/slog" ) // discardHandler is a [slog.Handler] that just discards any input. It is a safe default if any library // method does not get passed a logger through the context. type discardHandler struct{} func (discardHandler) Enabled(_ context.Context, _ slog.Level) bool { return false } func (discardHandler) Handle(_ context.Context, _ slog.Record) error { return nil } func (d discardHandler) WithAttrs(_ []slog.Attr) slog.Handler { return d } func (d discardHandler) WithGroup(_ string) slog.Handler { return d } 0707010000001B000081A4000000000000000000000001667830F7000007D0000000000000000000000000000000000000002E00000000hcloud-upload-image-0.3.0/hcloudimages/doc.go// Package hcloudimages is a library to upload Disk Images into your Hetzner Cloud project. // // # Overview // // The Hetzner Cloud API 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. // // To create a completely custom disk image, users have to follow these steps: // // 1. Create server with the correct server type // 2. Enable 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 // 6. Shut down the server // 7. Take a snapshot of the servers root disk // 8. Delete the server // // This is an annoyingly long process. Many users have automated this with Packer before, but Packer offers a lot of // additional complexity to understand. // // This library is a single call to do the above: [Client.Upload] // // # Costs // // The temporary server and the snapshot itself cost money. See the [Hetzner Cloud website] for up-to-date pricing // information. // // Usually the upload takes no more than a few minutes of server time, so you will only be billed for the first hour // (<1ct for most cases). If this process fails, the server might stay around until you manually delete it. In that case // it continues to cost its hourly price. There is a utility [Client.CleanupTemporaryResources] that removes any // leftover resources. // // # Logging // // By default, nothing is logged. As the update process takes a bit of time you might want to gain some insight into // the process. For this we provide optional logs through [log/slog]. You can set a [log/slog.Logger] in the // [context.Context] through [github.com/apricote/hcloud-upload-image/hcloudimages/contextlogger.New]. // // [Hetzner Cloud website]: https://www.hetzner.com/cloud/ package hcloudimages 0707010000001C000081A4000000000000000000000001667830F700000329000000000000000000000000000000000000002E00000000hcloud-upload-image-0.3.0/hcloudimages/go.modmodule github.com/apricote/hcloud-upload-image/hcloudimages go 1.22.2 require ( github.com/hetznercloud/hcloud-go/v2 v2.9.0 github.com/stretchr/testify v1.9.0 golang.org/x/crypto v0.24.0 ) require ( github.com/beorn7/perks v1.0.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.19.1 // 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.25.0 // indirect golang.org/x/sys v0.21.0 // indirect golang.org/x/text v0.16.0 // indirect google.golang.org/protobuf v1.33.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) 0707010000001D000081A4000000000000000000000001667830F700000F18000000000000000000000000000000000000002E00000000hcloud-upload-image-0.3.0/hcloudimages/go.sumgithub.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hetznercloud/hcloud-go/v2 v2.9.0 h1:s0N6R7Zoi2DPfMtUF5o9VeUBzTtHVY6MIkHOQnfu/AY= github.com/hetznercloud/hcloud-go/v2 v2.9.0/go.mod h1:qtW/TuU7Bs16ibXl/ktJarWqU2LwHr7eGlwoilHxtgg= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE= github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho= github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE= github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc= github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 0707010000001E000041ED000000000000000000000002667830F700000000000000000000000000000000000000000000003000000000hcloud-upload-image-0.3.0/hcloudimages/internal0707010000001F000041ED000000000000000000000002667830F700000000000000000000000000000000000000000000003B00000000hcloud-upload-image-0.3.0/hcloudimages/internal/actionutil07070100000020000081A4000000000000000000000001667830F70000027E000000000000000000000000000000000000004500000000hcloud-upload-image-0.3.0/hcloudimages/internal/actionutil/action.gopackage actionutil import ( "context" "github.com/hetznercloud/hcloud-go/v2/hcloud" ) func Settle(ctx context.Context, client hcloud.IActionClient, actions ...*hcloud.Action) (successActions []*hcloud.Action, errorActions []*hcloud.Action, err error) { err = client.WaitForFunc(ctx, func(update *hcloud.Action) error { switch update.Status { case hcloud.ActionStatusSuccess: successActions = append(successActions, update) case hcloud.ActionStatusError: errorActions = append(errorActions, update) } return nil }, actions...) if err != nil { return nil, nil, err } return successActions, errorActions, nil } 07070100000021000041ED000000000000000000000002667830F700000000000000000000000000000000000000000000003800000000hcloud-upload-image-0.3.0/hcloudimages/internal/control07070100000022000081A4000000000000000000000001667830F7000003A9000000000000000000000000000000000000004100000000hcloud-upload-image-0.3.0/hcloudimages/internal/control/retry.go// SPDX-License-Identifier: MPL-2.0 // From https://github.com/hetznercloud/terraform-provider-hcloud/blob/v1.46.1/internal/control/retry.go // Copyright (c) Hetzner Cloud GmbH package control import ( "context" "time" "github.com/apricote/hcloud-upload-image/hcloudimages/backoff" "github.com/apricote/hcloud-upload-image/hcloudimages/contextlogger" ) // Retry executes f at most maxTries times. func Retry(ctx context.Context, maxTries int, f func() error) error { logger := contextlogger.From(ctx) var err error backoffFunc := backoff.ExponentialBackoffWithLimit(2, 1*time.Second, 30*time.Second) for try := 0; try < maxTries; try++ { if ctx.Err() != nil { return ctx.Err() } err = f() if err != nil { sleep := backoffFunc(try) logger.DebugContext(ctx, "operation failed, waiting before trying again", "try", try, "backoff", sleep) time.Sleep(sleep) continue } return nil } return err } 07070100000023000041ED000000000000000000000002667830F700000000000000000000000000000000000000000000003A00000000hcloud-upload-image-0.3.0/hcloudimages/internal/labelutil07070100000024000081A4000000000000000000000001667830F700000210000000000000000000000000000000000000004400000000hcloud-upload-image-0.3.0/hcloudimages/internal/labelutil/labels.gopackage labelutil import "fmt" func Merge(a, b map[string]string) map[string]string { result := make(map[string]string, len(a)+len(b)) for k, v := range a { result[k] = v } for k, v := range b { result[k] = v } return result } func Selector(labels map[string]string) string { selector := make([]byte, 0, 64) separator := "" for k, v := range labels { selector = fmt.Appendf(selector, "%s%s=%s", separator, k, v) // Do not print separator on first element separator = "," } return string(selector) } 07070100000025000041ED000000000000000000000002667830F700000000000000000000000000000000000000000000003900000000hcloud-upload-image-0.3.0/hcloudimages/internal/randomid07070100000026000081A4000000000000000000000001667830F7000001E5000000000000000000000000000000000000004500000000hcloud-upload-image-0.3.0/hcloudimages/internal/randomid/randomid.go// SPDX-License-Identifier: MIT // From https://gitlab.com/hetznercloud/fleeting-plugin-hetzner/-/blob/0f60204582289c243599f8ca0f5be4822789131d/internal/utils/random.go // Copyright (c) 2024 Hetzner Cloud GmbH package randomid import ( "crypto/rand" "encoding/hex" "fmt" ) func Generate() (string, error) { b := make([]byte, 4) _, err := rand.Read(b) if err != nil { return "", fmt.Errorf("failed to generate random string: %w", err) } return hex.EncodeToString(b), nil } 07070100000027000081A4000000000000000000000001667830F700000213000000000000000000000000000000000000004A00000000hcloud-upload-image-0.3.0/hcloudimages/internal/randomid/randomid_test.go// SPDX-License-Identifier: MIT // From https://gitlab.com/hetznercloud/fleeting-plugin-hetzner/-/blob/0f60204582289c243599f8ca0f5be4822789131d/internal/utils/random_test.go // Copyright (c) 2024 Hetzner Cloud GmbH package randomid import ( "testing" "github.com/stretchr/testify/assert" ) func TestGenerateRandomID(t *testing.T) { found1, err := Generate() assert.NoError(t, err) found2, err := Generate() assert.NoError(t, err) assert.Len(t, found1, 8) assert.Len(t, found2, 8) assert.NotEqual(t, found1, found2) } 07070100000028000041ED000000000000000000000002667830F700000000000000000000000000000000000000000000003700000000hcloud-upload-image-0.3.0/hcloudimages/internal/sshkey07070100000029000081A4000000000000000000000001667830F700000404000000000000000000000000000000000000004200000000hcloud-upload-image-0.3.0/hcloudimages/internal/sshkey/ssh_key.go// 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 } 0707010000002A000081A4000000000000000000000001667830F700000314000000000000000000000000000000000000004700000000hcloud-upload-image-0.3.0/hcloudimages/internal/sshkey/ssh_key_test.go// 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) } } 0707010000002B000041ED000000000000000000000002667830F700000000000000000000000000000000000000000000003B00000000hcloud-upload-image-0.3.0/hcloudimages/internal/sshsession0707010000002C000081A4000000000000000000000001667830F70000013A000000000000000000000000000000000000004600000000hcloud-upload-image-0.3.0/hcloudimages/internal/sshsession/session.gopackage sshsession import ( "io" "golang.org/x/crypto/ssh" ) func Run(client *ssh.Client, cmd string, stdin io.Reader) ([]byte, error) { sess, err := client.NewSession() if err != nil { return nil, err } defer sess.Close() if stdin != nil { sess.Stdin = stdin } return sess.CombinedOutput(cmd) } 0707010000002D000041ED000000000000000000000002667830F700000000000000000000000000000000000000000000002300000000hcloud-upload-image-0.3.0/internal0707010000002E000041ED000000000000000000000002667830F700000000000000000000000000000000000000000000002600000000hcloud-upload-image-0.3.0/internal/ui0707010000002F000081A4000000000000000000000001667830F700001394000000000000000000000000000000000000003600000000hcloud-upload-image-0.3.0/internal/ui/slog_handler.gopackage ui import ( "context" "fmt" "io" "log/slog" "sync" ) // Developed with guidance from golang docs: // https://github.com/golang/example/blob/32022caedd6a177a7717aa8680cbe179e1045935/slog-handler-guide/README.md const ( ansiClear = "\033[0m" ansiBold = "\033[1m" ansiBoldYellow = "\033[1;93m" ansiBoldRed = "\033[1;31m" ansiThinGray = "\033[2;37m" ) type Handler struct { opts HandlerOptions goas []groupOrAttrs mu *sync.Mutex out io.Writer } // HandlerOptions are a subset of [slog.HandlerOptions] that are implemented for the UI handler. type HandlerOptions struct { // Level reports the minimum record level that will be logged. // The handler discards records with lower levels. // If Level is nil, the handler assumes LevelInfo. // The handler calls Level.Level for each record processed; // to adjust the minimum level dynamically, use a LevelVar. Level slog.Leveler // ReplaceAttr is called to rewrite each non-group attribute before it is logged. // The attribute's value has been resolved (see [Value.Resolve]). // If ReplaceAttr returns a zero Attr, the attribute is discarded. // // The built-in attributes with keys "time", "level", "source", and "msg" // are passed to this function, except that time is omitted // if zero, and source is omitted if AddSource is false. // // The first argument is a list of currently open groups that contain the // Attr. It must not be retained or modified. ReplaceAttr is never called // for Group attributes, only their contents. For example, the attribute // list // // Int("a", 1), Group("g", Int("b", 2)), Int("c", 3) // // results in consecutive calls to ReplaceAttr with the following arguments: // // nil, Int("a", 1) // []string{"g"}, Int("b", 2) // nil, Int("c", 3) // // ReplaceAttr can be used to change the default keys of the built-in // attributes, convert types (for example, to replace a `time.Time` with the // integer seconds since the Unix epoch), sanitize personal information, or // remove attributes from the output. ReplaceAttr func(groups []string, a slog.Attr) slog.Attr } // groupOrAttrs holds either a group name or a list of [slog.Attr]. type groupOrAttrs struct { group string // group name if non-empty attrs []slog.Attr // attrs if non-empty } var _ slog.Handler = &Handler{} func NewHandler(out io.Writer, opts *HandlerOptions) *Handler { h := &Handler{ out: out, mu: &sync.Mutex{}, } if opts != nil { h.opts = *opts } if h.opts.Level == nil { h.opts.Level = slog.LevelInfo } return h } func (h *Handler) Enabled(_ context.Context, level slog.Level) bool { return level >= h.opts.Level.Level() } func (h *Handler) Handle(_ context.Context, record slog.Record) error { buf := make([]byte, 0, 512) formattingPrefix := "" switch record.Level { case slog.LevelInfo: formattingPrefix = ansiBold case slog.LevelWarn: // Bold + Yellow formattingPrefix = ansiBoldYellow case slog.LevelError: // Bold + Red formattingPrefix = ansiBoldRed } // Print main message in formatted text buf = fmt.Appendf(buf, "%s%s%s", formattingPrefix, record.Message, ansiClear) // Add attributes in thin gray buf = fmt.Append(buf, ansiThinGray) // Attributes from [WithGroup] and [WithAttrs] calls goas := h.goas if record.NumAttrs() == 0 { for len(goas) > 0 && goas[len(goas)-1].group != "" { goas = goas[:len(goas)-1] } } group := "" for _, goa := range goas { if goa.group != "" { group = goa.group } else { for _, a := range goa.attrs { buf = h.appendAttr(buf, group, a) } } } record.Attrs(func(a slog.Attr) bool { buf = h.appendAttr(buf, group, a) return true }) buf = fmt.Appendf(buf, "%s\n", ansiClear) h.mu.Lock() defer h.mu.Unlock() _, err := h.out.Write(buf) return err } func (h *Handler) appendAttr(buf []byte, group string, a slog.Attr) []byte { a.Value = a.Value.Resolve() if h.opts.ReplaceAttr != nil { a = h.opts.ReplaceAttr([]string{group}, a) } // No-op if null attr if a.Equal(slog.Attr{}) { return buf } if group != "" { group += "." } switch a.Value.Kind() { case slog.KindString: buf = fmt.Appendf(buf, " %s%s=%q", group, a.Key, a.Value) case slog.KindAny: if err, ok := a.Value.Any().(error); ok { buf = fmt.Appendf(buf, " %s%s=%q", group, a.Key, err.Error()) } else { buf = fmt.Appendf(buf, " %s%s=%s", group, a.Key, a.Value) } default: buf = fmt.Appendf(buf, " %s%s=%s", group, a.Key, a.Value) } return buf } func (h *Handler) WithAttrs(attrs []slog.Attr) slog.Handler { if len(attrs) == 0 { return h } return h.withGroupOrAttrs(groupOrAttrs{attrs: attrs}) } func (h *Handler) WithGroup(name string) slog.Handler { if name == "" { return h } return h.withGroupOrAttrs(groupOrAttrs{group: name}) } func (h *Handler) withGroupOrAttrs(goa groupOrAttrs) *Handler { h2 := *h h2.goas = make([]groupOrAttrs, len(h.goas)+1) copy(h2.goas, h.goas) h2.goas[len(h2.goas)-1] = goa return &h2 } 07070100000030000041ED000000000000000000000002667830F700000000000000000000000000000000000000000000002B00000000hcloud-upload-image-0.3.0/internal/version07070100000031000081A4000000000000000000000001667830F7000001DA000000000000000000000000000000000000003600000000hcloud-upload-image-0.3.0/internal/version/version.gopackage version var ( // version is a semver version (https://semver.org). version = "0.3.0" // x-release-please-version // versionPrerelease is a semver version pre-release identifier (https://semver.org). // // For final releases, we set this to an empty string. versionPrerelease = "dev" // Version of the hcloud-upload-image CLI. Version = func() string { if versionPrerelease != "" { return version + "-" + versionPrerelease } return version }() ) 07070100000032000081A4000000000000000000000001667830F700000068000000000000000000000000000000000000002200000000hcloud-upload-image-0.3.0/main.gopackage main import ( "github.com/apricote/hcloud-upload-image/cmd" ) func main() { cmd.Execute() } 07070100000033000081A4000000000000000000000001667830F7000002AD000000000000000000000000000000000000002800000000hcloud-upload-image-0.3.0/renovate.json{ "$schema": "https://docs.renovatebot.com/renovate-schema.json", "extends": [ "config:recommended", ":semanticCommits", ":semanticCommitTypeAll(chore)", ":semanticCommitScope(deps)", ":enableVulnerabilityAlerts" ], "postUpdateOptions": [ "gomodTidy", "gomodUpdateImportPaths" ], "customManagers": [ { "customType": "regex", "fileMatch": ["^\\.github\\/(?:workflows|actions)\\/.+\\.ya?ml$"], "matchStrings": [ "(?:version|VERSION): (?<currentValue>.+) # renovate: datasource=(?<datasource>[a-z-]+) depName=(?<depName>.+)(?: lookupName=(?<lookupName>.+))?(?: versioning=(?<versioning>[a-z-]+))?" ] } ] } 07070100000034000041ED000000000000000000000002667830F700000000000000000000000000000000000000000000002200000000hcloud-upload-image-0.3.0/scripts07070100000035000081ED000000000000000000000001667830F700000099000000000000000000000000000000000000003100000000hcloud-upload-image-0.3.0/scripts/completions.sh#!/bin/sh set -e rm -rf completions mkdir completions for sh in bash zsh fish; do go run . completion "$sh" >"completions/hcloud-upload-image.$sh" done 07070100000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000B00000000TRAILER!!!147 blocks
Locations
Projects
Search
Status Monitor
Help
OpenBuildService.org
Documentation
API Documentation
Code of Conduct
Contact
Support
@OBShq
Terms
openSUSE Build Service is sponsored by
The Open Build Service is an
openSUSE project
.
Sign Up
Log In
Places
Places
All Projects
Status Monitor