Compare commits

..

No commits in common. "main" and "hcloudimages/v0.3.1" have entirely different histories.

39 changed files with 214 additions and 769 deletions

View file

@ -1,16 +0,0 @@
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

View file

@ -3,7 +3,7 @@
"include-component-in-tag": false,
"include-v-in-tag": true,
"release-type": "go",
"separate-pull-requests": true,
"group-pull-request-title-pattern": "chore(${branch}): release ${version}",
"packages": {
".": {
"component": "cli",
@ -16,5 +16,14 @@
"include-component-in-tag": true,
"tag-separator": "/"
}
}
},
"plugins": [
{
"type": "linked-versions",
"groupName": "repo",
"components": [
"cli", "hcloudimages"
]
}
]
}

View file

@ -1 +1 @@
{".":"1.3.0","hcloudimages":"1.3.0"}
{".":"0.3.1","hcloudimages":"0.3.1"}

View file

@ -10,23 +10,23 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version-file: go.mod
- name: Run golangci-lint (CLI)
uses: golangci/golangci-lint-action@v9
uses: golangci/golangci-lint-action@v6
with:
version: v2.7.2 # renovate: datasource=github-releases depName=golangci/golangci-lint
version: v1.62.2 # renovate: datasource=github-releases depName=golangci/golangci-lint
args: --timeout 5m
- name: Run golangci-lint (Lib)
uses: golangci/golangci-lint-action@v9
uses: golangci/golangci-lint-action@v6
with:
version: v2.7.2 # renovate: datasource=github-releases depName=golangci/golangci-lint
version: v1.62.2 # renovate: datasource=github-releases depName=golangci/golangci-lint
args: --timeout 5m
working-directory: hcloudimages
@ -34,10 +34,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version-file: go.mod
@ -48,10 +48,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version-file: go.mod
@ -68,10 +68,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v6
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version-file: go.mod

View file

@ -1,39 +0,0 @@
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

View file

@ -7,24 +7,13 @@ on:
jobs:
release:
permissions:
contents: read
packages: write
runs-on: ubuntu-latest
steps:
- name: Checkout
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 }}
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v6
uses: actions/setup-go@v5
with:
go-version-file: go.mod

View file

@ -7,8 +7,7 @@ before:
- ./scripts/completions.sh
builds:
- id: default
env:
- env:
- CGO_ENABLED=0
goos:
- linux
@ -22,7 +21,7 @@ builds:
- -X {{ .ModulePath }}/internal/version.versionPrerelease=
archives:
- formats: [ "tar.gz" ]
- format: tar.gz
# this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >-
{{ .ProjectName }}_
@ -34,7 +33,7 @@ archives:
# use zip for windows archives
format_overrides:
- goos: windows
formats: [ "zip" ]
format: zip
files:
- README.md
@ -105,23 +104,9 @@ 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:
version_template: "{{ .Version }}-dev+{{ .ShortCommit }}"
name_template: "{{ .Version }}-dev+{{ .ShortCommit }}"
changelog:
# Generated by release-please

View file

@ -1,50 +1,5 @@
# 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)

View file

@ -1,34 +1,29 @@
# hcloud-upload-image
<p align="center">
Quickly upload any raw disk images into your <a href="https://hetzner.com/cloud" target="_blank">Hetzner Cloud</a> projects!
</p>
<p align="center">
<a href="https://apricote.github.io/hcloud-upload-image" target="_blank"><img src="https://img.shields.io/badge/Documentation-brightgreen?style=flat-square" alt="Badge: Documentation"/></a>
<a href="https://github.com/apricote/hcloud-upload-image/releases" target="_blank"><img src="https://img.shields.io/github/v/release/apricote/hcloud-upload-image?sort=semver&display_name=release&style=flat-square&color=green" alt="Badge: Stable Release"/></a>
<img src="https://img.shields.io/badge/License-MIT-green?style=flat-square" alt="Badge: License MIT"/>
</p>
# `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 only provides a limited set of default images. The only option for custom disk images is to take a snapshot of an existing servers root disk. These snapshots can then be used to create new servers.
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 need to follow these steps:
To create a completely custom disk image, users have to follow these steps:
1. Create a server with the correct server type
2. Enable the rescue system for the server
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 the disk image to the servers root disk
5. Write disk image to servers root disk
6. Shut down the server
7. Take a snapshot of the servers root disk
7. Take a snapshot of the servers root disk
8. Delete the server
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 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 and Go library to streamline the process.
This repository provides a simple CLI tool & Go library to do the above.
## Getting Started
@ -40,7 +35,7 @@ We provide pre-built `deb`, `rpm` and `apk` packages. Alternatively we also prov
Check out the [GitHub release artifacts](https://github.com/apricote/hcloud-upload-image/releases/latest) for all of these files and archives.
#### Arch Linux
##### Arch Linux
You can get [`hcloud-upload-image-bin`](https://aur.archlinux.org/packages/hcloud-upload-image-bin) from the AUR.
@ -58,14 +53,6 @@ If you already have a recent Go toolchain installed, you can build & install the
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="<your token>" ghcr.io/apricote/hcloud-upload-image:latest <command>
```
#### Usage
```shell
@ -73,11 +60,10 @@ export HCLOUD_TOKEN="<your token>"
hcloud-upload-image upload \
--image-url "https://example.com/disk-image-x86.raw.bz2" \
--architecture x86 \
--compression bz2 \
--location nbg1 # Optional: defaults to fsn1
--compression bz2
```
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).:
To learn more, you can use the embedded help output or check out the [CLI help pages in this repository](docs/cli/hcloud-upload-image.md).:
```shell
hcloud-upload-image --help
@ -124,7 +110,6 @@ func main() {
ImageURL: imageURL,
ImageCompression: hcloudimages.CompressionBZ2,
Architecture: hcloud.ArchitectureX86,
Location: &hcloud.Location{Name: "nbg1"}, // Optional: defaults to fsn1
})
if err != nil {
panic(err)

View file

@ -9,6 +9,7 @@ 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"
@ -88,7 +89,7 @@ func initClient(cmd *cobra.Command, _ []string) {
opts := []hcloud.ClientOption{
hcloud.WithToken(os.Getenv("HCLOUD_TOKEN")),
hcloud.WithApplication("hcloud-upload-image", version.Version),
hcloud.WithPollOpts(hcloud.PollOpts{BackoffFunc: hcloud.ExponentialBackoffWithOpts(hcloud.ExponentialBackoffOpts{Multiplier: 2, Base: 1 * time.Second, Cap: 30 * time.Second})}),
hcloud.WithPollOpts(hcloud.PollOpts{BackoffFunc: backoff.ExponentialBackoffWithLimit(2, 1*time.Second, 30*time.Second)}),
}
if os.Getenv("HCLOUD_DEBUG") != "" || verbose >= 2 {

View file

@ -1,9 +1,7 @@
package cmd
import (
_ "embed"
"fmt"
"net/http"
"net/url"
"os"
@ -18,25 +16,20 @@ 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=<local-path> | --image-url=<url>) --architecture=<x86|arm>",
Short: "Upload the specified disk image into your Hetzner Cloud project.",
Long: longDescription,
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
hcloud-upload-image upload --image-url https://examples.com/image-x86.qcow2 --architecture x86 --format qcow2`,
hcloud-upload-image upload --image-url https://examples.com/image-arm.raw --architecture arm --labels foo=bar,version=latest`,
DisableAutoGenTag: true,
GroupID: "primary",
@ -50,16 +43,13 @@ var uploadCmd = &cobra.Command{
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),
ImageFormat: hcloudimages.Format(imageFormat),
Description: hcloud.Ptr(description),
Labels: labels,
}
@ -70,26 +60,8 @@ var uploadCmd = &cobra.Command{
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)
@ -104,10 +76,6 @@ var uploadCmd = &cobra.Command{
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)
@ -127,16 +95,10 @@ func init() {
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, zstd]")
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), 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),
cobra.FixedCompletions([]string{string(hcloudimages.CompressionBZ2), string(hcloudimages.CompressionXZ)}, cobra.ShellCompDirectiveNoFileComp),
)
uploadCmd.Flags().String(uploadFlagArchitecture, "", "CPU architecture of the disk image [choices: x86, arm]")
@ -154,10 +116,4 @@ func init() {
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),
)
}

View file

@ -1,12 +0,0 @@
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.

2
docs/.gitignore vendored
View file

@ -1,2 +0,0 @@
book
https:

View file

@ -1,22 +0,0 @@
# 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)

View file

@ -1,7 +0,0 @@
[book]
language = "en"
src = "."
title = "hcloud-upload-image"
[output.html]
git-repository-url = "https://github.com/apricote/hcloud-upload-image"

View file

@ -1,3 +0,0 @@
# Changelog Library
{{#include ../hcloudimages/CHANGELOG.md:2: }}

View file

@ -1,3 +0,0 @@
# Changelog CLI
{{#include ../CHANGELOG.md:2: }}

View file

@ -4,19 +4,8 @@ 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.
This command implements a fake "upload", by going through a real server and snapshots.
This does cost a bit of money for the server.
```
hcloud-upload-image upload (--image-path=<local-path> | --image-url=<url>) --architecture=<x86|arm> [flags]
@ -27,21 +16,18 @@ hcloud-upload-image upload (--image-path=<local-path> | --image-url=<url>) --arc
```
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]
--compression string Type of compression that was used on the disk image [choices: bz2, xz]
--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.
```

View file

@ -1,7 +0,0 @@
# 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)

View file

@ -1,3 +0,0 @@
# Introduction
{{#include ../README.md:2:}}

View file

@ -1,3 +0,0 @@
# 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).

37
go.mod
View file

@ -1,32 +1,31 @@
module github.com/apricote/hcloud-upload-image
go 1.24.0
toolchain go1.25.5
go 1.22.2
require (
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
github.com/apricote/hcloud-upload-image/hcloudimages v0.3.0
github.com/hetznercloud/hcloud-go/v2 v2.17.0
github.com/spf13/cobra v1.8.1
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/compress v1.17.9 // 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/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.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
github.com/spf13/pflag v1.0.5 // indirect
github.com/stretchr/testify v1.10.0 // indirect
golang.org/x/crypto v0.30.0 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

78
go.sum
View file

@ -1,21 +1,21 @@
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/apricote/hcloud-upload-image/hcloudimages v0.3.0 h1:POoU0nKeTq3fw7Ok/BTwJ/I3CTKhcChtn8L+Ecpba5U=
github.com/apricote/hcloud-upload-image/hcloudimages v0.3.0/go.mod h1:9W0XQVhzb9x69UBXXR0nxF0taC6s6fl0/nemDGUVrfI=
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.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/cpuguy83/go-md2man/v2 v2.0.4 h1:wfIWP927BUkWJb2NmU/kNDYIBTh/ziUX91+lVfRxZq4=
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.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/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.17.0 h1:ge0w2piey9SV6XGyU/wQ6HBR24QyMbJ3wLzezplqR68=
github.com/hetznercloud/hcloud-go/v2 v2.17.0/go.mod h1:zfyZ4Orx+mPpYDzWAxXR7DHGL50nnlZ5Edzgs1o6f/s=
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/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
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=
@ -26,42 +26,36 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
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.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/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
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.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=
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.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
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=

View file

@ -1,6 +1,4 @@
go 1.24.0
toolchain go1.25.4
go 1.22.2
use (
.

View file

@ -3,7 +3,6 @@ github.com/alecthomas/kingpin/v2 v2.4.0 h1:f48lwail6p8zpO1bC4TxtqACaGqHYA22qkHjH
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=
@ -33,20 +32,13 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS
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=
@ -73,8 +65,6 @@ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UV
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=
@ -85,22 +75,13 @@ 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=
@ -111,23 +92,12 @@ 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=
@ -135,10 +105,6 @@ 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=

View file

@ -1,47 +1,5 @@
# 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)

View file

@ -16,10 +16,6 @@ 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

View file

@ -34,7 +34,7 @@ var (
}
serverTypePerArchitecture = map[hcloud.Architecture]*hcloud.ServerType{
hcloud.ArchitectureX86: {Name: "cx23"},
hcloud.ArchitectureX86: {Name: "cx22"},
hcloud.ArchitectureARM: {Name: "cax11"},
}
@ -43,10 +43,6 @@ var (
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 {
@ -60,14 +56,10 @@ 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.
@ -94,10 +86,6 @@ 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
}
@ -108,23 +96,9 @@ const (
CompressionNone Compression = ""
CompressionBZ2 Compression = "bz2"
CompressionXZ Compression = "xz"
CompressionZSTD Compression = "zstd"
// Possible future additions:
// 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"
// zip,zstd
)
// NewClient instantiates a new client. It requires a working [*hcloud.Client] to interact with the Hetzner Cloud API.
@ -160,19 +134,6 @@ 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")
privateKey, publicKey, err := sshutil.GenerateKeyPair()
@ -218,14 +179,9 @@ func (s *Client) Upload(ctx context.Context, options UploadOptions) (*hcloud.Ima
}
}
location := defaultLocation
if options.Location != nil {
location = options.Location
}
logger.DebugContext(ctx, "creating server with config",
"image", defaultImage.Name,
"location", location.Name,
"location", defaultLocation.Name,
"serverType", serverType.Name,
)
serverCreateResult, _, err := s.c.Server.Create(ctx, hcloud.ServerCreateOpts{
@ -239,7 +195,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: location,
Location: defaultLocation,
Labels: labels,
})
if err != nil {
@ -326,7 +282,7 @@ func (s *Client) Upload(ctx context.Context, options UploadOptions) (*hcloud.Ima
err = control.Retry(
contextlogger.New(ctx, logger.With("operation", "ssh")),
100, // ~ 3 minutes
10,
func() error {
var err error
logger.DebugContext(ctx, "trying to connect to server", "ip", server.PublicNet.IPv4.IP)
@ -337,44 +293,50 @@ 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 func() { _ = sshClient.Close() }()
defer sshClient.Close()
// 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)
// 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())
}
// 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
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 7: Finished writing image to disk")
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)
}
// 8. SSH On Server: Shutdown
logger.InfoContext(ctx, "# Step 8: Shutting down server")
// 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)
}
// 9. Create Image from Server
logger.InfoContext(ctx, "# Step 9: Creating Image")
// 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,
@ -519,41 +481,3 @@ 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
}

View file

@ -1,125 +1,33 @@
package hcloudimages
package hcloudimages_test
import (
"context"
"fmt"
"net/url"
"testing"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
"github.com/apricote/hcloud-upload-image/hcloudimages"
)
func mustParseURL(s string) *url.URL {
u, err := url.Parse(s)
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)
}
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)
}
})
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)
}

View file

@ -1,34 +0,0 @@
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("<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,
Location: &hcloud.Location{Name: "nbg1"}, // Optional: defaults to fsn1
})
if err != nil {
panic(err)
}
fmt.Printf("Uploaded Image: %d", image.ID)
}

View file

@ -1,29 +1,27 @@
module github.com/apricote/hcloud-upload-image/hcloudimages
go 1.24.0
toolchain go1.25.5
go 1.22.2
require (
github.com/hetznercloud/hcloud-go/v2 v2.34.0
github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.46.0
github.com/hetznercloud/hcloud-go/v2 v2.17.0
github.com/stretchr/testify v1.10.0
golang.org/x/crypto v0.30.0
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/klauspost/compress v1.17.9 // 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.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
github.com/prometheus/client_golang v1.20.5 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.55.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
golang.org/x/net v0.30.0 // indirect
golang.org/x/sys v0.28.0 // indirect
golang.org/x/text v0.21.0 // indirect
google.golang.org/protobuf v1.34.2 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

View file

@ -4,12 +4,12 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
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.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/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.17.0 h1:ge0w2piey9SV6XGyU/wQ6HBR24QyMbJ3wLzezplqR68=
github.com/hetznercloud/hcloud-go/v2 v2.17.0/go.mod h1:zfyZ4Orx+mPpYDzWAxXR7DHGL50nnlZ5Edzgs1o6f/s=
github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
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=
@ -20,34 +20,30 @@ github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq
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.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/prometheus/client_golang v1.20.5 h1:cxppBPuYhUnsO6yo/aoRol4L7q7UFfdm+bR9r+8l63Y=
github.com/prometheus/client_golang v1.20.5/go.mod h1:PIEt8X02hGcP8JWbeHyeZ53Y/jReSnHgO035n//V5WE=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.55.0 h1:KEi6DK7lXW/m7Ig5i47x0vRzuBsHuvJdi5ee6Y3G1dc=
github.com/prometheus/common v0.55.0/go.mod h1:2SECS4xJG1kd8XF9IcM1gMX6510RAEL65zxzNImwdc8=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
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.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=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/crypto v0.30.0 h1:RwoQn3GkWiMkzlX562cLB7OxWvjH1L8xutO2WoJcRoY=
golang.org/x/crypto v0.30.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.27.0 h1:WP60Sv1nlK1T6SupCHbXzSaN0b9wUmsPoRS9b61A23Q=
golang.org/x/term v0.27.0/go.mod h1:iMsnZpn0cago0GOrHO2+Y7u7JPn5AylBrcoWkElMTSM=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
google.golang.org/protobuf v1.34.2 h1:6xV6lTsCfpGD21XK49h7MhtcApnLqkfYgPcdHftf6hg=
google.golang.org/protobuf v1.34.2/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw=
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=

View file

@ -8,8 +8,7 @@ import (
"context"
"time"
"github.com/hetznercloud/hcloud-go/v2/hcloud"
"github.com/apricote/hcloud-upload-image/hcloudimages/backoff"
"github.com/apricote/hcloud-upload-image/hcloudimages/contextlogger"
)
@ -19,7 +18,7 @@ func Retry(ctx context.Context, maxTries int, f func() error) error {
var err error
backoffFunc := hcloud.ExponentialBackoffWithOpts(hcloud.ExponentialBackoffOpts{Multiplier: 2, Base: 200 * time.Millisecond, Cap: 2 * time.Second})
backoffFunc := backoff.ExponentialBackoffWithLimit(2, 1*time.Second, 30*time.Second)
for try := 0; try < maxTries; try++ {
if ctx.Err() != nil {

View file

@ -12,7 +12,7 @@ func Run(client *ssh.Client, cmd string, stdin io.Reader) ([]byte, error) {
if err != nil {
return nil, err
}
defer func() { _ = sess.Close() }()
defer sess.Close()
if stdin != nil {
sess.Stdin = stdin

View file

@ -2,7 +2,7 @@ package version
var (
// version is a semver version (https://semver.org).
version = "1.3.0" // x-release-please-version
version = "0.3.1" // x-release-please-version
// versionPrerelease is a semver version pre-release identifier (https://semver.org).
//

View file

@ -11,18 +11,12 @@
"gomodTidy",
"gomodUpdateImportPaths"
],
"goGetDirs": [
"./...",
"./hcloudimages/..."
],
"customManagers": [
{
"customType": "regex",
"managerFilePatterns": [
"/^\\.github\\/(?:workflows|actions)\\/.+\\.ya?ml$/"
],
"fileMatch": ["^\\.github\\/(?:workflows|actions)\\/.+\\.ya?ml$"],
"matchStrings": [
"(?:version|VERSION): (?<currentValue>.+) # renovate: datasource=(?<datasource>[a-z-]+) depName=(?<depName>.+)(?: packageName=(?<packageName>.+))?(?: versioning=(?<versioning>[a-z-]+))?"
"(?:version|VERSION): (?<currentValue>.+) # renovate: datasource=(?<datasource>[a-z-]+) depName=(?<depName>.+)(?: lookupName=(?<lookupName>.+))?(?: versioning=(?<versioning>[a-z-]+))?"
]
}
]

View file

@ -11,7 +11,7 @@ import (
func run() error {
// Define the directory where the docs will be generated
dir := "docs/reference/cli"
dir := "docs/cli"
// Ensure the directory exists
if err := os.MkdirAll(dir, 0755); err != nil {