Skip to content

Commit 2a7d5ac

Browse files
authored
Merge pull request #254 from chrisdoherty4/patch/userdata-compression
Fix userdata compression
2 parents 072e1dc + 9856d80 commit 2a7d5ac

File tree

7 files changed

+219
-64
lines changed

7 files changed

+219
-64
lines changed

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -263,7 +263,8 @@ config/.flag-test.mk: $(CONTROLLER_GEN) $(MANIFEST_GEN_INPUTS_TEST)
263263
@touch config/.flag-test.mk
264264

265265
.PHONY: test
266-
test: generate-deepcopy-test generate-manifest-test generate-mocks lint $(GINKGO_V2) $(KUBECTL) $(API_SERVER) $(ETCD) ## Run tests. At the moment this is only unit tests.
266+
test: ## Run tests.
267+
test: generate-deepcopy-test generate-manifest-test generate-mocks lint $(GINKGO_V2) $(KUBECTL) $(API_SERVER) $(ETCD)
267268
@./hack/testing_ginkgo_recover_statements.sh --add # Add ginkgo.GinkgoRecover() statements to controllers.
268269
@# The following is a slightly funky way to make sure the ginkgo statements are removed regardless the test results.
269270
@$(GINKGO_V2) --label-filter="!integ" --cover -coverprofile cover.out --covermode=atomic -v ./api/... ./controllers/... ./pkg/...; EXIT_STATUS=$$?;\

api/v1beta2/cloudstackmachine_types.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,10 @@ type CloudStackMachineSpec struct {
9292
UncompressedUserData *bool `json:"uncompressedUserData,omitempty"`
9393
}
9494

95+
func (c *CloudStackMachine) CompressUserdata() bool {
96+
return c.Spec.UncompressedUserData == nil || !*c.Spec.UncompressedUserData
97+
}
98+
9599
type CloudStackResourceIdentifier struct {
96100
// Cloudstack resource ID.
97101
// +optional
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/*
2+
Copyright 2022 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package v1beta2_test
18+
19+
import (
20+
"k8s.io/utils/pointer"
21+
capcv1 "sigs.k8s.io/cluster-api-provider-cloudstack/api/v1beta2"
22+
23+
. "github.com/onsi/ginkgo/v2"
24+
. "github.com/onsi/gomega"
25+
)
26+
27+
var _ = Describe("CloudStackMachineConfig_CompressUserdata", func() {
28+
for _, tc := range []struct {
29+
Name string
30+
Machine capcv1.CloudStackMachine
31+
Expect bool
32+
}{
33+
{
34+
Name: "is true when uncompressed user data is nil",
35+
Machine: capcv1.CloudStackMachine{
36+
Spec: capcv1.CloudStackMachineSpec{
37+
UncompressedUserData: nil,
38+
},
39+
},
40+
Expect: true,
41+
},
42+
{
43+
Name: "is false when uncompressed user data is true",
44+
Machine: capcv1.CloudStackMachine{
45+
Spec: capcv1.CloudStackMachineSpec{
46+
UncompressedUserData: pointer.Bool(true),
47+
},
48+
},
49+
Expect: false,
50+
},
51+
{
52+
Name: "Is false when uncompressed user data is false",
53+
Machine: capcv1.CloudStackMachine{
54+
Spec: capcv1.CloudStackMachineSpec{
55+
UncompressedUserData: pointer.Bool(false),
56+
},
57+
},
58+
Expect: true,
59+
},
60+
} {
61+
tc := tc
62+
It(tc.Name, func() {
63+
result := tc.Machine.CompressUserdata()
64+
Expect(result).To(Equal(tc.Expect))
65+
})
66+
}
67+
})

pkg/cloud/helpers.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ package cloud
1818

1919
import (
2020
"bytes"
21-
"compress/gzip"
21+
cgzip "compress/gzip"
2222
)
2323

2424
type set func(string)
@@ -43,13 +43,13 @@ func setIntIfPositive(num int64, setFn setInt) {
4343
}
4444
}
4545

46-
func CompressString(str string) (string, error) {
47-
buf := &bytes.Buffer{}
48-
gzipWriter := gzip.NewWriter(buf)
49-
if _, err := gzipWriter.Write([]byte(str)); err != nil {
46+
func compress(str string) (string, error) {
47+
var buf bytes.Buffer
48+
w := cgzip.NewWriter(&buf)
49+
if _, err := w.Write([]byte(str)); err != nil {
5050
return "", err
5151
}
52-
if err := gzipWriter.Close(); err != nil {
52+
if err := w.Close(); err != nil {
5353
return "", err
5454
}
5555
return buf.String(), nil

pkg/cloud/helpers_test.go

Lines changed: 13 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -24,27 +24,10 @@ import (
2424
"reflect"
2525

2626
"github.com/golang/mock/gomock"
27-
. "github.com/onsi/ginkgo/v2"
2827
. "github.com/onsi/gomega"
2928
"github.com/onsi/gomega/types"
30-
"sigs.k8s.io/cluster-api-provider-cloudstack/pkg/cloud"
3129
)
3230

33-
var _ = Describe("Helpers", func() {
34-
35-
It("should compress and encode string", func() {
36-
str := "Hello World"
37-
38-
compressedData, err := cloud.CompressString(str)
39-
40-
reader, _ := gzip.NewReader(bytes.NewReader([]byte(compressedData)))
41-
result, _ := io.ReadAll(reader)
42-
43-
Ω(err).Should(BeNil())
44-
Ω(string(result)).Should(Equal(str))
45-
})
46-
})
47-
4831
// This matcher is used to make gomega matching compatible with gomock parameter matching.
4932
// It's pretty awesome!
5033
//
@@ -91,9 +74,16 @@ func FieldMatcherGenerator(fetchFunc string) func(string) types.GomegaMatcher {
9174
}
9275
}
9376

94-
var (
95-
DomainIDEquals = FieldMatcherGenerator("GetDomainid")
96-
AccountEquals = FieldMatcherGenerator("GetAccount")
97-
IDEquals = FieldMatcherGenerator("GetId")
98-
NameEquals = FieldMatcherGenerator("GetName")
99-
)
77+
var NameEquals = FieldMatcherGenerator("GetName")
78+
79+
func decompress(data []byte) ([]byte, error) {
80+
reader, err := gzip.NewReader(bytes.NewBuffer(data))
81+
if err != nil {
82+
return nil, err
83+
}
84+
data, err = io.ReadAll(reader)
85+
if err != nil {
86+
return nil, err
87+
}
88+
return data, nil
89+
}

pkg/cloud/instance.go

Lines changed: 48 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -214,7 +214,8 @@ func (c *client) GetOrCreateVMInstance(
214214
csCluster *infrav1.CloudStackCluster,
215215
fd *infrav1.CloudStackFailureDomain,
216216
affinity *infrav1.CloudStackAffinityGroup,
217-
userData string) error {
217+
userData string,
218+
) error {
218219

219220
// Check if VM instance already exists.
220221
if err := c.ResolveVMInstanceDetails(csMachine); err == nil ||
@@ -245,19 +246,19 @@ func (c *client) GetOrCreateVMInstance(
245246

246247
setIfNotEmpty(csMachine.Spec.SSHKey, p.SetKeypair)
247248

248-
userData, err = handleUserData(userData, csMachine.Spec.UncompressedUserData)
249-
if err != nil {
250-
return err
249+
if csMachine.CompressUserdata() {
250+
userData, err = compress(userData)
251+
if err != nil {
252+
return err
253+
}
251254
}
255+
userData = base64.StdEncoding.EncodeToString([]byte(userData))
252256
setIfNotEmpty(userData, p.SetUserdata)
253257

254258
if len(csMachine.Spec.AffinityGroupIDs) > 0 {
255259
p.SetAffinitygroupids(csMachine.Spec.AffinityGroupIDs)
256260
} else if strings.ToLower(csMachine.Spec.Affinity) != "no" && csMachine.Spec.Affinity != "" {
257261
p.SetAffinitygroupids([]string{affinity.Spec.ID})
258-
if err != nil {
259-
return err
260-
}
261262
}
262263

263264
if csMachine.Spec.Details != nil {
@@ -268,21 +269,22 @@ func (c *client) GetOrCreateVMInstance(
268269
if err != nil {
269270
c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err)
270271

271-
// Just because an error was returned doesn't mean a (failed) VM wasn't created and will need to be dealt with.
272-
// Regretfully the deployVMResp may be nil, so we need to get the VM ID with a separate query, so we
273-
// can return it to the caller, so they can clean it up.
274-
listVirtualMachineParams := c.cs.VirtualMachine.NewListVirtualMachinesParams()
275-
listVirtualMachineParams.SetTemplateid(templateID)
276-
listVirtualMachineParams.SetZoneid(fd.Spec.Zone.ID)
277-
listVirtualMachineParams.SetNetworkid(fd.Spec.Zone.Network.ID)
278-
listVirtualMachineParams.SetName(csMachine.Name)
279-
listVirtualMachinesResponse, err2 := c.cs.VirtualMachine.ListVirtualMachines(listVirtualMachineParams)
280-
if err2 != nil || listVirtualMachinesResponse.Count <= 0 {
281-
c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err2)
272+
// CloudStack may have created the VM even though it reported an error. We attempt to
273+
// retrieve the VM so we can populate the CloudStackMachine for the user to manually
274+
// clean up.
275+
vm, findErr := findVirtualMachine(c.cs.VirtualMachine, templateID, fd, csMachine)
276+
if findErr != nil {
277+
c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(findErr)
278+
return fmt.Errorf("%v; find virtual machine: %v", err, findErr)
279+
}
280+
281+
// We didn't find a VM so return the original error.
282+
if vm == nil {
282283
return err
283284
}
284-
csMachine.Spec.InstanceID = pointer.String(listVirtualMachinesResponse.VirtualMachines[0].Id)
285-
csMachine.Status.InstanceState = listVirtualMachinesResponse.VirtualMachines[0].State
285+
286+
csMachine.Spec.InstanceID = pointer.String(vm.Id)
287+
csMachine.Status.InstanceState = vm.State
286288
} else {
287289
csMachine.Spec.InstanceID = pointer.String(deployVMResp.Id)
288290
csMachine.Status.Status = pointer.String(metav1.StatusSuccess)
@@ -292,6 +294,32 @@ func (c *client) GetOrCreateVMInstance(
292294
return c.ResolveVMInstanceDetails(csMachine)
293295
}
294296

297+
// findVirtualMachine retrieves a virtual machine by matching its expected name, template, failure
298+
// domain zone and failure domain network. If no virtual machine is found it returns nil, nil.
299+
func findVirtualMachine(
300+
client cloudstack.VirtualMachineServiceIface,
301+
templateID string,
302+
failureDomain *infrav1.CloudStackFailureDomain,
303+
machine *infrav1.CloudStackMachine,
304+
) (*cloudstack.VirtualMachine, error) {
305+
params := client.NewListVirtualMachinesParams()
306+
params.SetTemplateid(templateID)
307+
params.SetZoneid(failureDomain.Spec.Zone.ID)
308+
params.SetNetworkid(failureDomain.Spec.Zone.Network.ID)
309+
params.SetName(machine.Name)
310+
311+
response, err := client.ListVirtualMachines(params)
312+
if err != nil {
313+
return nil, err
314+
}
315+
316+
if response.Count == 0 {
317+
return nil, nil
318+
}
319+
320+
return response.VirtualMachines[0], nil
321+
}
322+
295323
// DestroyVMInstance Destroys a VM instance. Assumes machine has been fetched prior and has an instance ID.
296324
func (c *client) DestroyVMInstance(csMachine *infrav1.CloudStackMachine) error {
297325
// Attempt deletion regardless of machine state.
@@ -346,15 +374,3 @@ func (c *client) listVMInstanceDatadiskVolumeIDs(instanceID string) ([]string, e
346374
return ret, nil
347375

348376
}
349-
350-
// handleUserData optionally compresses and then base64 encodes userdata
351-
func handleUserData(userData string, uncompressed *bool) (string, error) {
352-
var err error
353-
if uncompressed != nil && !*uncompressed {
354-
userData, err = CompressString(userData)
355-
if err != nil {
356-
return "", err
357-
}
358-
}
359-
return base64.StdEncoding.EncodeToString([]byte(userData)), nil
360-
}

pkg/cloud/instance_test.go

Lines changed: 79 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ limitations under the License.
1717
package cloud_test
1818

1919
import (
20+
"encoding/base64"
2021
"fmt"
2122

2223
"github.com/apache/cloudstack-go/v2/cloudstack"
@@ -264,14 +265,27 @@ var _ = Describe("Instance", func() {
264265

265266
deploymentResp := &cloudstack.DeployVirtualMachineResponse{Id: *dummies.CSMachine1.Spec.InstanceID}
266267

268+
expectUserData := "my special userdata"
269+
267270
vms.EXPECT().DeployVirtualMachine(gomock.Any()).Do(
268271
func(p interface{}) {
269-
displayName, _ := p.(*cloudstack.DeployVirtualMachineParams).GetDisplayname()
272+
params := p.(*cloudstack.DeployVirtualMachineParams)
273+
displayName, _ := params.GetDisplayname()
270274
Ω(displayName == dummies.CAPIMachine.Name).Should(BeTrue())
275+
276+
b64UserData, _ := params.GetUserdata()
277+
278+
userData, err := base64.StdEncoding.DecodeString(b64UserData)
279+
Ω(err).ToNot(HaveOccurred())
280+
281+
decompressedUserData, err := decompress(userData)
282+
Ω(err).ToNot(HaveOccurred())
283+
284+
Ω(string(decompressedUserData)).To(Equal(expectUserData))
271285
}).Return(deploymentResp, nil)
272286

273287
Ω(client.GetOrCreateVMInstance(
274-
dummies.CSMachine1, dummies.CAPIMachine, dummies.CSCluster, dummies.CSFailureDomain1, dummies.CSAffinityGroup, "")).
288+
dummies.CSMachine1, dummies.CAPIMachine, dummies.CSCluster, dummies.CSFailureDomain1, dummies.CSAffinityGroup, expectUserData)).
275289
Should(Succeed())
276290
}
277291

@@ -424,6 +438,69 @@ var _ = Describe("Instance", func() {
424438

425439
})
426440
})
441+
442+
It("doesn't compress user data", func() {
443+
dummies.CSMachine1.Spec.DiskOffering.ID = diskOfferingFakeID
444+
dummies.CSMachine1.Spec.Offering.ID = ""
445+
dummies.CSMachine1.Spec.Template.ID = ""
446+
dummies.CSMachine1.Spec.Offering.Name = "offering"
447+
dummies.CSMachine1.Spec.Template.Name = "template"
448+
dummies.CSMachine1.Spec.UncompressedUserData = pointer.Bool(true)
449+
450+
vms.EXPECT().
451+
GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID).
452+
Return(nil, -1, notFoundError)
453+
vms.EXPECT().
454+
GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID).
455+
Return(&cloudstack.VirtualMachinesMetric{}, 1, nil)
456+
vms.EXPECT().
457+
GetVirtualMachinesMetricByName(dummies.CSMachine1.Name).
458+
Return(nil, -1, notFoundError)
459+
sos.EXPECT().
460+
GetServiceOfferingID(dummies.CSMachine1.Spec.Offering.Name, gomock.Any()).
461+
Return(offeringFakeID, 1, nil)
462+
dos.EXPECT().
463+
GetDiskOfferingID(dummies.CSMachine1.Spec.DiskOffering.Name, gomock.Any()).
464+
Return(diskOfferingFakeID, 1, nil)
465+
dos.EXPECT().
466+
GetDiskOfferingByID(dummies.CSMachine1.Spec.DiskOffering.ID).
467+
Return(&cloudstack.DiskOffering{Iscustomized: false}, 1, nil)
468+
ts.EXPECT().
469+
GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID).
470+
Return(templateFakeID, 1, nil)
471+
vms.EXPECT().
472+
NewDeployVirtualMachineParams(offeringFakeID, templateFakeID, dummies.Zone1.ID).
473+
Return(&cloudstack.DeployVirtualMachineParams{})
474+
475+
deploymentResp := &cloudstack.DeployVirtualMachineResponse{
476+
Id: *dummies.CSMachine1.Spec.InstanceID,
477+
}
478+
479+
expectUserData := "my special userdata"
480+
481+
vms.EXPECT().DeployVirtualMachine(gomock.Any()).Do(
482+
func(p interface{}) {
483+
params := p.(*cloudstack.DeployVirtualMachineParams)
484+
displayName, _ := params.GetDisplayname()
485+
Ω(displayName == dummies.CAPIMachine.Name).Should(BeTrue())
486+
487+
// Ensure the user data is only base64 encoded.
488+
b64UserData, _ := params.GetUserdata()
489+
userData, err := base64.StdEncoding.DecodeString(b64UserData)
490+
Ω(err).ToNot(HaveOccurred())
491+
Ω(string(userData)).To(Equal(expectUserData))
492+
}).Return(deploymentResp, nil)
493+
494+
err := client.GetOrCreateVMInstance(
495+
dummies.CSMachine1,
496+
dummies.CAPIMachine,
497+
dummies.CSCluster,
498+
dummies.CSFailureDomain1,
499+
dummies.CSAffinityGroup,
500+
expectUserData,
501+
)
502+
Ω(err).Should(Succeed())
503+
})
427504
})
428505

429506
Context("when destroying a VM instance", func() {

0 commit comments

Comments
 (0)