Skip to content

Add Gzip Compression Option for User Data #649

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
27 changes: 27 additions & 0 deletions bootstrap/api/v1alpha1/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,10 @@ func (src *RKE2Config) ConvertTo(dstRaw conversion.Hub) error {
dst.Spec.AgentConfig.PodSecurityAdmissionConfigFile = restored.Spec.AgentConfig.PodSecurityAdmissionConfigFile
}

if restored.Spec.GzipUserData != nil {
dst.Spec.GzipUserData = restored.Spec.GzipUserData
}

return nil
}

Expand Down Expand Up @@ -110,6 +114,10 @@ func (src *RKE2ConfigTemplate) ConvertTo(dstRaw conversion.Hub) error {
dst.Spec.Template.Spec.AgentConfig.PodSecurityAdmissionConfigFile = restored.Spec.Template.Spec.AgentConfig.PodSecurityAdmissionConfigFile
}

if restored.Spec.Template.Spec.GzipUserData != nil {
dst.Spec.Template.Spec.GzipUserData = restored.Spec.Template.Spec.GzipUserData
}

return nil
}

Expand Down Expand Up @@ -154,3 +162,22 @@ func Convert_v1beta1_RKE2AgentConfig_To_v1alpha1_RKE2AgentConfig(in *bootstrapv1
// We have to invoke conversion manually because of the added AirGappedChecksum field.
return autoConvert_v1beta1_RKE2AgentConfig_To_v1alpha1_RKE2AgentConfig(in, out, s)
}

func Convert_v1alpha1_RKE2ConfigSpec_To_v1beta1_RKE2ConfigSpec(in *RKE2ConfigSpec, out *bootstrapv1.RKE2ConfigSpec, s apiconversion.Scope) error {
if err := autoConvert_v1alpha1_RKE2ConfigSpec_To_v1beta1_RKE2ConfigSpec(in, out, s); err != nil {
return err
}

// Default to false during up-conversion since field doesn't exist in v1alpha1
out.GzipUserData = nil
return nil
}

func Convert_v1beta1_RKE2ConfigSpec_To_v1alpha1_RKE2ConfigSpec(in *bootstrapv1.RKE2ConfigSpec, out *RKE2ConfigSpec, s apiconversion.Scope) error {
if err := autoConvert_v1beta1_RKE2ConfigSpec_To_v1alpha1_RKE2ConfigSpec(in, out, s); err != nil {
return err
}

// GzipUserData does not exist in v1alpha1, so it's intentionally ignored
return nil
}
22 changes: 14 additions & 8 deletions bootstrap/api/v1alpha1/conversion_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,17 +37,23 @@ func TestFuzzyConversion(t *testing.T) {
g.Expect(bootstrapv1.AddToScheme(scheme)).To(Succeed())

t.Run("for RKE2Config", utilconversion.FuzzTestFunc(utilconversion.FuzzTestFuncInput{
Scheme: scheme,
Hub: &bootstrapv1.RKE2Config{},
Spoke: &RKE2Config{},
FuzzerFuncs: []fuzzer.FuzzerFuncs{fuzzFuncs},
Scheme: scheme,
Hub: &bootstrapv1.RKE2Config{},
Spoke: &RKE2Config{},
FuzzerFuncs: []fuzzer.FuzzerFuncs{
fuzzFuncs, // v1alpha1 fuzzer
bootstrapv1.FuzzFuncsv1beta1, // v1beta1 fuzzer
},
}))

t.Run("for RKE2ConfigTemplate", utilconversion.FuzzTestFunc(utilconversion.FuzzTestFuncInput{
Scheme: scheme,
Hub: &bootstrapv1.RKE2ConfigTemplate{},
Spoke: &RKE2ConfigTemplate{},
FuzzerFuncs: []fuzzer.FuzzerFuncs{fuzzFuncs},
Scheme: scheme,
Hub: &bootstrapv1.RKE2ConfigTemplate{},
Spoke: &RKE2ConfigTemplate{},
FuzzerFuncs: []fuzzer.FuzzerFuncs{
fuzzFuncs,
bootstrapv1.FuzzFuncsv1beta1,
},
}))
}

Expand Down
31 changes: 11 additions & 20 deletions bootstrap/api/v1alpha1/zz_generated.conversion.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 44 additions & 0 deletions bootstrap/api/v1beta1/fuzzer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
/*
Copyright 2025 SUSE LLC.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package v1beta1

import (
fuzz "github.com/google/gofuzz"
runtimeserializer "k8s.io/apimachinery/pkg/runtime/serializer"
)

// FuzzFuncsv1beta1 exposes fuzzers for testing conversion logic.
func FuzzFuncsv1beta1(_ runtimeserializer.CodecFactory) []interface{} {
return []interface{}{
RKE2ConfigFuzzer,
RKE2ConfigTemplateFuzzer,
}
}

// RKE2ConfigFuzzer is fuzzer for v1beta1 RKE2Config (hub).
func RKE2ConfigFuzzer(obj *RKE2Config, c fuzz.Continue) {
c.FuzzNoCustom(obj)
val := c.RandBool()
obj.Spec.GzipUserData = &val
}

// RKE2ConfigTemplateFuzzer is fuzzer for v1beta1 RKE2ConfigTemplate (hub).
func RKE2ConfigTemplateFuzzer(obj *RKE2ConfigTemplate, c fuzz.Continue) {
c.FuzzNoCustom(obj)
val := c.RandBool()
obj.Spec.Template.Spec.GzipUserData = &val
}
4 changes: 4 additions & 0 deletions bootstrap/api/v1beta1/rke2config_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,10 @@ type RKE2ConfigSpec struct {
// PrivateRegistriesConfig defines the containerd configuration for private registries and local registry mirrors.
//+optional
PrivateRegistriesConfig Registry `json:"privateRegistriesConfig,omitempty"`

// GzipUserData specifies if the user data should be gzipped.
//+optional
GzipUserData *bool `json:"gzipUserData,omitempty"`
}

// RKE2AgentConfig describes some attributes that are common to agent and server nodes.
Expand Down
5 changes: 5 additions & 0 deletions bootstrap/api/v1beta1/rke2config_webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,11 @@ func DefaultRKE2ConfigSpec(spec *RKE2ConfigSpec) {
if spec.AgentConfig.Format == "" {
spec.AgentConfig.Format = CloudConfig
}

if spec.GzipUserData == nil {
spec.GzipUserData = new(bool)
*spec.GzipUserData = false
}
}

//+kubebuilder:webhook:path=/validate-bootstrap-cluster-x-k8s-io-v1beta1-rke2config,mutating=false,failurePolicy=fail,sideEffects=None,groups=bootstrap.cluster.x-k8s.io,resources=rke2configs,verbs=create;update,versions=v1beta1,name=vrke2config.kb.io,admissionReviewVersions=v1
Expand Down
5 changes: 5 additions & 0 deletions bootstrap/api/v1beta1/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -952,6 +952,9 @@ spec:
- path
type: object
type: array
gzipUserData:
description: GzipUserData specifies if the user data should be gzipped.
type: boolean
postRKE2Commands:
description: PostRKE2Commands specifies extra commands to run after
rke2 setup runs.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,10 @@ spec:
- path
type: object
type: array
gzipUserData:
description: GzipUserData specifies if the user data should
be gzipped.
type: boolean
postRKE2Commands:
description: PostRKE2Commands specifies extra commands to
run after rke2 setup runs.
Expand Down
30 changes: 26 additions & 4 deletions bootstrap/internal/controllers/rke2config_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package controllers

import (
"bytes"
"compress/gzip"
"context"
"fmt"
"time"
Expand Down Expand Up @@ -50,6 +51,7 @@ import (
bootstrapv1 "github.com/rancher/cluster-api-provider-rke2/bootstrap/api/v1beta1"
"github.com/rancher/cluster-api-provider-rke2/bootstrap/internal/cloudinit"
"github.com/rancher/cluster-api-provider-rke2/bootstrap/internal/ignition"
"github.com/rancher/cluster-api-provider-rke2/bootstrap/internal/ignition/butane"
controlplanev1 "github.com/rancher/cluster-api-provider-rke2/controlplane/api/v1beta1"
"github.com/rancher/cluster-api-provider-rke2/pkg/consts"
"github.com/rancher/cluster-api-provider-rke2/pkg/locking"
Expand Down Expand Up @@ -642,10 +644,6 @@ func (r *RKE2ConfigReconciler) joinControlplane(ctx context.Context, scope *Scop
return ctrl.Result{}, fmt.Errorf("unable to marshal config.yaml: %w", err)
}

if err != nil {
return ctrl.Result{}, err
}

scope.Logger.Info("Joining Server config marshalled successfully")

initConfigFile := bootstrapv1.File{
Expand Down Expand Up @@ -893,6 +891,30 @@ func (r *RKE2ConfigReconciler) generateAndStoreToken(ctx context.Context, scope
// storeBootstrapData creates a new secret with the data passed in as input,
// sets the reference in the configuration status and ready to true.
func (r *RKE2ConfigReconciler) storeBootstrapData(ctx context.Context, scope *Scope, data []byte) error {
if *scope.Config.Spec.GzipUserData {
var buf bytes.Buffer
gz := gzip.NewWriter(&buf)

if _, err := gz.Write(data); err != nil {
return err
}

if err := gz.Close(); err != nil {
return err
}

if scope.Config.Spec.AgentConfig.Format == bootstrapv1.Ignition {
res, err := butane.EncapsulateGzippedConfig(buf.Bytes())
if err != nil {
return err
}

data = res
} else {
data = buf.Bytes()
}
}

secret := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: scope.Config.Name,
Expand Down
26 changes: 26 additions & 0 deletions bootstrap/internal/ignition/butane/butane.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package butane

import (
"bytes"
"encoding/base64"
"encoding/json"
"fmt"
"strings"
Expand Down Expand Up @@ -280,3 +281,28 @@ func Render(input *cloudinit.BaseUserData, butaneCfg *bootstrapv1.AdditionalUser

return userData, nil
}

// EncapsulateGzippedConfig takes a gzipped Ignition config and encapsulates it in an Ignition config with compression.
func EncapsulateGzippedConfig(gzippedConfig []byte) ([]byte, error) {
comp := "gzip"
dataSource := "data:text/plain;base64," + base64.StdEncoding.EncodeToString(gzippedConfig)

encapCfg := ignitionTypes.Config{
Ignition: ignitionTypes.Ignition{
Version: "3.3.0",
Config: ignitionTypes.IgnitionConfig{
Replace: ignitionTypes.Resource{
Compression: &comp,
Source: &dataSource,
},
},
},
}

cfg, err := json.Marshal(encapCfg)
if err != nil {
return nil, errors.Wrap(err, "marshaling Ignition config into JSON")
}

return cfg, nil
}
4 changes: 4 additions & 0 deletions controlplane/api/v1alpha1/conversion.go
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,10 @@ func (src *RKE2ControlPlane) ConvertTo(dstRaw conversion.Hub) error {
return err
}

if restored.Spec.GzipUserData != nil {
dst.Spec.GzipUserData = restored.Spec.GzipUserData
}

if restored.Spec.Version != "" {
dst.Spec.Version = restored.Spec.Version
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1591,6 +1591,9 @@ spec:
- path
type: object
type: array
gzipUserData:
description: GzipUserData specifies if the user data should be gzipped.
type: boolean
infrastructureRef:
description: |-
InfrastructureRef is a required reference to a custom resource
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,10 @@ spec:
- path
type: object
type: array
gzipUserData:
description: GzipUserData specifies if the user data should
be gzipped.
type: boolean
infrastructureRef:
description: |-
InfrastructureRef is a required reference to a custom resource
Expand Down
21 changes: 21 additions & 0 deletions docs/book/src/02_topics/05_user_data_compression.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
## Configuring User Data Compression

## Overview
Cloud-init user-data can grow significantly in size, especially when many configurations and embedded scripts are used. Some infrastructure providers (e.g., AWS, OpenStack) have strict size limits for user-data, which can lead to provisioning failures. To address this, the gzipUserData field in RKE2ConfigSpec allows optional gzip compression of the user-data payload before it is passed to the infrastructure provider.

This feature helps reduce the size of the cloud-init payload but should only be used when the infrastructure provider supports gzipped user-data. For example, OpenStack typically handles it well, while others like Metal3 may not.

## Using Gzip Compression for User Data
The `gzipUserData` field in RKE2ConfigSpec allows you to enable gzip compression for the rendered cloud-init user data.
By default, `gzipUserData` is set to `false`. When set to `true`, the user-data is gzipped before being passed to the infrastructure provider.

Example:

```yaml
apiVersion: bootstrap.cluster.x-k8s.io/v1beta1
kind: RKE2Config
metadata:
name: my-cluster-bootstrap
spec:
gzipUserData: true
```
Loading
Loading