From daadff7e76c5d39d6f2883767c534ea8a1b7174c Mon Sep 17 00:00:00 2001 From: Ashley Dumaine <5779804+AshleyDumaine@users.noreply.github.com> Date: Tue, 12 Nov 2024 09:23:54 -0500 Subject: [PATCH] add an env var to toggle gzip compression for cloud-init data (#531) --- cmd/main.go | 14 +++++--- controller/linodemachine_controller.go | 4 ++- .../linodemachine_controller_helpers.go | 36 ++++++++++++++++--- .../linodemachine_controller_helpers_test.go | 24 +++++++++++-- controller/linodemachine_controller_test.go | 11 +++--- 5 files changed, 72 insertions(+), 17 deletions(-) diff --git a/cmd/main.go b/cmd/main.go index 36ac470aa..5bb4890ce 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -271,12 +271,18 @@ func setupControllers(mgr manager.Manager, flags flagVars, linodeClientConfig, d os.Exit(1) } + useGzip, err := strconv.ParseBool(os.Getenv("GZIP_COMPRESSION_ENABLED")) + if err != nil { + setupLog.Error(err, "proceeding without gzip compression for cloud-init data") + } + // LinodeMachine Controller if err := (&controller.LinodeMachineReconciler{ - Client: mgr.GetClient(), - Recorder: mgr.GetEventRecorderFor("LinodeMachineReconciler"), - WatchFilterValue: flags.machineWatchFilter, - LinodeClientConfig: linodeClientConfig, + Client: mgr.GetClient(), + Recorder: mgr.GetEventRecorderFor("LinodeMachineReconciler"), + WatchFilterValue: flags.machineWatchFilter, + LinodeClientConfig: linodeClientConfig, + GzipCompressionEnabled: useGzip, }).SetupWithManager(mgr, crcontroller.Options{MaxConcurrentReconciles: flags.linodeMachineConcurrency}); err != nil { setupLog.Error(err, "unable to create controller", "controller", "LinodeMachine") os.Exit(1) diff --git a/controller/linodemachine_controller.go b/controller/linodemachine_controller.go index e922cc388..b2e354584 100644 --- a/controller/linodemachine_controller.go +++ b/controller/linodemachine_controller.go @@ -95,6 +95,8 @@ type LinodeMachineReconciler struct { LinodeClientConfig scope.ClientConfig WatchFilterValue string ReconcileTimeout time.Duration + // Feature flags + GzipCompressionEnabled bool } // +kubebuilder:rbac:groups=infrastructure.cluster.x-k8s.io,resources=linodemachines,verbs=get;list;watch;create;update;patch;delete @@ -400,7 +402,7 @@ func (r *LinodeMachineReconciler) reconcilePreflightMetadataSupportConfigure(ctx func (r *LinodeMachineReconciler) reconcilePreflightCreate(ctx context.Context, logger logr.Logger, machineScope *scope.MachineScope) (ctrl.Result, error) { // get the bootstrap data for the Linode instance and set it for create config - createOpts, err := newCreateConfig(ctx, machineScope, logger) + createOpts, err := newCreateConfig(ctx, machineScope, r.GzipCompressionEnabled, logger) if err != nil { logger.Error(err, "Failed to create Linode machine InstanceCreateOptions") return retryIfTransient(err, logger) diff --git a/controller/linodemachine_controller_helpers.go b/controller/linodemachine_controller_helpers.go index 35c255620..47304b27d 100644 --- a/controller/linodemachine_controller_helpers.go +++ b/controller/linodemachine_controller_helpers.go @@ -17,6 +17,8 @@ limitations under the License. package controller import ( + "bytes" + "compress/gzip" "context" b64 "encoding/base64" "errors" @@ -97,7 +99,7 @@ func fillCreateConfig(createConfig *linodego.InstanceCreateOptions, machineScope } } -func newCreateConfig(ctx context.Context, machineScope *scope.MachineScope, logger logr.Logger) (*linodego.InstanceCreateOptions, error) { +func newCreateConfig(ctx context.Context, machineScope *scope.MachineScope, gzipCompressionEnabled bool, logger logr.Logger) (*linodego.InstanceCreateOptions, error) { var err error createConfig := linodeMachineSpecToInstanceCreateConfig(machineScope.LinodeMachine.Spec) @@ -110,7 +112,7 @@ func newCreateConfig(ctx context.Context, machineScope *scope.MachineScope, logg } createConfig.Booted = util.Pointer(false) - if err := setUserData(ctx, machineScope, createConfig, logger); err != nil { + if err := setUserData(ctx, machineScope, createConfig, gzipCompressionEnabled, logger); err != nil { return nil, err } @@ -478,7 +480,21 @@ func linodeMachineSpecToInstanceCreateConfig(machineSpec infrav1alpha2.LinodeMac } } -func setUserData(ctx context.Context, machineScope *scope.MachineScope, createConfig *linodego.InstanceCreateOptions, logger logr.Logger) error { +func compressUserData(bootstrapData []byte) ([]byte, error) { + var userDataBuff bytes.Buffer + var err error + gz := gzip.NewWriter(&userDataBuff) + defer func(gz *gzip.Writer) { + err = gz.Close() + }(gz) + if _, err := gz.Write(bootstrapData); err != nil { + return nil, err + } + err = gz.Close() + return userDataBuff.Bytes(), err +} + +func setUserData(ctx context.Context, machineScope *scope.MachineScope, createConfig *linodego.InstanceCreateOptions, gzipCompressionEnabled bool, logger logr.Logger) error { bootstrapData, err := machineScope.GetBootstrapData(ctx) if err != nil { logger.Error(err, "Failed to get bootstrap data") @@ -486,8 +502,18 @@ func setUserData(ctx context.Context, machineScope *scope.MachineScope, createCo return err } + userData := bootstrapData + //nolint:nestif // this is a temp flag until cloud-init is updated in cloud-init compatible images if machineScope.LinodeMachine.Status.CloudinitMetadataSupport { - bootstrapSize := len(bootstrapData) + if gzipCompressionEnabled { + userData, err = compressUserData(bootstrapData) + if err != nil { + logger.Error(err, "failed to compress bootstrap data") + + return err + } + } + bootstrapSize := len(userData) if bootstrapSize > maxBootstrapDataBytesCloudInit { err = errors.New("bootstrap data too large") logger.Error(err, "decoded bootstrap data exceeds size limit", @@ -498,7 +524,7 @@ func setUserData(ctx context.Context, machineScope *scope.MachineScope, createCo return err } createConfig.Metadata = &linodego.InstanceMetadataOptions{ - UserData: b64.StdEncoding.EncodeToString(bootstrapData), + UserData: b64.StdEncoding.EncodeToString(userData), } } else { logger.Info("using StackScripts for bootstrapping") diff --git a/controller/linodemachine_controller_helpers_test.go b/controller/linodemachine_controller_helpers_test.go index f38c320c2..0cf1ac74e 100644 --- a/controller/linodemachine_controller_helpers_test.go +++ b/controller/linodemachine_controller_helpers_test.go @@ -1,7 +1,10 @@ package controller import ( + "bytes" + "compress/gzip" "context" + "crypto/rand" b64 "encoding/base64" "fmt" "testing" @@ -9,6 +12,7 @@ import ( "github.com/go-logr/logr" "github.com/linode/linodego" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" @@ -64,6 +68,20 @@ func TestLinodeMachineSpecToCreateInstanceConfig(t *testing.T) { func TestSetUserData(t *testing.T) { t.Parallel() + userData := []byte("test-data") + if gzipCompressionFlag { + var userDataBuff bytes.Buffer + gz := gzip.NewWriter(&userDataBuff) + _, err = gz.Write([]byte("test-data")) + err = gz.Close() + require.NoError(t, err, "Failed to compress bootstrap data") + userData = userDataBuff.Bytes() + } + + largeData := make([]byte, maxBootstrapDataBytesCloudInit*10) + _, err = rand.Read(largeData) + require.NoError(t, err, "Failed to create bootstrap data") + tests := []struct { name string machineScope *scope.MachineScope @@ -92,7 +110,7 @@ func TestSetUserData(t *testing.T) { }}, createConfig: &linodego.InstanceCreateOptions{}, wantConfig: &linodego.InstanceCreateOptions{Metadata: &linodego.InstanceMetadataOptions{ - UserData: b64.StdEncoding.EncodeToString([]byte("test-data")), + UserData: b64.StdEncoding.EncodeToString(userData), }}, expects: func(mockClient *mock.MockLinodeClient, kMock *mock.MockK8sClient) { kMock.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj *corev1.Secret, opts ...client.GetOption) error { @@ -169,7 +187,7 @@ func TestSetUserData(t *testing.T) { kMock.EXPECT().Get(gomock.Any(), gomock.Any(), gomock.Any()).DoAndReturn(func(ctx context.Context, key types.NamespacedName, obj *corev1.Secret, opts ...client.GetOption) error { cred := corev1.Secret{ Data: map[string][]byte{ - "value": make([]byte, maxBootstrapDataBytesCloudInit+1), + "value": largeData, }, } *obj = cred @@ -289,7 +307,7 @@ func TestSetUserData(t *testing.T) { testcase.expects(mockClient, mockK8sClient) logger := logr.Logger{} - err := setUserData(context.Background(), testcase.machineScope, testcase.createConfig, logger) + err := setUserData(context.Background(), testcase.machineScope, testcase.createConfig, gzipCompressionFlag, logger) if testcase.expectedError != nil { assert.ErrorContains(t, err, testcase.expectedError.Error()) } else { diff --git a/controller/linodemachine_controller_test.go b/controller/linodemachine_controller_test.go index 7e3a47ec7..9d240135b 100644 --- a/controller/linodemachine_controller_test.go +++ b/controller/linodemachine_controller_test.go @@ -49,7 +49,10 @@ import ( . "github.com/onsi/gomega" ) -const defaultNamespace = "default" +const ( + defaultNamespace = "default" + gzipCompressionFlag = false +) var _ = Describe("create", Label("machine", "create"), func() { var machine clusterv1.Machine @@ -1944,7 +1947,7 @@ var _ = Describe("machine in PlacementGroup", Label("machine", "placementGroup") Expect(err).NotTo(HaveOccurred()) mScope.PatchHelper = patchHelper - createOpts, err := newCreateConfig(ctx, &mScope, logger) + createOpts, err := newCreateConfig(ctx, &mScope, gzipCompressionFlag, logger) Expect(err).NotTo(HaveOccurred()) Expect(createOpts).NotTo(BeNil()) Expect(createOpts.PlacementGroup.ID).To(Equal(1)) @@ -2121,7 +2124,7 @@ var _ = Describe("machine in VPC", Label("machine", "VPC"), Ordered, func() { Expect(err).NotTo(HaveOccurred()) mScope.PatchHelper = patchHelper - createOpts, err := newCreateConfig(ctx, &mScope, logger) + createOpts, err := newCreateConfig(ctx, &mScope, gzipCompressionFlag, logger) Expect(err).NotTo(HaveOccurred()) Expect(createOpts).NotTo(BeNil()) Expect(createOpts.Interfaces).To(Equal([]linodego.InstanceConfigInterfaceCreateOptions{ @@ -2197,7 +2200,7 @@ var _ = Describe("machine in VPC", Label("machine", "VPC"), Ordered, func() { Expect(err).NotTo(HaveOccurred()) mScope.PatchHelper = patchHelper - createOpts, err := newCreateConfig(ctx, &mScope, logger) + createOpts, err := newCreateConfig(ctx, &mScope, gzipCompressionFlag, logger) Expect(err).NotTo(HaveOccurred()) Expect(createOpts).NotTo(BeNil()) Expect(createOpts.Interfaces).To(Equal([]linodego.InstanceConfigInterfaceCreateOptions{