-
Notifications
You must be signed in to change notification settings - Fork 54
✨ Adds new condition to VirtualMachine to the expose state of metadata+userdata #167
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,32 @@ import ( | |
"github.com/vmware-tanzu/vm-operator/api/v1alpha2/common" | ||
) | ||
|
||
// TODO: Since this API version replaces the concept of metadata to BootstrapSpec, rename the types once | ||
// these reasons before using them in Conditions. Keeping the reasons the same for the sake of consistency. | ||
const ( | ||
// VirtualMachineMetadataReadyCondition documents the state of the metadata passed to the VirtualMachine. | ||
VirtualMachineMetadataReadyCondition = "VirtualMachineMetadataReady" | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think we have a concept of "metadata" in v1a2? It is called There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Atm I am just trying to replicate the reasons in v1a2 types, I can add a TODO and get back to updating the description on this one once we make the switch to the new types. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a comment which states the intent of adding these and the future updates to these types. |
||
|
||
// VirtualMachineMetadataDuplicatedReason (Severity=Error) documents that the mutually exclusive Secret and ConfigMap are | ||
// specified in the VirtualMachineMetadata. | ||
// | ||
// This validation is performed by the validation webhook, defensively adding the reason. | ||
VirtualMachineMetadataDuplicatedReason = "VirtualMachineMetadataDuplicated" | ||
|
||
// VirtualMachineMetadataNotFoundReason (Severity=Error) documents that the Secret or ConfigMap specified in the VirtualMachineSpec | ||
// cannot be found. | ||
VirtualMachineMetadataNotFoundReason = "VirtualMachineMetadataNotFound" | ||
|
||
// VirtualMachineMetadataFormatInvalidReason (Severity=Error) documents that the format of the supplied metadata is invalid. | ||
// | ||
// It is used to indicate issues with the formatting of the metadata irrespective of the Transport in use. | ||
VirtualMachineMetadataFormatInvalidReason = "VirtualMachineMetadataFormatInvalid" | ||
|
||
// VirtualMachineGuestInfoSizeExceededReason (Severity=Error) documents that the supplied Cloud Init metadata exceeds the | ||
// maximum allowed capacity. | ||
VirtualMachineGuestInfoSizeExceededReason = "VirtualMachineGuestInfoSizeExceeded" | ||
) | ||
|
||
const ( | ||
// VirtualMachineConditionClassReady indicates that a referenced | ||
// VirtualMachineClass is ready. | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -17,6 +17,7 @@ import ( | |
apiEquality "k8s.io/apimachinery/pkg/api/equality" | ||
|
||
vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha1" | ||
"github.com/vmware-tanzu/vm-operator/pkg/conditions" | ||
"github.com/vmware-tanzu/vm-operator/pkg/context" | ||
"github.com/vmware-tanzu/vm-operator/pkg/lib" | ||
"github.com/vmware-tanzu/vm-operator/pkg/util" | ||
|
@@ -114,17 +115,27 @@ func GetCloudInitMetadata(vm *vmopv1.VirtualMachine, | |
return string(metadataBytes), nil | ||
} | ||
|
||
// ErrWithReason encapsulates the error type as well as a human-readable reason that will be | ||
// used to set the Reason field on the Condition set on the object. | ||
type ErrWithReason struct { | ||
error | ||
conditionReason string | ||
} | ||
|
||
func GetCloudInitPrepCustSpec( | ||
cloudInitMetadata string, | ||
updateArgs VMUpdateArgs) (*vimTypes.VirtualMachineConfigSpec, *vimTypes.CustomizationSpec, error) { | ||
updateArgs VMUpdateArgs) (*vimTypes.VirtualMachineConfigSpec, *vimTypes.CustomizationSpec, *ErrWithReason) { | ||
|
||
userdata := updateArgs.VMMetadata.Data["user-data"] | ||
|
||
if userdata != "" { | ||
// Ensure the data is normalized first to plain-text. | ||
plainText, err := util.TryToDecodeBase64Gzip([]byte(userdata)) | ||
if err != nil { | ||
return nil, nil, fmt.Errorf("decoding cloud-init prep userdata failed %v", err) | ||
return nil, nil, &ErrWithReason{ | ||
error: fmt.Errorf("decoding cloud-init prep userdata failed %v", err), | ||
conditionReason: vmopv1.VirtualMachineMetadataFormatInvalidReason, | ||
} | ||
} | ||
userdata = plainText | ||
} | ||
|
@@ -147,17 +158,27 @@ func GetCloudInitPrepCustSpec( | |
func GetCloudInitGuestInfoCustSpec( | ||
cloudInitMetadata string, | ||
config *vimTypes.VirtualMachineConfigInfo, | ||
updateArgs VMUpdateArgs) (*vimTypes.VirtualMachineConfigSpec, error) { | ||
updateArgs VMUpdateArgs) (*vimTypes.VirtualMachineConfigSpec, *ErrWithReason) { | ||
|
||
extraConfig := map[string]string{} | ||
|
||
encodedMetadata, err := EncodeGzipBase64(cloudInitMetadata) | ||
if err != nil { | ||
return nil, fmt.Errorf("encoding cloud-init metadata failed %v", err) | ||
return nil, &ErrWithReason{ | ||
error: fmt.Errorf("encoding cloud-init metadata failed %v", err), | ||
conditionReason: vmopv1.VirtualMachineMetadataFormatInvalidReason, | ||
} | ||
} | ||
extraConfig[constants.CloudInitGuestInfoMetadata] = encodedMetadata | ||
extraConfig[constants.CloudInitGuestInfoMetadataEncoding] = "gzip+base64" | ||
|
||
if len(encodedMetadata) > int(constants.CloudInitGuestInfoMaxSize) { | ||
return nil, &ErrWithReason{ | ||
error: fmt.Errorf("size of cloud-init guest info exceeds the maximum allowed size of %s", constants.CloudInitGuestInfoMaxSize.String()), | ||
conditionReason: vmopv1.VirtualMachineGuestInfoSizeExceededReason, | ||
} | ||
} | ||
|
||
var data string | ||
// Check for the 'user-data' key as per official contract and API documentation. | ||
// Additionally, To support the cluster bootstrap data supplied by CAPBK's secret, | ||
|
@@ -173,16 +194,30 @@ func GetCloudInitGuestInfoCustSpec( | |
// Ensure the data is normalized first to plain-text. | ||
plainText, err := util.TryToDecodeBase64Gzip([]byte(data)) | ||
if err != nil { | ||
return nil, fmt.Errorf("decoding cloud-init userdata failed %v", err) | ||
return nil, &ErrWithReason{ | ||
error: fmt.Errorf("decoding cloud-init userdata failed %v", err), | ||
conditionReason: vmopv1.VirtualMachineMetadataFormatInvalidReason, | ||
} | ||
} | ||
|
||
encodedUserdata, err := EncodeGzipBase64(plainText) | ||
if err != nil { | ||
return nil, fmt.Errorf("encoding cloud-init userdata failed %v", err) | ||
return nil, &ErrWithReason{ | ||
error: fmt.Errorf("encoding cloud-init userdata failed %v", err), | ||
conditionReason: vmopv1.VirtualMachineMetadataFormatInvalidReason, | ||
} | ||
} | ||
|
||
extraConfig[constants.CloudInitGuestInfoUserdata] = encodedUserdata | ||
extraConfig[constants.CloudInitGuestInfoUserdataEncoding] = "gzip+base64" | ||
|
||
if len(encodedUserdata) > int(constants.CloudInitGuestInfoMaxSize) || | ||
len(encodedUserdata)+len(encodedMetadata) > int(constants.CloudInitGuestInfoMaxSize) { | ||
return nil, &ErrWithReason{ | ||
error: fmt.Errorf("size of cloud-init guest info exceeds the maximum allowed size of %s", constants.CloudInitGuestInfoMaxSize.String()), | ||
conditionReason: vmopv1.VirtualMachineGuestInfoSizeExceededReason, | ||
} | ||
} | ||
} | ||
|
||
configSpec := &vimTypes.VirtualMachineConfigSpec{} | ||
|
@@ -245,6 +280,11 @@ func customizeCloudInit( | |
|
||
cloudInitMetadata, err := GetCloudInitMetadata(vmCtx.VM, netplan, updateArgs.VMMetadata.Data) | ||
if err != nil { | ||
conditions.MarkFalse(vmCtx.VM, | ||
vmopv1.VirtualMachineMetadataReadyCondition, | ||
vmopv1.VirtualMachineMetadataFormatInvalidReason, | ||
vmopv1.ConditionSeverityError, | ||
err.Error()) | ||
return nil, nil, err | ||
} | ||
|
||
|
@@ -261,6 +301,12 @@ func customizeCloudInit( | |
} | ||
|
||
if err != nil { | ||
errWithReason := err.(ErrWithReason) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was supposed to be |
||
conditions.MarkFalse(vmCtx.VM, | ||
vmopv1.VirtualMachineMetadataReadyCondition, | ||
errWithReason.conditionReason, | ||
vmopv1.ConditionSeverityError, | ||
errWithReason.Error()) | ||
return nil, nil, err | ||
srm09 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
} | ||
|
||
|
@@ -307,6 +353,7 @@ func (s *Session) customize( | |
if err != nil { | ||
return err | ||
} | ||
conditions.MarkTrue(vmCtx.VM, vmopv1.VirtualMachineMetadataReadyCondition) | ||
|
||
if configSpec != nil { | ||
defaultConfigSpec := &vimTypes.VirtualMachineConfigSpec{} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ import ( | |
goctx "context" | ||
"encoding/base64" | ||
"fmt" | ||
"math/rand" | ||
"strings" | ||
|
||
. "github.com/onsi/ginkgo" | ||
|
@@ -402,6 +403,59 @@ var _ = Describe("Cloud-Init Customization", func() { | |
}) | ||
}) | ||
|
||
Context("With cloud init guest info exceeding the maximum size", func() { | ||
generateDataFunc := func(length int) string { | ||
charset := "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" | ||
|
||
sb := strings.Builder{} | ||
sb.Grow(length) | ||
for i := 0; i < length; i++ { | ||
sb.WriteByte(charset[rand.Intn(len(charset))]) //nolint:gosec | ||
} | ||
return sb.String() | ||
} | ||
|
||
Context("With cloud init user data exceeding the maximum size", func() { | ||
BeforeEach(func() { | ||
// Doubling the input to the max allowed size to make sure the compressed and encoded | ||
// output is still above the allowed limit. | ||
data, err := session.EncodeGzipBase64(generateDataFunc(128 * 1000)) | ||
Expect(err).ToNot(HaveOccurred()) | ||
updateArgs.VMMetadata.Data["user-data"] = data | ||
}) | ||
It("will return a limit exceeded error", func() { | ||
Expect(err).To(HaveOccurred()) | ||
Expect(err.Error()).To(ContainSubstring("exceeds the maximum allowed size of 64KiB")) | ||
}) | ||
}) | ||
|
||
Context("With cloud init metadata exceeding the maximum size", func() { | ||
BeforeEach(func() { | ||
// Doubling the input to the max allowed size to make sure the compressed and encoded | ||
// output is still above the allowed limit. | ||
data, err := session.EncodeGzipBase64(generateDataFunc(128 * 1000)) | ||
Expect(err).ToNot(HaveOccurred()) | ||
cloudInitMetadata = data | ||
}) | ||
It("will return a limit exceeded error", func() { | ||
Expect(err).To(HaveOccurred()) | ||
Expect(err.Error()).To(ContainSubstring("exceeds the maximum allowed size of 64KiB")) | ||
}) | ||
}) | ||
|
||
Context("With cloud init metadata + userdata exceeding the maximum size", func() { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is this a valid scenario though? IIRC, the limit is enforced by the size of the value of each GuestInfo key. If both of the keys individually are less than the limit, then we should be fine? |
||
BeforeEach(func() { | ||
data, err := session.EncodeGzipBase64(generateDataFunc(64 * 1000)) | ||
Expect(err).ToNot(HaveOccurred()) | ||
cloudInitMetadata = data | ||
updateArgs.VMMetadata.Data["user-data"] = data | ||
}) | ||
It("will return a limit exceeded error", func() { | ||
Expect(err).To(HaveOccurred()) | ||
Expect(err.Error()).To(ContainSubstring("exceeds the maximum allowed size of 64KiB")) | ||
}) | ||
}) | ||
}) | ||
}) | ||
|
||
Context("GetCloudInitPrepCustSpec", func() { | ||
|
Uh oh!
There was an error while loading. Please reload this page.