Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions pkg/svc/provider/hetzner/labels.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,12 +34,28 @@ const (
LabelTalosVersion = "ksail.io/talos-version"

// LabelTalosSchematic identifies the Talos factory schematic ID of the snapshot image.
// The stored value is truncated to maxLabelValueLen characters via SchematicLabelValue.
LabelTalosSchematic = "ksail.io/talos-schematic"

// LabelTalosCluster identifies which cluster created the snapshot image.
LabelTalosCluster = "ksail.io/cluster"

// maxLabelValueLen is the maximum length of a Hetzner Cloud label value.
// See https://github.com/hetznercloud/hcloud-go/blob/main/hcloud/label.go.
maxLabelValueLen = 63
)

// SchematicLabelValue returns the schematic ID truncated to fit within
// Hetzner Cloud's 63-character label value limit. Talos factory schematic
// IDs are SHA256 hex digests (64 chars), which exceed the limit by one.
func SchematicLabelValue(schematicID string) string {
if len(schematicID) > maxLabelValueLen {
return schematicID[:maxLabelValueLen]
}

return schematicID
}

// Node type values for LabelNodeType.
const (
// NodeTypeControlPlane indicates a control-plane node.
Expand Down
67 changes: 67 additions & 0 deletions pkg/svc/provider/hetzner/labels_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package hetzner_test

import (
"strings"
"testing"

"github.com/devantler-tech/ksail/v7/pkg/svc/provider/hetzner"
"github.com/stretchr/testify/assert"
)

func TestSchematicLabelValue(t *testing.T) {
t.Parallel()

tests := []struct {
name string
input string
wantLen int
wantFull bool // whether the output should equal the input
}{
{
name: "short value unchanged",
input: "abc123",
wantLen: 6,
wantFull: true,
},
{
name: "63-char value unchanged",
input: strings.Repeat("a", 63),
wantLen: 63,
wantFull: true,
},
{
name: "64-char SHA256 truncated to 63",
input: "e187c9b90f773cd8c84e5a3265c5554ee787b2fe67b508d9f955e90e7ae8c96c",
wantLen: 63,
wantFull: false,
},
{
name: "longer value truncated to 63",
input: strings.Repeat("b", 100),
wantLen: 63,
wantFull: false,
},
{
name: "empty value unchanged",
input: "",
wantLen: 0,
wantFull: true,
},
}

for _, testCase := range tests {
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()

got := hetzner.SchematicLabelValue(testCase.input)

assert.Len(t, got, testCase.wantLen)

if testCase.wantFull {
assert.Equal(t, testCase.input, got)
} else {
assert.Equal(t, testCase.input[:63], got)
}
})
}
}
4 changes: 2 additions & 2 deletions pkg/svc/provider/hetzner/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ func (sm *SnapshotManager) EnsureTalosSnapshot(
Architecture: hcloud.ArchitectureX86,
Labels: map[string]string{
LabelTalosVersion: talosVersion,
LabelTalosSchematic: schematicID,
LabelTalosSchematic: SchematicLabelValue(schematicID),
LabelTalosCluster: clusterName,
},
})
Expand Down Expand Up @@ -146,7 +146,7 @@ func (sm *SnapshotManager) findExistingSnapshot(
ListOpts: hcloud.ListOpts{
LabelSelector: fmt.Sprintf("%s=%s,%s=%s,%s=%s",
LabelTalosVersion, talosVersion,
LabelTalosSchematic, schematicID,
LabelTalosSchematic, SchematicLabelValue(schematicID),
LabelTalosCluster, clusterName,
),
},
Expand Down
56 changes: 56 additions & 0 deletions pkg/svc/provider/hetzner/snapshot_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -380,3 +380,59 @@ func TestSnapshotManager_EnsureTalosSnapshot_SkipsNonAvailableSnapshot(t *testin
"Upload should have been called when existing snapshot is not available",
)
}

func TestSnapshotManager_EnsureTalosSnapshot_SHA256SchematicID(t *testing.T) {
t.Parallel()

const (
clusterName = "sha256-cluster"
version = "v1.12.4"
// A real 64-char SHA256 schematic ID that exceeds Hetzner's 63-char label limit.
schematic = "e187c9b90f773cd8c84e5a3265c5554ee787b2fe67b508d9f955e90e7ae8c96c"
imageID = int64(55)
)

truncatedSchematic := hetzner.SchematicLabelValue(schematic)

mux := http.NewServeMux()
srv := httptest.NewServer(mux)
t.Cleanup(srv.Close)

mux.HandleFunc("/images", func(responseWriter http.ResponseWriter, r *http.Request) {
responseWriter.Header().Set("Content-Type", "application/json")

// Verify the label selector uses the truncated value.
labelSelector := r.URL.Query().Get("label_selector")
assert.Contains(t, labelSelector, truncatedSchematic)
assert.NotContains(t, labelSelector, schematic)

resp := hcloudImageListResponse{
Images: []hcloudImageSchema{
{
ID: imageID,
Labels: map[string]string{
"ksail.io/talos-version": version,
"ksail.io/talos-schematic": truncatedSchematic,
"ksail.io/cluster": clusterName,
},
Type: "snapshot",
Status: "available",
},
},
}
_, _ = responseWriter.Write(marshalJSON(t, resp))
})

client := newTestHcloudClient(srv.URL)
uploader := &mockUploader{}

var logBuf bytes.Buffer

sm := hetzner.NewSnapshotManagerWithUploaderForTest(client, uploader, &logBuf)

gotID, err := sm.EnsureTalosSnapshot(context.Background(), clusterName, version, schematic)

require.NoError(t, err)
assert.Equal(t, imageID, gotID)
assert.False(t, uploader.called)
}
14 changes: 12 additions & 2 deletions pkg/svc/provisioner/cluster/talos/provisioner_hetzner.go
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,12 @@ func (p *Provisioner) ensureHcloudSecret(ctx context.Context, clusterName string
return fmt.Errorf("creating kubeclient for hcloud secret: %w", k8s.ErrKubeconfigPathEmpty)
}

kubeconfigPath, err := fsutil.EvalCanonicalPath(p.options.KubeconfigPath)
expandedPath, err := fsutil.ExpandHomePath(p.options.KubeconfigPath)
if err != nil {
return fmt.Errorf("expanding kubeconfig path for hcloud secret: %w", err)
}

kubeconfigPath, err := fsutil.EvalCanonicalPath(expandedPath)
if err != nil {
return fmt.Errorf("canonicalizing kubeconfig path for hcloud secret: %w", err)
}
Expand Down Expand Up @@ -407,7 +412,12 @@ func (p *Provisioner) ensureAutoscalerSecret(
)
}

kubeconfigPath, err := fsutil.EvalCanonicalPath(p.options.KubeconfigPath)
expandedPath, err := fsutil.ExpandHomePath(p.options.KubeconfigPath)
if err != nil {
return fmt.Errorf("expanding kubeconfig path for autoscaler secret: %w", err)
}

kubeconfigPath, err := fsutil.EvalCanonicalPath(expandedPath)
if err != nil {
return fmt.Errorf("canonicalizing kubeconfig path for autoscaler secret: %w", err)
}
Expand Down
Loading