feat: optionally decode hcloud userdata as base64

When fetching the machine configuration in the hcloud platform implementation,
try to decode the data returned from the 'userdata' endpoint as a base64 string.
If the data is not in base64 format, decoding does not succeed and the unmodified data is used.

Signed-off-by: Philipp Kleber <philipp.t.kleber@gmail.com>
Signed-off-by: Andrey Smirnov <andrey.smirnov@siderolabs.com>
(cherry picked from commit ccbd5aed39b360664d1f80c8b146050e9df9ff7b)
This commit is contained in:
Philipp Kleber 2024-10-07 21:19:46 +02:00 committed by Andrey Smirnov
parent f20a6900db
commit ce47912518
No known key found for this signature in database
GPG Key ID: FE042E3D4085A811
5 changed files with 169 additions and 1 deletions

View File

@ -0,0 +1,10 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package hcloud
// MaybeBase64Decode is exported for testing.
func MaybeBase64Decode(data []byte) []byte {
return maybeBase64Decode(data)
}

View File

@ -7,6 +7,7 @@ package hcloud
import (
"context"
"encoding/base64"
"fmt"
"log"
"net/netip"
@ -156,9 +157,27 @@ func (h *Hcloud) Configuration(ctx context.Context, r state.State) ([]byte, erro
log.Printf("fetching machine config from: %q", HCloudUserDataEndpoint)
return download.Download(ctx, HCloudUserDataEndpoint,
configBytes, err := download.Download(ctx, HCloudUserDataEndpoint,
download.WithErrorOnNotFound(errors.ErrNoConfigSource),
download.WithErrorOnEmptyResponse(errors.ErrNoConfigSource))
if err != nil {
return nil, err
}
// Try to parse the downloaded config bytes as base64 string, so that users can provide the config in base64 format.
// This also allows users to gzip this data, since the calling code will try to un-gzip the data if it detects it.
return maybeBase64Decode(configBytes), nil
}
// maybeBase64Decode tries to interpret the provided bytes as base64 string and decode them.
// If the provided bytes are not a valid base64 string, the original bytes are returned.
func maybeBase64Decode(data []byte) []byte {
out, err := base64.StdEncoding.AppendDecode(nil, data)
if err != nil {
return data
}
return out
}
// Mode implements the runtime.Platform interface.

View File

@ -44,3 +44,17 @@ func TestParseMetadata(t *testing.T) {
assert.Equal(t, expectedNetworkConfig, string(marshaled))
}
//go:embed testdata/userdata-plain.yaml
var userdataPlain []byte
//go:embed testdata/userdata-base64.txt
var userdataBase64 []byte
func TestParseUserdata(t *testing.T) {
decodedUserdataPlain := hcloud.MaybeBase64Decode(userdataPlain)
decodedUserdataBase64 := hcloud.MaybeBase64Decode(userdataBase64)
assert.Equal(t, decodedUserdataPlain, decodedUserdataBase64)
assert.Equal(t, userdataPlain, decodedUserdataBase64)
}

View File

@ -0,0 +1 @@
dmVyc2lvbjogdjFhbHBoYTEKZGVidWc6IGZhbHNlCnBlcnNpc3Q6IHRydWUKbWFjaGluZToKICB0eXBlOiBjb250cm9scGxhbmUKICBjZXJ0U0FOczoKICAgIC0gMTAuMC4xLjEwMQogICAgLSAxMC4wLjEuMTAwCiAga3ViZWxldDoKICAgIGltYWdlOiBnaGNyLmlvL3NpZGVyb2xhYnMva3ViZWxldDp2MS4zMS4xCiAgICBleHRyYUFyZ3M6CiAgICAgIGNsb3VkLXByb3ZpZGVyOiBleHRlcm5hbAogICAgICByb3RhdGUtc2VydmVyLWNlcnRpZmljYXRlczogInRydWUiCiAgICBkZWZhdWx0UnVudGltZVNlY2NvbXBQcm9maWxlRW5hYmxlZDogdHJ1ZQogICAgbm9kZUlQOgogICAgICB2YWxpZFN1Ym5ldHM6CiAgICAgICAgLSAxMC4wLjEuMC8yNAogICAgZGlzYWJsZU1hbmlmZXN0c0RpcmVjdG9yeTogdHJ1ZQogIG5ldHdvcms6CiAgICBob3N0bmFtZTogY29udHJvbHBsYW5lLTAwMQogICAgaW50ZXJmYWNlczoKICAgICAgLSBpbnRlcmZhY2U6IGV0aDAKICAgICAgICBkaGNwOiB0cnVlCiAgICBrdWJlc3BhbjoKICAgICAgZW5hYmxlZDogZmFsc2UKICBpbnN0YWxsOgogICAgZGlzazogL2Rldi9zZGEKICAgIGV4dHJhS2VybmVsQXJnczoKICAgICAgLSBpcHY2LmRpc2FibGU9MQogICAgaW1hZ2U6IGdoY3IuaW8vc2lkZXJvbGFicy9pbnN0YWxsZXI6djEuOC4wCiAgICB3aXBlOiBmYWxzZQogIHN5c2N0bHM6CiAgICBuZXQuY29yZS5uZXRkZXZfbWF4X2JhY2tsb2c6ICI0MDk2IgogICAgbmV0LmNvcmUuc29tYXhjb25uOiAiNjU1MzUiCiAgZmVhdHVyZXM6CiAgICByYmFjOiB0cnVlCiAgICBzdGFibGVIb3N0bmFtZTogdHJ1ZQogICAga3ViZXJuZXRlc1RhbG9zQVBJQWNjZXNzOgogICAgICBlbmFibGVkOiB0cnVlCiAgICAgIGFsbG93ZWRSb2xlczoKICAgICAgICAtIG9zOnJlYWRlcgogICAgICBhbGxvd2VkS3ViZXJuZXRlc05hbWVzcGFjZXM6CiAgICAgICAgLSBrdWJlLXN5c3RlbQogICAgYXBpZENoZWNrRXh0S2V5VXNhZ2U6IHRydWUKICAgIGRpc2tRdW90YVN1cHBvcnQ6IHRydWUKICAgIGt1YmVQcmlzbToKICAgICAgZW5hYmxlZDogdHJ1ZQogICAgICBwb3J0OiA3NDQ1CiAgICBob3N0RE5TOgogICAgICBlbmFibGVkOiB0cnVlCiAgICAgIGZvcndhcmRLdWJlRE5TVG9Ib3N0OiB0cnVlCiAgICAgIHJlc29sdmVNZW1iZXJOYW1lczogdHJ1ZQogIGtlcm5lbDoge30KICBub2RlTGFiZWxzOgogICAgbm9kZS5rdWJlcm5ldGVzLmlvL2V4Y2x1ZGUtZnJvbS1leHRlcm5hbC1sb2FkLWJhbGFuY2VyczogIiIKY2x1c3RlcjoKICBjb250cm9sUGxhbmU6CiAgICBlbmRwb2ludDogaHR0cHM6Ly8xMC4wLjEuMTAwOjY0NDMKICBjbHVzdGVyTmFtZTogdGVzdC1jbHVzdGVyCiAgbmV0d29yazoKICAgIGNuaToKICAgICAgbmFtZTogbm9uZQogICAgZG5zRG9tYWluOiBjbHVzdGVyLmxvY2FsCiAgICBwb2RTdWJuZXRzOgogICAgICAtIDEwLjAuMTYuMC8yMAogICAgc2VydmljZVN1Ym5ldHM6CiAgICAgIC0gMTAuMC44LjAvMjEKICBhcGlTZXJ2ZXI6CiAgICBpbWFnZTogcmVnaXN0cnkuazhzLmlvL2t1YmUtYXBpc2VydmVyOnYxLjMxLjEKICAgIGNlcnRTQU5zOgogICAgICAtIDEwLjAuMS4xMDAKICAgICAgLSAxMC4wLjEuMTAxCiAgICAgIC0gMTAuMC4xLjEwMAogICAgZGlzYWJsZVBvZFNlY3VyaXR5UG9saWN5OiB0cnVlCiAgICBhZG1pc3Npb25Db250cm9sOgogICAgICAtIG5hbWU6IFBvZFNlY3VyaXR5CiAgICAgICAgY29uZmlndXJhdGlvbjoKICAgICAgICAgIGFwaVZlcnNpb246IHBvZC1zZWN1cml0eS5hZG1pc3Npb24uY29uZmlnLms4cy5pby92MWFscGhhMQogICAgICAgICAgZGVmYXVsdHM6CiAgICAgICAgICAgIGF1ZGl0OiByZXN0cmljdGVkCiAgICAgICAgICAgIGF1ZGl0LXZlcnNpb246IGxhdGVzdAogICAgICAgICAgICBlbmZvcmNlOiBiYXNlbGluZQogICAgICAgICAgICBlbmZvcmNlLXZlcnNpb246IGxhdGVzdAogICAgICAgICAgICB3YXJuOiByZXN0cmljdGVkCiAgICAgICAgICAgIHdhcm4tdmVyc2lvbjogbGF0ZXN0CiAgICAgICAgICBleGVtcHRpb25zOgogICAgICAgICAgICBuYW1lc3BhY2VzOgogICAgICAgICAgICAgIC0ga3ViZS1zeXN0ZW0KICAgICAgICAgICAgcnVudGltZUNsYXNzZXM6IFtdCiAgICAgICAgICAgIHVzZXJuYW1lczogW10KICAgICAgICAgIGtpbmQ6IFBvZFNlY3VyaXR5Q29uZmlndXJhdGlvbgogICAgYXVkaXRQb2xpY3k6CiAgICAgIGFwaVZlcnNpb246IGF1ZGl0Lms4cy5pby92MQogICAgICBraW5kOiBQb2xpY3kKICAgICAgcnVsZXM6CiAgICAgICAgLSBsZXZlbDogTWV0YWRhdGEKICBjb250cm9sbGVyTWFuYWdlcjoKICAgIGltYWdlOiByZWdpc3RyeS5rOHMuaW8va3ViZS1jb250cm9sbGVyLW1hbmFnZXI6djEuMzEuMQogICAgZXh0cmFBcmdzOgogICAgICBiaW5kLWFkZHJlc3M6IDAuMC4wLjAKICAgICAgY2xvdWQtcHJvdmlkZXI6IGV4dGVybmFsCiAgICAgIG5vZGUtY2lkci1tYXNrLXNpemUtaXB2NDogIjI0IgogIHByb3h5OgogICAgZGlzYWJsZWQ6IHRydWUKICAgIGltYWdlOiByZWdpc3RyeS5rOHMuaW8va3ViZS1wcm94eTp2MS4zMS4xCiAgc2NoZWR1bGVyOgogICAgaW1hZ2U6IHJlZ2lzdHJ5Lms4cy5pby9rdWJlLXNjaGVkdWxlcjp2MS4zMS4xCiAgICBleHRyYUFyZ3M6CiAgICAgIGJpbmQtYWRkcmVzczogMC4wLjAuMAogIGRpc2NvdmVyeToKICAgIGVuYWJsZWQ6IHRydWUKICAgIHJlZ2lzdHJpZXM6CiAgICAgIGt1YmVybmV0ZXM6CiAgICAgICAgZGlzYWJsZWQ6IHRydWUKICAgICAgc2VydmljZToge30KICBldGNkOgogICAgZXh0cmFBcmdzOgogICAgICBsaXN0ZW4tbWV0cmljcy11cmxzOiBodHRwOi8vMC4wLjAuMDoyMzgxCiAgICBhZHZlcnRpc2VkU3VibmV0czoKICAgICAgLSAxMC4wLjEuMC8yNAogIGNvcmVETlM6CiAgICBkaXNhYmxlZDogZmFsc2UKICBleHRlcm5hbENsb3VkUHJvdmlkZXI6CiAgICBlbmFibGVkOiB0cnVlCg==

View File

@ -0,0 +1,124 @@
version: v1alpha1
debug: false
persist: true
machine:
type: controlplane
certSANs:
- 10.0.1.101
- 10.0.1.100
kubelet:
image: ghcr.io/siderolabs/kubelet:v1.31.1
extraArgs:
cloud-provider: external
rotate-server-certificates: "true"
defaultRuntimeSeccompProfileEnabled: true
nodeIP:
validSubnets:
- 10.0.1.0/24
disableManifestsDirectory: true
network:
hostname: controlplane-001
interfaces:
- interface: eth0
dhcp: true
kubespan:
enabled: false
install:
disk: /dev/sda
extraKernelArgs:
- ipv6.disable=1
image: ghcr.io/siderolabs/installer:v1.8.0
wipe: false
sysctls:
net.core.netdev_max_backlog: "4096"
net.core.somaxconn: "65535"
features:
rbac: true
stableHostname: true
kubernetesTalosAPIAccess:
enabled: true
allowedRoles:
- os:reader
allowedKubernetesNamespaces:
- kube-system
apidCheckExtKeyUsage: true
diskQuotaSupport: true
kubePrism:
enabled: true
port: 7445
hostDNS:
enabled: true
forwardKubeDNSToHost: true
resolveMemberNames: true
kernel: {}
nodeLabels:
node.kubernetes.io/exclude-from-external-load-balancers: ""
cluster:
controlPlane:
endpoint: https://10.0.1.100:6443
clusterName: test-cluster
network:
cni:
name: none
dnsDomain: cluster.local
podSubnets:
- 10.0.16.0/20
serviceSubnets:
- 10.0.8.0/21
apiServer:
image: registry.k8s.io/kube-apiserver:v1.31.1
certSANs:
- 10.0.1.100
- 10.0.1.101
- 10.0.1.100
disablePodSecurityPolicy: true
admissionControl:
- name: PodSecurity
configuration:
apiVersion: pod-security.admission.config.k8s.io/v1alpha1
defaults:
audit: restricted
audit-version: latest
enforce: baseline
enforce-version: latest
warn: restricted
warn-version: latest
exemptions:
namespaces:
- kube-system
runtimeClasses: []
usernames: []
kind: PodSecurityConfiguration
auditPolicy:
apiVersion: audit.k8s.io/v1
kind: Policy
rules:
- level: Metadata
controllerManager:
image: registry.k8s.io/kube-controller-manager:v1.31.1
extraArgs:
bind-address: 0.0.0.0
cloud-provider: external
node-cidr-mask-size-ipv4: "24"
proxy:
disabled: true
image: registry.k8s.io/kube-proxy:v1.31.1
scheduler:
image: registry.k8s.io/kube-scheduler:v1.31.1
extraArgs:
bind-address: 0.0.0.0
discovery:
enabled: true
registries:
kubernetes:
disabled: true
service: {}
etcd:
extraArgs:
listen-metrics-urls: http://0.0.0.0:2381
advertisedSubnets:
- 10.0.1.0/24
coreDNS:
disabled: false
externalCloudProvider:
enabled: true