Skip to content

Commit 9856d80

Browse files
Fix userdata compression
When the UncompressedUserData field is nil on CloudStackMachine instances CAPC is interpreting it as true. This reworks the compression decision logic to give a nil UncompressedUserData the same semantics as false. Co-authored-by: Guillermo Gaston <[email protected]>
1 parent 0a451ed commit 9856d80

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)