Script 'mail_helper' called by obssrc
Hello community,
here is the log from the commit of package hcloud-upload-image for
openSUSE:Factory checked in at 2026-06-22 17:35:18
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/hcloud-upload-image (Old)
and /work/SRC/openSUSE:Factory/.hcloud-upload-image.new.1956 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "hcloud-upload-image"
Mon Jun 22 17:35:18 2026 rev:7 rq:1360865 version:1.5.0
Changes:
--------
--- /work/SRC/openSUSE:Factory/hcloud-upload-image/hcloud-upload-image.changes
2026-05-15 23:54:46.036213814 +0200
+++
/work/SRC/openSUSE:Factory/.hcloud-upload-image.new.1956/hcloud-upload-image.changes
2026-06-22 17:35:28.526437997 +0200
@@ -1,0 +2,23 @@
+Sun Jun 21 16:30:35 UTC 2026 - Johannes Kastl
<[email protected]>
+
+- Update to version 1.5.0:
+ * BREAKING CHANGES
+ - new write-to-disk command (#178)
+ * Features
+ - new write-to-disk command (#178) (d65441f), closes #157
+ * Bug Fixes
+ - update hcloudimages module path for v2 (#184) (f4fd485)
+ * Miscellaneous Chores
+ - deps: update module
+ github.com/apricote/hcloud-upload-image/hcloudimages to
+ v2.0.1 (#186) (1b1e1af)
+ * Dependencies
+ - chore(deps): update actions/checkout action to v7 (#188)
+ - chore(main): release hcloudimages 2.0.1 (#185)
+ - chore(main): release hcloudimages 2.0.0 (#181)
+ - chore(deps): update dependency rust-lang/mdbook to v0.5.3
+ (#175)
+ - chore(deps): update go toolchain directive to v1.26.4 (#179)
+ - chore(deps): update actions/checkout digest to df4cb1c (#180)
+
+-------------------------------------------------------------------
Old:
----
hcloud-upload-image-1.4.0.obscpio
New:
----
hcloud-upload-image-1.5.0.obscpio
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Other differences:
------------------
++++++ hcloud-upload-image.spec ++++++
--- /var/tmp/diff_new_pack.MYCqvo/_old 2026-06-22 17:35:31.318536254 +0200
+++ /var/tmp/diff_new_pack.MYCqvo/_new 2026-06-22 17:35:31.322536394 +0200
@@ -17,7 +17,7 @@
Name: hcloud-upload-image
-Version: 1.4.0
+Version: 1.5.0
Release: 0
Summary: Quickly upload any raw disk images into your Hetzner Cloud
projects
License: MIT
++++++ _service ++++++
--- /var/tmp/diff_new_pack.MYCqvo/_old 2026-06-22 17:35:31.358537661 +0200
+++ /var/tmp/diff_new_pack.MYCqvo/_new 2026-06-22 17:35:31.362537802 +0200
@@ -5,7 +5,7 @@
<param name="exclude">.git</param>
<param name="exclude">go.work</param>
<param name="exclude">go.work.sum</param>
- <param name="revision">v1.4.0</param>
+ <param name="revision">v1.5.0</param>
<param name="match-tag">v*</param>
<param name="versionformat">@PARENT_TAG@</param>
<param name="versionrewrite-pattern">v(.*)</param>
++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.MYCqvo/_old 2026-06-22 17:35:31.406539350 +0200
+++ /var/tmp/diff_new_pack.MYCqvo/_new 2026-06-22 17:35:31.410539491 +0200
@@ -1,6 +1,6 @@
<servicedata>
<service name="tar_scm">
<param
name="url">https://github.com/apricote/hcloud-upload-image/</param>
- <param
name="changesrevision">73af6a8a750a37d062bbb8e8dd4d09f17274f5a3</param></service></servicedata>
+ <param
name="changesrevision">97083898f202471b7f7b96fa9417c1c0f1c025fc</param></service></servicedata>
(No newline at EOF)
++++++ hcloud-upload-image-1.4.0.obscpio -> hcloud-upload-image-1.5.0.obscpio
++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/hcloud-upload-image-1.4.0/CHANGELOG.md
new/hcloud-upload-image-1.5.0/CHANGELOG.md
--- old/hcloud-upload-image-1.4.0/CHANGELOG.md 2026-05-14 20:52:38.000000000
+0200
+++ new/hcloud-upload-image-1.5.0/CHANGELOG.md 2026-06-21 17:36:47.000000000
+0200
@@ -1,5 +1,26 @@
# Changelog
+##
[1.5.0](https://github.com/apricote/hcloud-upload-image/compare/v1.4.0...v1.5.0)
(2026-06-21)
+
+
+### ⚠ BREAKING CHANGES
+
+* new write-to-disk command
([#178](https://github.com/apricote/hcloud-upload-image/issues/178))
+
+### Features
+
+* new write-to-disk command
([#178](https://github.com/apricote/hcloud-upload-image/issues/178))
([d65441f](https://github.com/apricote/hcloud-upload-image/commit/d65441fc41e377e4a51c4b817383eb7167264c97)),
closes [#157](https://github.com/apricote/hcloud-upload-image/issues/157)
+
+
+### Bug Fixes
+
+* update hcloudimages module path for v2
([#184](https://github.com/apricote/hcloud-upload-image/issues/184))
([f4fd485](https://github.com/apricote/hcloud-upload-image/commit/f4fd4854492fd53221293cd828db16305c5accf5))
+
+
+### Miscellaneous Chores
+
+* **deps:** update module github.com/apricote/hcloud-upload-image/hcloudimages
to v2.0.1 ([#186](https://github.com/apricote/hcloud-upload-image/issues/186))
([1b1e1af](https://github.com/apricote/hcloud-upload-image/commit/1b1e1afffc089db41affaff730c18850abe1ac13))
+
##
[1.4.0](https://github.com/apricote/hcloud-upload-image/compare/v1.3.0...v1.4.0)
(2026-05-14)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/hcloud-upload-image-1.4.0/README.md
new/hcloud-upload-image-1.5.0/README.md
--- old/hcloud-upload-image-1.4.0/README.md 2026-05-14 20:52:38.000000000
+0200
+++ new/hcloud-upload-image-1.5.0/README.md 2026-06-21 17:36:47.000000000
+0200
@@ -134,7 +134,7 @@
#### Install
```shell
-go get github.com/apricote/hcloud-upload-image/hcloudimages
+go get github.com/apricote/hcloud-upload-image/hcloudimages/v2
```
#### Usages
@@ -149,7 +149,7 @@
"github.com/hetznercloud/hcloud-go/v2/hcloud"
- "github.com/apricote/hcloud-upload-image/hcloudimages"
+ "github.com/apricote/hcloud-upload-image/hcloudimages/v2"
)
func main() {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/hcloud-upload-image-1.4.0/cmd/cleanup.go
new/hcloud-upload-image-1.5.0/cmd/cleanup.go
--- old/hcloud-upload-image-1.4.0/cmd/cleanup.go 2026-05-14
20:52:38.000000000 +0200
+++ new/hcloud-upload-image-1.5.0/cmd/cleanup.go 2026-06-21
17:36:47.000000000 +0200
@@ -5,7 +5,7 @@
"github.com/spf13/cobra"
- "github.com/apricote/hcloud-upload-image/hcloudimages/contextlogger"
+ "github.com/apricote/hcloud-upload-image/hcloudimages/v2/contextlogger"
)
// cleanupCmd represents the cleanup command
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/hcloud-upload-image-1.4.0/cmd/root.go
new/hcloud-upload-image-1.5.0/cmd/root.go
--- old/hcloud-upload-image-1.4.0/cmd/root.go 2026-05-14 20:52:38.000000000
+0200
+++ new/hcloud-upload-image-1.5.0/cmd/root.go 2026-06-21 17:36:47.000000000
+0200
@@ -8,8 +8,8 @@
"github.com/hetznercloud/hcloud-go/v2/hcloud"
"github.com/spf13/cobra"
- "github.com/apricote/hcloud-upload-image/hcloudimages"
- "github.com/apricote/hcloud-upload-image/hcloudimages/contextlogger"
+ "github.com/apricote/hcloud-upload-image/hcloudimages/v2"
+ "github.com/apricote/hcloud-upload-image/hcloudimages/v2/contextlogger"
"github.com/apricote/hcloud-upload-image/internal/ui"
"github.com/apricote/hcloud-upload-image/internal/version"
)
@@ -26,6 +26,7 @@
// The pre-authenticated client. Set in the root command PersistentPreRun
var client *hcloudimages.Client
+var hcloudclient *hcloud.Client
// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
@@ -95,7 +96,8 @@
opts = append(opts, hcloud.WithDebugWriter(os.Stderr))
}
- client = hcloudimages.NewClient(hcloud.NewClient(opts...))
+ hcloudclient = hcloud.NewClient(opts...)
+ client = hcloudimages.NewClient(hcloudclient)
}
func Execute() {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/hcloud-upload-image-1.4.0/cmd/upload.go
new/hcloud-upload-image-1.5.0/cmd/upload.go
--- old/hcloud-upload-image-1.4.0/cmd/upload.go 2026-05-14 20:52:38.000000000
+0200
+++ new/hcloud-upload-image-1.5.0/cmd/upload.go 2026-06-21 17:36:47.000000000
+0200
@@ -3,22 +3,15 @@
import (
_ "embed"
"fmt"
- "net/http"
- "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"
+ "github.com/apricote/hcloud-upload-image/hcloudimages/v2"
+ "github.com/apricote/hcloud-upload-image/hcloudimages/v2/contextlogger"
)
const (
- uploadFlagImageURL = "image-url"
- uploadFlagImagePath = "image-path"
- uploadFlagCompression = "compression"
- uploadFlagFormat = "format"
uploadFlagArchitecture = "architecture"
uploadFlagServerType = "server-type"
uploadFlagDescription = "description"
@@ -27,13 +20,13 @@
)
//go:embed upload.md
-var longDescription string
+var uploadLongDescription 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: uploadLongDescription,
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`,
@@ -47,10 +40,11 @@
ctx := cmd.Context()
logger := contextlogger.From(ctx)
- imageURLString, _ := cmd.Flags().GetString(uploadFlagImageURL)
- imagePathString, _ := cmd.Flags().GetString(uploadFlagImagePath)
- imageCompression, _ :=
cmd.Flags().GetString(uploadFlagCompression)
- imageFormat, _ := cmd.Flags().GetString(uploadFlagFormat)
+ writeOptions, err := parseAndValidateWriteOptions(ctx,
cmd.Flags())
+ if err != nil {
+ return err
+ }
+
architecture, _ := cmd.Flags().GetString(uploadFlagArchitecture)
serverType, _ := cmd.Flags().GetString(uploadFlagServerType)
description, _ := cmd.Flags().GetString(uploadFlagDescription)
@@ -58,44 +52,9 @@
location, _ := cmd.Flags().GetString(uploadFlagLocation)
options := hcloudimages.UploadOptions{
- ImageCompression:
hcloudimages.Compression(imageCompression),
- ImageFormat: hcloudimages.Format(imageFormat),
- 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)
- }
-
- // 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)
- }
-
- options.ImageReader = imageFile
+ WriteOptions: writeOptions,
+ Description: hcloud.Ptr(description),
+ Labels: labels,
}
if architecture != "" {
@@ -122,22 +81,7 @@
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, zstd]")
- _ = 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),
- )
+ registerWriteOptions(uploadCmd)
uploadCmd.Flags().String(uploadFlagArchitecture, "", "CPU architecture
of the disk image [choices: x86, arm]")
_ = uploadCmd.RegisterFlagCompletionFunc(
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/hcloud-upload-image-1.4.0/cmd/write-to-disk.go
new/hcloud-upload-image-1.5.0/cmd/write-to-disk.go
--- old/hcloud-upload-image-1.4.0/cmd/write-to-disk.go 1970-01-01
01:00:00.000000000 +0100
+++ new/hcloud-upload-image-1.5.0/cmd/write-to-disk.go 2026-06-21
17:36:47.000000000 +0200
@@ -0,0 +1,166 @@
+package cmd
+
+import (
+ "context"
+ _ "embed"
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
+
+ "github.com/hetznercloud/hcloud-go/v2/hcloud"
+ "github.com/spf13/cobra"
+ "github.com/spf13/pflag"
+
+ "github.com/apricote/hcloud-upload-image/hcloudimages/v2"
+ "github.com/apricote/hcloud-upload-image/hcloudimages/v2/contextlogger"
+)
+
+const (
+ writeFlagImageURL = "image-url"
+ writeFlagImagePath = "image-path"
+ writeFlagCompression = "compression"
+ writeFlagFormat = "format"
+ writeFlagServer = "server"
+)
+
+func registerWriteOptions(cmd *cobra.Command) {
+ cmd.Flags().String(writeFlagImageURL, "", "Remote URL of the disk
image")
+ cmd.Flags().String(writeFlagImagePath, "", "Local path to the disk
image")
+ cmd.MarkFlagsMutuallyExclusive(writeFlagImageURL, writeFlagImagePath)
+ cmd.MarkFlagsOneRequired(writeFlagImageURL, writeFlagImagePath)
+
+ cmd.Flags().String(writeFlagCompression, "", "Type of compression that
was used on the disk image [choices: bz2, xz, zstd]")
+ _ = cmd.RegisterFlagCompletionFunc(
+ writeFlagCompression,
+
cobra.FixedCompletions([]string{string(hcloudimages.CompressionBZ2),
string(hcloudimages.CompressionXZ), string(hcloudimages.CompressionZSTD)},
cobra.ShellCompDirectiveNoFileComp),
+ )
+
+ cmd.Flags().String(writeFlagFormat, "", "Format of the disk image.
[default: raw, choices: qcow2]")
+ _ = cmd.RegisterFlagCompletionFunc(
+ writeFlagFormat,
+
cobra.FixedCompletions([]string{string(hcloudimages.FormatQCOW2)},
cobra.ShellCompDirectiveNoFileComp),
+ )
+}
+
+func parseAndValidateWriteOptions(ctx context.Context, flags *pflag.FlagSet)
(hcloudimages.WriteOptions, error) {
+ logger := contextlogger.From(ctx)
+
+ imageURLString, _ := flags.GetString(writeFlagImageURL)
+ imagePathString, _ := flags.GetString(writeFlagImagePath)
+ imageCompression, _ := flags.GetString(writeFlagCompression)
+ imageFormat, _ := flags.GetString(writeFlagFormat)
+
+ options := hcloudimages.WriteOptions{
+ ImageCompression: hcloudimages.Compression(imageCompression),
+ ImageFormat: hcloudimages.Format(imageFormat),
+ }
+
+ if imageURLString != "" {
+ imageURL, err := url.Parse(imageURLString)
+ if err != nil {
+ return hcloudimages.WriteOptions{}, fmt.Errorf("unable
to parse url from --%s=%q: %w", writeFlagImageURL, 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
+ }
+ _ = resp.Body.Close()
+
+ 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 hcloudimages.WriteOptions{}, fmt.Errorf("unable
to read file from --%s=%q: %w", writeFlagImagePath, imagePathString, err)
+ }
+
+ options.ImageReader = imageFile
+ }
+
+ return options, nil
+}
+
+//go:embed write-to-disk.md
+var writeToDiskLongDescription string
+
+// writeToDiskCmd represents the write-to-disk command
+var writeToDiskCmd = &cobra.Command{
+ Use: "write-to-disk (--image-path=<local-path> | --image-url=<url>)
--server <id-or-name>",
+ Short: "Write the specified disk image to the root disk of the
specified server.",
+ Long: writeToDiskLongDescription,
+ Example: ` hcloud-upload-image write-to-disk --image-path
/home/you/images/custom-linux-image-x86.bz2 --compression bz2 --server my-server
+ hcloud-upload-image write-to-disk --image-url
https://examples.com/image-arm.raw --server my-arm-server
+ hcloud-upload-image write-to-disk --image-url
https://examples.com/image-x86.qcow2 --format qcow2 --server my-x86-server`,
+ DisableAutoGenTag: true,
+
+ GroupID: "primary",
+
+ PreRun: initClient,
+
+ RunE: func(cmd *cobra.Command, args []string) error {
+ ctx := cmd.Context()
+ logger := contextlogger.From(ctx)
+
+ options, err := parseAndValidateWriteOptions(ctx, cmd.Flags())
+ if err != nil {
+ return err
+ }
+
+ serverIDOrName, _ := cmd.Flags().GetString(writeFlagServer)
+ options.Server, _, err = hcloudclient.Server.Get(ctx,
serverIDOrName)
+ if err != nil {
+ return fmt.Errorf("could not get server %q: %w",
serverIDOrName, err)
+ }
+ if options.Server == nil {
+ return fmt.Errorf("server %q not found", serverIDOrName)
+ }
+
+ err = client.WriteToDisk(ctx, options)
+ if err != nil {
+ return fmt.Errorf("failed to write the image: %w", err)
+ }
+
+ logger.InfoContext(ctx, "Successfully wrote the image!")
+
+ return nil
+ },
+}
+
+func init() {
+ RootCmd.AddCommand(writeToDiskCmd)
+
+ registerWriteOptions(writeToDiskCmd)
+
+ writeToDiskCmd.Flags().String(writeFlagServer, "", "ID or name of
target server")
+ _ = writeToDiskCmd.MarkFlagRequired(writeFlagServer)
+ _ = writeToDiskCmd.RegisterFlagCompletionFunc(
+ writeFlagServer,
+ func(cmd *cobra.Command, args []string, toComplete string)
([]cobra.Completion, cobra.ShellCompDirective) {
+ servers, err :=
hcloudclient.Server.AllWithOpts(cmd.Context(), hcloud.ServerListOpts{})
+ if err != nil {
+ return []cobra.Completion{},
cobra.ShellCompDirectiveError
+ }
+
+ serverNames := make([]string, len(servers))
+ for i, server := range servers {
+ serverNames[i] = server.Name
+ }
+
+ return serverNames, cobra.ShellCompDirectiveNoFileComp
+ },
+ )
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/hcloud-upload-image-1.4.0/cmd/write-to-disk.md
new/hcloud-upload-image-1.5.0/cmd/write-to-disk.md
--- old/hcloud-upload-image-1.4.0/cmd/write-to-disk.md 1970-01-01
01:00:00.000000000 +0100
+++ new/hcloud-upload-image-1.5.0/cmd/write-to-disk.md 2026-06-21
17:36:47.000000000 +0200
@@ -0,0 +1,12 @@
+This command writes the specified image to the target servers root disk. Think
of
+it as a one-off "upload".
+
+#### Image Size
+
+The image size for raw disk images is only limited by the servers root disk.
+
+The image size for qcow2 images is limited to the rescue systems root disk.
+This is currently a memory-backed file system with **960 MB** of space. A qcow2
+image not be larger than this size, or the process will error. There is a
+warning being logged if hcloud-upload-image can detect that your file is larger
+than this size.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/hcloud-upload-image-1.4.0/default.nix
new/hcloud-upload-image-1.5.0/default.nix
--- old/hcloud-upload-image-1.4.0/default.nix 2026-05-14 20:52:38.000000000
+0200
+++ new/hcloud-upload-image-1.5.0/default.nix 2026-06-21 17:36:47.000000000
+0200
@@ -9,13 +9,13 @@
(builtins.readFile ./internal/version/version.go));
src = ./.;
- vendorHash = "sha256-CVdhf8pDBANzlZzaqaGPv+vUTOQB/K+LEIh8tZUTSnA=";
+ vendorHash = "sha256-K8d6Q6WYJw3SelckscSh1CJBecG+y11+q9UcMDyotLU=";
env.GOWORK = "off";
subPackages = ["."];
goSum = ./go.sum; # make sure to rebuild
postPatch = ''
- echo 'replace github.com/apricote/hcloud-upload-image/hcloudimages =>
./hcloudimages' >> go.mod
+ echo 'replace github.com/apricote/hcloud-upload-image/hcloudimages/v2 =>
./hcloudimages' >> go.mod
'';
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/hcloud-upload-image-1.4.0/docs/reference/cli/hcloud-upload-image.md
new/hcloud-upload-image-1.5.0/docs/reference/cli/hcloud-upload-image.md
--- old/hcloud-upload-image-1.4.0/docs/reference/cli/hcloud-upload-image.md
2026-05-14 20:52:38.000000000 +0200
+++ new/hcloud-upload-image-1.5.0/docs/reference/cli/hcloud-upload-image.md
2026-06-21 17:36:47.000000000 +0200
@@ -17,4 +17,5 @@
* [hcloud-upload-image cleanup](hcloud-upload-image_cleanup.md) -
Remove any temporary resources that were left over
* [hcloud-upload-image upload](hcloud-upload-image_upload.md) - Upload the
specified disk image into your Hetzner Cloud project.
+* [hcloud-upload-image write-to-disk](hcloud-upload-image_write-to-disk.md)
- Write the specified disk image to the root disk of the specified server.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/hcloud-upload-image-1.4.0/docs/reference/cli/hcloud-upload-image_upload.md
new/hcloud-upload-image-1.5.0/docs/reference/cli/hcloud-upload-image_upload.md
---
old/hcloud-upload-image-1.4.0/docs/reference/cli/hcloud-upload-image_upload.md
2026-05-14 20:52:38.000000000 +0200
+++
new/hcloud-upload-image-1.5.0/docs/reference/cli/hcloud-upload-image_upload.md
2026-06-21 17:36:47.000000000 +0200
@@ -36,10 +36,10 @@
--architecture string CPU architecture of the disk image [choices:
x86, arm]
--compression string Type of compression that was used on the disk
image [choices: bz2, xz, zstd]
--description string Description for the resulting image
- --format string Format of the image. [default: raw, choices:
qcow2]
+ --format string Format of the disk 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
+ --image-path string Local path to the disk image
+ --image-url string Remote URL of the disk image
--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.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/hcloud-upload-image-1.4.0/docs/reference/cli/hcloud-upload-image_write-to-disk.md
new/hcloud-upload-image-1.5.0/docs/reference/cli/hcloud-upload-image_write-to-disk.md
---
old/hcloud-upload-image-1.4.0/docs/reference/cli/hcloud-upload-image_write-to-disk.md
1970-01-01 01:00:00.000000000 +0100
+++
new/hcloud-upload-image-1.5.0/docs/reference/cli/hcloud-upload-image_write-to-disk.md
2026-06-21 17:36:47.000000000 +0200
@@ -0,0 +1,53 @@
+## hcloud-upload-image write-to-disk
+
+Write the specified disk image to the root disk of the specified server.
+
+### Synopsis
+
+This command writes the specified image to the target servers root disk. Think
of
+it as a one-off "upload".
+
+#### Image Size
+
+The image size for raw disk images is only limited by the servers root disk.
+
+The image size for qcow2 images is limited to the rescue systems root disk.
+This is currently a memory-backed file system with **960 MB** of space. A qcow2
+image not be larger than this size, or the process will error. There is a
+warning being logged if hcloud-upload-image can detect that your file is larger
+than this size.
+
+
+```
+hcloud-upload-image write-to-disk (--image-path=<local-path> |
--image-url=<url>) --server <id-or-name> [flags]
+```
+
+### Examples
+
+```
+ hcloud-upload-image write-to-disk --image-path
/home/you/images/custom-linux-image-x86.bz2 --compression bz2 --server my-server
+ hcloud-upload-image write-to-disk --image-url
https://examples.com/image-arm.raw --server my-arm-server
+ hcloud-upload-image write-to-disk --image-url
https://examples.com/image-x86.qcow2 --format qcow2 --server my-x86-server
+```
+
+### Options
+
+```
+ --compression string Type of compression that was used on the disk
image [choices: bz2, xz, zstd]
+ --format string Format of the disk image. [default: raw, choices:
qcow2]
+ -h, --help help for write-to-disk
+ --image-path string Local path to the disk image
+ --image-url string Remote URL of the disk image
+ --server string ID or name of target server
+```
+
+### Options inherited from parent commands
+
+```
+ -v, --verbose count verbose debug output, can be specified up to 2 times
+```
+
+### SEE ALSO
+
+* [hcloud-upload-image](hcloud-upload-image.md) - Manage custom OS
images on Hetzner Cloud.
+
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/hcloud-upload-image-1.4.0/go.mod
new/hcloud-upload-image-1.5.0/go.mod
--- old/hcloud-upload-image-1.4.0/go.mod 2026-05-14 20:52:38.000000000
+0200
+++ new/hcloud-upload-image-1.5.0/go.mod 2026-06-21 17:36:47.000000000
+0200
@@ -2,12 +2,13 @@
go 1.25.0
-toolchain go1.26.3
+toolchain go1.26.4
require (
- github.com/apricote/hcloud-upload-image/hcloudimages v1.4.0
+ github.com/apricote/hcloud-upload-image/hcloudimages/v2 v2.0.1
github.com/hetznercloud/hcloud-go/v2 v2.40.0
github.com/spf13/cobra v1.10.2
+ github.com/spf13/pflag v1.0.9
)
require (
@@ -21,7 +22,6 @@
github.com/prometheus/common v0.66.1 // indirect
github.com/prometheus/procfs v0.16.1 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
- github.com/spf13/pflag v1.0.9 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.51.0 // indirect
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/hcloud-upload-image-1.4.0/go.sum
new/hcloud-upload-image-1.5.0/go.sum
--- old/hcloud-upload-image-1.4.0/go.sum 2026-05-14 20:52:38.000000000
+0200
+++ new/hcloud-upload-image-1.5.0/go.sum 2026-06-21 17:36:47.000000000
+0200
@@ -1,5 +1,5 @@
-github.com/apricote/hcloud-upload-image/hcloudimages v1.4.0
h1:h9cxSQo8uYn7y5zLEOQF/GjLuXAqU9eVHt6vY/XIM1Y=
-github.com/apricote/hcloud-upload-image/hcloudimages v1.4.0/go.mod
h1:9452aAvFQ28wGKZOMWS6djWvoQwqhbfUg6I2zEQIUXI=
+github.com/apricote/hcloud-upload-image/hcloudimages/v2 v2.0.1
h1:IX6IfhGIfA7gw51KlyBCU25dJuDEWIAtkWM9/F8aHC4=
+github.com/apricote/hcloud-upload-image/hcloudimages/v2 v2.0.1/go.mod
h1:OBnyHwXlvUFZPqptHRGjDTPM0DWHOOhym2RVp8X+q5s=
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=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/hcloud-upload-image-1.4.0/hcloudimages/CHANGELOG.md
new/hcloud-upload-image-1.5.0/hcloudimages/CHANGELOG.md
--- old/hcloud-upload-image-1.4.0/hcloudimages/CHANGELOG.md 2026-05-14
20:52:38.000000000 +0200
+++ new/hcloud-upload-image-1.5.0/hcloudimages/CHANGELOG.md 2026-06-21
17:36:47.000000000 +0200
@@ -1,5 +1,23 @@
# Changelog
+##
[2.0.1](https://github.com/apricote/hcloud-upload-image/compare/hcloudimages/v2.0.0...hcloudimages/v2.0.1)
(2026-06-17)
+
+
+### Bug Fixes
+
+* update hcloudimages module path for v2
([#184](https://github.com/apricote/hcloud-upload-image/issues/184))
([f4fd485](https://github.com/apricote/hcloud-upload-image/commit/f4fd4854492fd53221293cd828db16305c5accf5))
+
+##
[2.0.0](https://github.com/apricote/hcloud-upload-image/compare/hcloudimages/v1.4.0...hcloudimages/v2.0.0)
(2026-06-17)
+
+
+### ⚠ BREAKING CHANGES
+
+* new write-to-disk command
([#178](https://github.com/apricote/hcloud-upload-image/issues/178))
+
+### Features
+
+* new write-to-disk command
([#178](https://github.com/apricote/hcloud-upload-image/issues/178))
([d65441f](https://github.com/apricote/hcloud-upload-image/commit/d65441fc41e377e4a51c4b817383eb7167264c97)),
closes [#157](https://github.com/apricote/hcloud-upload-image/issues/157)
+
##
[1.4.0](https://github.com/apricote/hcloud-upload-image/compare/hcloudimages/v1.3.0...hcloudimages/v1.4.0)
(2026-05-14)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/hcloud-upload-image-1.4.0/hcloudimages/client.go
new/hcloud-upload-image-1.5.0/hcloudimages/client.go
--- old/hcloud-upload-image-1.4.0/hcloudimages/client.go 2026-05-14
20:52:38.000000000 +0200
+++ new/hcloud-upload-image-1.5.0/hcloudimages/client.go 2026-06-21
17:36:47.000000000 +0200
@@ -13,12 +13,12 @@
"github.com/hetznercloud/hcloud-go/v2/hcloud/exp/kit/sshutil"
"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/sshsession"
+ "github.com/apricote/hcloud-upload-image/hcloudimages/v2/contextlogger"
+
"github.com/apricote/hcloud-upload-image/hcloudimages/v2/internal/actionutil"
+
"github.com/apricote/hcloud-upload-image/hcloudimages/v2/internal/control"
+
"github.com/apricote/hcloud-upload-image/hcloudimages/v2/internal/labelutil"
+
"github.com/apricote/hcloud-upload-image/hcloudimages/v2/internal/randomid"
+
"github.com/apricote/hcloud-upload-image/hcloudimages/v2/internal/sshsession"
)
const (
@@ -49,7 +49,7 @@
rescueSystemRootDiskSizeMB int64 = 960
)
-type UploadOptions struct {
+type WriteOptions struct {
// ImageURL must be publicly available. The instance will download the
image from this endpoint.
ImageURL *url.URL
@@ -65,9 +65,12 @@
// 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
+ // Server the image is written to.
+ Server *hcloud.Server
+}
+
+type UploadOptions struct {
+ WriteOptions
// Architecture should match the architecture of the Image. This
decides if the Snapshot can later be
// used with [hcloud.ArchitectureX86] or [hcloud.ArchitectureARM]
servers.
@@ -122,7 +125,7 @@
// 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
+ // qcow2 to raw, requires a file as an input. If
[WriteOptions.ImageSize] is set and FormatQCOW2 is used, there is a
// warning message displayed if there is a high probability of issues.
FormatQCOW2 Format = "qcow2"
)
@@ -138,46 +141,58 @@
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.
+// WriteToDisk writes the specified image onto the root disk of an existing
server on Hetzner Cloud.
//
-// 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) {
+// The server will be rebooted multiple times and any existing data is lost.
+func (s *Client) WriteToDisk(ctx context.Context, options WriteOptions) error {
+ id, err := randomid.Generate()
+ if err != nil {
+ return err
+ }
+
logger := contextlogger.From(ctx).With(
"library", "hcloudimages",
- "method", "upload",
+ "method", "write",
+ "run-id", id,
)
+ ctx = contextlogger.New(ctx, logger)
- id, err := randomid.Generate()
+ resourceName := resourcePrefix + id
+
+ // 1. Create SSH Key
+ key, privateKey, keyCleanup, err := s.generateSSHKey(ctx, 1,
resourceName, DefaultLabels)
if err != nil {
- return nil, err
+ return 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)
+ defer keyCleanup(false)
- // 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),
- )
- }
+ // 2. Power off Server
+ logger.InfoContext(ctx, "# Step 2: Shutting down server")
+ powerOffAction, _, err := s.c.Server.Poweroff(ctx, options.Server)
+ if err != nil {
+ return fmt.Errorf("stopping the server failed: %w", err)
}
+ logger.DebugContext(ctx, "power off requested, waiting on action")
+
+ err = s.c.Action.WaitFor(ctx, powerOffAction)
+ if err != nil {
+ return fmt.Errorf("stopping the server failed: %w", err)
+ }
+ logger.DebugContext(ctx, "action finished, server is powered off")
+
+ // 3-8
+ return s.write(ctx, options, 3, key, privateKey)
+}
+
+func (s *Client) generateSSHKey(ctx context.Context, step int, resourceName
string, labels map[string]string) (*hcloud.SSHKey, []byte, func(bool), error) {
+ logger := contextlogger.From(ctx)
+
// 1. Create SSH Key
- logger.InfoContext(ctx, "# Step 1: Generating SSH Key")
+ logger.InfoContext(ctx, fmt.Sprintf("# Step %d: Generating SSH Key",
step))
privateKey, publicKey, err := sshutil.GenerateKeyPair()
if err != nil {
- return nil, fmt.Errorf("failed to generate temporary ssh key
pair: %w", err)
+ return nil, nil, nil, fmt.Errorf("failed to generate temporary
ssh key pair: %w", err)
}
key, _, err := s.c.SSHKey.Create(ctx, hcloud.SSHKeyCreateOpts{
@@ -186,12 +201,12 @@
Labels: labels,
})
if err != nil {
- return nil, fmt.Errorf("failed to submit temporary ssh key to
API: %w", err)
+ return nil, nil, 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() {
+ return key, privateKey, func(skipCleanup bool) {
// Cleanup SSH Key
- if options.DebugSkipResourceCleanup {
+ if skipCleanup {
logger.InfoContext(ctx, "Cleanup: Skipping cleanup of
temporary ssh key")
return
}
@@ -203,112 +218,64 @@
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)
- }
- }
-
- location := defaultLocation
- if options.Location != nil {
- location = options.Location
- }
-
- logger.DebugContext(ctx, "creating server with config",
- "image", defaultImage.Name,
- "location", location.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: location,
- 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
- }
+ }, nil
+}
- logger.InfoContext(ctx, "Cleanup: Deleting temporary server")
+// write is the internal utility function to actually write the image to a
root disk. It expects a powered off server and returns a powered off server.
+func (s *Client) write(ctx context.Context, options WriteOptions, initialStep
int, key *hcloud.SSHKey, privateKey []byte) error {
+ logger := contextlogger.From(ctx)
- _, _, err := s.c.Server.DeleteWithResult(ctx, server)
- if err != nil {
- logger.WarnContext(ctx, "Cleanup: server could not be
deleted", "error", err)
+ // 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),
+ )
}
- }()
+ }
// 3. Activate Rescue System
- logger.InfoContext(ctx, "# Step 3: Activating Rescue System")
- enableRescueResult, _, err := s.c.Server.EnableRescue(ctx, server,
hcloud.ServerEnableRescueOpts{
+ logger.InfoContext(ctx, fmt.Sprintf("# Step %d: Activating Rescue
System", initialStep+0))
+ enableRescueResult, _, err := s.c.Server.EnableRescue(ctx,
options.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)
+ return 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)
+ return 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)
+ logger.InfoContext(ctx, fmt.Sprintf("# Step %d: Booting Server",
initialStep+1))
+ powerOnAction, _, err := s.c.Server.Poweron(ctx, options.Server)
if err != nil {
- return nil, fmt.Errorf("starting the temporary server failed:
%w", err)
+ return 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)
+ return 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")
+ logger.InfoContext(ctx, fmt.Sprintf("# Step %d: Opening SSH
Connection", initialStep+2))
signer, err := ssh.ParsePrivateKey(privateKey)
if err != nil {
- return nil, fmt.Errorf("parsing the automatically generated
temporary private key failed: %w", err)
+ return fmt.Errorf("parsing the automatically generated
temporary private key failed: %w", err)
}
sshClientConfig := &ssh.ClientConfig{
@@ -329,53 +296,158 @@
100, // ~ 3 minutes
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)
+ logger.DebugContext(ctx, "trying to connect to server",
"ip", options.Server.PublicNet.IPv4.IP)
+ sshClient, err = ssh.Dial("tcp",
options.Server.PublicNet.IPv4.IP.String()+":ssh", sshClientConfig)
return err
},
)
if err != nil {
- return nil, fmt.Errorf("failed to ssh into temporary server:
%w", err)
+ return fmt.Errorf("failed to ssh into temporary server: %w",
err)
}
defer func() { _ = sshClient.Close() }()
// 6. Wipe existing disk, to avoid storing any bytes from it in the
snapshot
- logger.InfoContext(ctx, "# Step 6: Cleaning existing disk")
+ logger.InfoContext(ctx, fmt.Sprintf("# Step %d: Cleaning existing
disk", initialStep+3))
- output, err := sshsession.Run(sshClient, "blkdiscard /dev/sda", nil)
+ output, err := sshsession.Run(sshClient, "blkdiscard --force /dev/sda",
nil)
logger.DebugContext(ctx, string(output))
if err != nil {
- return nil, fmt.Errorf("failed to clean existing disk: %w", err)
+ return fmt.Errorf("failed to clean existing disk: %w", err)
}
// 7. SSH On Server: Download Image, Decompress, Write to Root Disk
- logger.InfoContext(ctx, "# Step 7: Downloading image and writing to
disk")
+ logger.InfoContext(ctx, fmt.Sprintf("# Step %d: Downloading image and
writing to disk", initialStep+4))
cmd, err := assembleCommand(options)
if err != nil {
- return nil, err
+ return err
}
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")
+ logger.InfoContext(ctx, fmt.Sprintf("# Step %d: Finished writing image
to disk", initialStep+4))
logger.DebugContext(ctx, string(output))
if err != nil {
- return nil, fmt.Errorf("failed to download and write the image:
%w", err)
+ return fmt.Errorf("failed to download and write the image: %w",
err)
}
// 8. SSH On Server: Shutdown
- logger.InfoContext(ctx, "# Step 8: Shutting down server")
+ logger.InfoContext(ctx, fmt.Sprintf("# Step %d: Shutting down server",
initialStep+5))
_, 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)
}
+ return nil
+}
+
+// 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) {
+ id, err := randomid.Generate()
+ if err != nil {
+ return nil, err
+ }
+
+ logger := contextlogger.From(ctx).With(
+ "library", "hcloudimages",
+ "method", "upload",
+ "run-id", id,
+ )
+ ctx = contextlogger.New(ctx, logger)
+
+ // For simplicity, we use the same random name for SSH Key + Server
+ resourceName := resourcePrefix + id
+ labels := labelutil.Merge(DefaultLabels, options.Labels)
+
+ key, privateKey, keyCleanup, err := s.generateSSHKey(ctx, 1,
resourceName, labels)
+ if err != nil {
+ return nil, err
+ }
+ defer keyCleanup(options.DebugSkipResourceCleanup)
+
+ // 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)
+ }
+ }
+
+ location := defaultLocation
+ if options.Location != nil {
+ location = options.Location
+ }
+
+ logger.DebugContext(ctx, "creating server with config",
+ "image", defaultImage.Name,
+ "location", location.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: location,
+ 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")
+
+ options.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, options.Server)
+ if err != nil {
+ logger.WarnContext(ctx, "Cleanup: server could not be
deleted", "error", err)
+ }
+ }()
+
+ // Steps 3-8
+ err = s.write(ctx, options.WriteOptions, 3, key, privateKey)
+ if err != nil {
+ return nil, err
+ }
+
// 9. Create Image from Server
logger.InfoContext(ctx, "# Step 9: Creating Image")
- createImageResult, _, err := s.c.Server.CreateImage(ctx, server,
&hcloud.ServerCreateImageOpts{
+ createImageResult, _, err := s.c.Server.CreateImage(ctx,
options.Server, &hcloud.ServerCreateImageOpts{
Type: hcloud.ImageTypeSnapshot,
Description: options.Description,
Labels: labels,
@@ -520,7 +592,7 @@
return nil
}
-func assembleCommand(options UploadOptions) (string, error) {
+func assembleCommand(options WriteOptions) (string, error) {
// Make sure that we fail early, ie. if the image url does not work
cmd := "set -euo pipefail && "
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/hcloud-upload-image-1.4.0/hcloudimages/client_test.go
new/hcloud-upload-image-1.5.0/hcloudimages/client_test.go
--- old/hcloud-upload-image-1.4.0/hcloudimages/client_test.go 2026-05-14
20:52:38.000000000 +0200
+++ new/hcloud-upload-image-1.5.0/hcloudimages/client_test.go 2026-06-21
17:36:47.000000000 +0200
@@ -17,32 +17,32 @@
func TestAssembleCommand(t *testing.T) {
tests := []struct {
name string
- options UploadOptions
+ options WriteOptions
want string
wantErr bool
}{
{
name: "local raw",
- options: UploadOptions{},
+ options: WriteOptions{},
want: "bash -c 'set -euo pipefail && dd of=/dev/sda
bs=4M conv=sparse && sync'",
},
{
name: "remote raw",
- options: UploadOptions{
+ options: WriteOptions{
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 conv=sparse &&
sync'",
},
{
name: "local xz",
- options: UploadOptions{
+ options: WriteOptions{
ImageCompression: CompressionXZ,
},
want: "bash -c 'set -euo pipefail && xz -cd | dd
of=/dev/sda bs=4M conv=sparse && sync'",
},
{
name: "remote xz",
- options: UploadOptions{
+ options: WriteOptions{
ImageURL:
mustParseURL("https://example.com/image.xz"),
ImageCompression: CompressionXZ,
},
@@ -50,14 +50,14 @@
},
{
name: "local zstd",
- options: UploadOptions{
+ options: WriteOptions{
ImageCompression: CompressionZSTD,
},
want: "bash -c 'set -euo pipefail && zstd -cd | dd
of=/dev/sda bs=4M conv=sparse && sync'",
},
{
name: "remote zstd",
- options: UploadOptions{
+ options: WriteOptions{
ImageURL:
mustParseURL("https://example.com/image.zst"),
ImageCompression: CompressionZSTD,
},
@@ -65,14 +65,14 @@
},
{
name: "local bz2",
- options: UploadOptions{
+ options: WriteOptions{
ImageCompression: CompressionBZ2,
},
want: "bash -c 'set -euo pipefail && bzip2 -cd | dd
of=/dev/sda bs=4M conv=sparse && sync'",
},
{
name: "remote bz2",
- options: UploadOptions{
+ options: WriteOptions{
ImageURL:
mustParseURL("https://example.com/image.bz2"),
ImageCompression: CompressionBZ2,
},
@@ -80,14 +80,14 @@
},
{
name: "local qcow2",
- options: UploadOptions{
+ options: WriteOptions{
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{
+ options: WriteOptions{
ImageURL:
mustParseURL("https://example.com/image.qcow2"),
ImageFormat: FormatQCOW2,
},
@@ -96,7 +96,7 @@
{
name: "unknown compression",
- options: UploadOptions{
+ options: WriteOptions{
ImageCompression: "noodle",
},
wantErr: true,
@@ -104,7 +104,7 @@
{
name: "unknown format",
- options: UploadOptions{
+ options: WriteOptions{
ImageFormat: "poodle",
},
wantErr: true,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/hcloud-upload-image-1.4.0/hcloudimages/doc.go
new/hcloud-upload-image-1.5.0/hcloudimages/doc.go
--- old/hcloud-upload-image-1.4.0/hcloudimages/doc.go 2026-05-14
20:52:38.000000000 +0200
+++ new/hcloud-upload-image-1.5.0/hcloudimages/doc.go 2026-06-21
17:36:47.000000000 +0200
@@ -36,7 +36,7 @@
//
// 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].
+// [context.Context] through
[github.com/apricote/hcloud-upload-image/hcloudimages/v2/contextlogger.New].
//
// [Hetzner Cloud website]: https://www.hetzner.com/cloud/
package hcloudimages
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/hcloud-upload-image-1.4.0/hcloudimages/doc_test.go
new/hcloud-upload-image-1.5.0/hcloudimages/doc_test.go
--- old/hcloud-upload-image-1.4.0/hcloudimages/doc_test.go 2026-05-14
20:52:38.000000000 +0200
+++ new/hcloud-upload-image-1.5.0/hcloudimages/doc_test.go 2026-06-21
17:36:47.000000000 +0200
@@ -7,7 +7,7 @@
"github.com/hetznercloud/hcloud-go/v2/hcloud"
- "github.com/apricote/hcloud-upload-image/hcloudimages"
+ "github.com/apricote/hcloud-upload-image/hcloudimages/v2"
)
func ExampleClient_Upload() {
@@ -21,10 +21,12 @@
}
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
+ WriteOptions: hcloudimages.WriteOptions{
+ ImageURL: imageURL,
+ ImageCompression: hcloudimages.CompressionBZ2,
+ },
+ Architecture: hcloud.ArchitectureX86,
+ Location: &hcloud.Location{Name: "nbg1"}, // Optional:
defaults to fsn1
})
if err != nil {
panic(err)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore' old/hcloud-upload-image-1.4.0/hcloudimages/go.mod
new/hcloud-upload-image-1.5.0/hcloudimages/go.mod
--- old/hcloud-upload-image-1.4.0/hcloudimages/go.mod 2026-05-14
20:52:38.000000000 +0200
+++ new/hcloud-upload-image-1.5.0/hcloudimages/go.mod 2026-06-21
17:36:47.000000000 +0200
@@ -1,8 +1,8 @@
-module github.com/apricote/hcloud-upload-image/hcloudimages
+module github.com/apricote/hcloud-upload-image/hcloudimages/v2
go 1.25.0
-toolchain go1.26.3
+toolchain go1.26.4
require (
github.com/hetznercloud/hcloud-go/v2 v2.40.0
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/hcloud-upload-image-1.4.0/hcloudimages/internal/control/retry.go
new/hcloud-upload-image-1.5.0/hcloudimages/internal/control/retry.go
--- old/hcloud-upload-image-1.4.0/hcloudimages/internal/control/retry.go
2026-05-14 20:52:38.000000000 +0200
+++ new/hcloud-upload-image-1.5.0/hcloudimages/internal/control/retry.go
2026-06-21 17:36:47.000000000 +0200
@@ -10,7 +10,7 @@
"github.com/hetznercloud/hcloud-go/v2/hcloud"
- "github.com/apricote/hcloud-upload-image/hcloudimages/contextlogger"
+ "github.com/apricote/hcloud-upload-image/hcloudimages/v2/contextlogger"
)
// Retry executes f at most maxTries times.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn'
'--exclude=.svnignore'
old/hcloud-upload-image-1.4.0/internal/version/version.go
new/hcloud-upload-image-1.5.0/internal/version/version.go
--- old/hcloud-upload-image-1.4.0/internal/version/version.go 2026-05-14
20:52:38.000000000 +0200
+++ new/hcloud-upload-image-1.5.0/internal/version/version.go 2026-06-21
17:36:47.000000000 +0200
@@ -2,7 +2,7 @@
var (
// version is a semver version (https://semver.org).
- version = "1.4.0" // x-release-please-version
+ version = "1.5.0" // x-release-please-version
// versionPrerelease is a semver version pre-release identifier
(https://semver.org).
//
++++++ hcloud-upload-image.obsinfo ++++++
--- /var/tmp/diff_new_pack.MYCqvo/_old 2026-06-22 17:35:31.642547656 +0200
+++ /var/tmp/diff_new_pack.MYCqvo/_new 2026-06-22 17:35:31.646547797 +0200
@@ -1,5 +1,5 @@
name: hcloud-upload-image
-version: 1.4.0
-mtime: 1778784758
-commit: 73af6a8a750a37d062bbb8e8dd4d09f17274f5a3
+version: 1.5.0
+mtime: 1782056207
+commit: 97083898f202471b7f7b96fa9417c1c0f1c025fc
++++++ vendor.tar.gz ++++++
++++ 1920 lines of diff (skipped)