Skip to content

Commit

Permalink
feat: optionally decode hcloud userdata as base64
Browse files Browse the repository at this point in the history
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 <[email protected]>
Signed-off-by: Andrey Smirnov <[email protected]>
  • Loading branch information
axiphi authored and smira committed Oct 16, 2024
1 parent 34f652c commit ccbd5ae
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 1 deletion.
Original file line number Diff line number Diff line change
@@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ package hcloud

import (
"context"
"encoding/base64"
"fmt"
"log"
"net/netip"
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dmVyc2lvbjogdjFhbHBoYTEKZGVidWc6IGZhbHNlCnBlcnNpc3Q6IHRydWUKbWFjaGluZToKICB0eXBlOiBjb250cm9scGxhbmUKICBjZXJ0U0FOczoKICAgIC0gMTAuMC4xLjEwMQogICAgLSAxMC4wLjEuMTAwCiAga3ViZWxldDoKICAgIGltYWdlOiBnaGNyLmlvL3NpZGVyb2xhYnMva3ViZWxldDp2MS4zMS4xCiAgICBleHRyYUFyZ3M6CiAgICAgIGNsb3VkLXByb3ZpZGVyOiBleHRlcm5hbAogICAgICByb3RhdGUtc2VydmVyLWNlcnRpZmljYXRlczogInRydWUiCiAgICBkZWZhdWx0UnVudGltZVNlY2NvbXBQcm9maWxlRW5hYmxlZDogdHJ1ZQogICAgbm9kZUlQOgogICAgICB2YWxpZFN1Ym5ldHM6CiAgICAgICAgLSAxMC4wLjEuMC8yNAogICAgZGlzYWJsZU1hbmlmZXN0c0RpcmVjdG9yeTogdHJ1ZQogIG5ldHdvcms6CiAgICBob3N0bmFtZTogY29udHJvbHBsYW5lLTAwMQogICAgaW50ZXJmYWNlczoKICAgICAgLSBpbnRlcmZhY2U6IGV0aDAKICAgICAgICBkaGNwOiB0cnVlCiAgICBrdWJlc3BhbjoKICAgICAgZW5hYmxlZDogZmFsc2UKICBpbnN0YWxsOgogICAgZGlzazogL2Rldi9zZGEKICAgIGV4dHJhS2VybmVsQXJnczoKICAgICAgLSBpcHY2LmRpc2FibGU9MQogICAgaW1hZ2U6IGdoY3IuaW8vc2lkZXJvbGFicy9pbnN0YWxsZXI6djEuOC4wCiAgICB3aXBlOiBmYWxzZQogIHN5c2N0bHM6CiAgICBuZXQuY29yZS5uZXRkZXZfbWF4X2JhY2tsb2c6ICI0MDk2IgogICAgbmV0LmNvcmUuc29tYXhjb25uOiAiNjU1MzUiCiAgZmVhdHVyZXM6CiAgICByYmFjOiB0cnVlCiAgICBzdGFibGVIb3N0bmFtZTogdHJ1ZQogICAga3ViZXJuZXRlc1RhbG9zQVBJQWNjZXNzOgogICAgICBlbmFibGVkOiB0cnVlCiAgICAgIGFsbG93ZWRSb2xlczoKICAgICAgICAtIG9zOnJlYWRlcgogICAgICBhbGxvd2VkS3ViZXJuZXRlc05hbWVzcGFjZXM6CiAgICAgICAgLSBrdWJlLXN5c3RlbQogICAgYXBpZENoZWNrRXh0S2V5VXNhZ2U6IHRydWUKICAgIGRpc2tRdW90YVN1cHBvcnQ6IHRydWUKICAgIGt1YmVQcmlzbToKICAgICAgZW5hYmxlZDogdHJ1ZQogICAgICBwb3J0OiA3NDQ1CiAgICBob3N0RE5TOgogICAgICBlbmFibGVkOiB0cnVlCiAgICAgIGZvcndhcmRLdWJlRE5TVG9Ib3N0OiB0cnVlCiAgICAgIHJlc29sdmVNZW1iZXJOYW1lczogdHJ1ZQogIGtlcm5lbDoge30KICBub2RlTGFiZWxzOgogICAgbm9kZS5rdWJlcm5ldGVzLmlvL2V4Y2x1ZGUtZnJvbS1leHRlcm5hbC1sb2FkLWJhbGFuY2VyczogIiIKY2x1c3RlcjoKICBjb250cm9sUGxhbmU6CiAgICBlbmRwb2ludDogaHR0cHM6Ly8xMC4wLjEuMTAwOjY0NDMKICBjbHVzdGVyTmFtZTogdGVzdC1jbHVzdGVyCiAgbmV0d29yazoKICAgIGNuaToKICAgICAgbmFtZTogbm9uZQogICAgZG5zRG9tYWluOiBjbHVzdGVyLmxvY2FsCiAgICBwb2RTdWJuZXRzOgogICAgICAtIDEwLjAuMTYuMC8yMAogICAgc2VydmljZVN1Ym5ldHM6CiAgICAgIC0gMTAuMC44LjAvMjEKICBhcGlTZXJ2ZXI6CiAgICBpbWFnZTogcmVnaXN0cnkuazhzLmlvL2t1YmUtYXBpc2VydmVyOnYxLjMxLjEKICAgIGNlcnRTQU5zOgogICAgICAtIDEwLjAuMS4xMDAKICAgICAgLSAxMC4wLjEuMTAxCiAgICAgIC0gMTAuMC4xLjEwMAogICAgZGlzYWJsZVBvZFNlY3VyaXR5UG9saWN5OiB0cnVlCiAgICBhZG1pc3Npb25Db250cm9sOgogICAgICAtIG5hbWU6IFBvZFNlY3VyaXR5CiAgICAgICAgY29uZmlndXJhdGlvbjoKICAgICAgICAgIGFwaVZlcnNpb246IHBvZC1zZWN1cml0eS5hZG1pc3Npb24uY29uZmlnLms4cy5pby92MWFscGhhMQogICAgICAgICAgZGVmYXVsdHM6CiAgICAgICAgICAgIGF1ZGl0OiByZXN0cmljdGVkCiAgICAgICAgICAgIGF1ZGl0LXZlcnNpb246IGxhdGVzdAogICAgICAgICAgICBlbmZvcmNlOiBiYXNlbGluZQogICAgICAgICAgICBlbmZvcmNlLXZlcnNpb246IGxhdGVzdAogICAgICAgICAgICB3YXJuOiByZXN0cmljdGVkCiAgICAgICAgICAgIHdhcm4tdmVyc2lvbjogbGF0ZXN0CiAgICAgICAgICBleGVtcHRpb25zOgogICAgICAgICAgICBuYW1lc3BhY2VzOgogICAgICAgICAgICAgIC0ga3ViZS1zeXN0ZW0KICAgICAgICAgICAgcnVudGltZUNsYXNzZXM6IFtdCiAgICAgICAgICAgIHVzZXJuYW1lczogW10KICAgICAgICAgIGtpbmQ6IFBvZFNlY3VyaXR5Q29uZmlndXJhdGlvbgogICAgYXVkaXRQb2xpY3k6CiAgICAgIGFwaVZlcnNpb246IGF1ZGl0Lms4cy5pby92MQogICAgICBraW5kOiBQb2xpY3kKICAgICAgcnVsZXM6CiAgICAgICAgLSBsZXZlbDogTWV0YWRhdGEKICBjb250cm9sbGVyTWFuYWdlcjoKICAgIGltYWdlOiByZWdpc3RyeS5rOHMuaW8va3ViZS1jb250cm9sbGVyLW1hbmFnZXI6djEuMzEuMQogICAgZXh0cmFBcmdzOgogICAgICBiaW5kLWFkZHJlc3M6IDAuMC4wLjAKICAgICAgY2xvdWQtcHJvdmlkZXI6IGV4dGVybmFsCiAgICAgIG5vZGUtY2lkci1tYXNrLXNpemUtaXB2NDogIjI0IgogIHByb3h5OgogICAgZGlzYWJsZWQ6IHRydWUKICAgIGltYWdlOiByZWdpc3RyeS5rOHMuaW8va3ViZS1wcm94eTp2MS4zMS4xCiAgc2NoZWR1bGVyOgogICAgaW1hZ2U6IHJlZ2lzdHJ5Lms4cy5pby9rdWJlLXNjaGVkdWxlcjp2MS4zMS4xCiAgICBleHRyYUFyZ3M6CiAgICAgIGJpbmQtYWRkcmVzczogMC4wLjAuMAogIGRpc2NvdmVyeToKICAgIGVuYWJsZWQ6IHRydWUKICAgIHJlZ2lzdHJpZXM6CiAgICAgIGt1YmVybmV0ZXM6CiAgICAgICAgZGlzYWJsZWQ6IHRydWUKICAgICAgc2VydmljZToge30KICBldGNkOgogICAgZXh0cmFBcmdzOgogICAgICBsaXN0ZW4tbWV0cmljcy11cmxzOiBodHRwOi8vMC4wLjAuMDoyMzgxCiAgICBhZHZlcnRpc2VkU3VibmV0czoKICAgICAgLSAxMC4wLjEuMC8yNAogIGNvcmVETlM6CiAgICBkaXNhYmxlZDogZmFsc2UKICBleHRlcm5hbENsb3VkUHJvdmlkZXI6CiAgICBlbmFibGVkOiB0cnVlCg==
Original file line number Diff line number Diff line change
@@ -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

0 comments on commit ccbd5ae

Please sign in to comment.