diff --git a/.golangci.yml b/.golangci.yml index a0a78372e..143c053c7 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -85,3 +85,6 @@ issues: text: ".* `ctx` is unused" - path: pkg/vmprovider/providers/vsphere/internal/internal.go text: ".*ST1003|don\'t use underscores in Go names.*" + - path: _test.go + linters: + - gosec diff --git a/api/v1alpha2/sysprep/sysprep.go b/api/v1alpha2/sysprep/sysprep.go index 9387bd7e9..45535b920 100644 --- a/api/v1alpha2/sysprep/sysprep.go +++ b/api/v1alpha2/sysprep/sysprep.go @@ -25,10 +25,10 @@ type Sysprep struct { GUIRunOnce GUIRunOnce `json:"guiRunOnce,omitempty"` // GUIUnattended is a representation of the Sysprep GUIUnattended key. - GUIUnattended GUIUnattended `json:"guiUnattended"` + GUIUnattended *GUIUnattended `json:"guiUnattended"` // Identification is a representation of the Sysprep Identification key. - Identification Identification `json:"identification"` + Identification *Identification `json:"identification"` // LicenseFilePrintData is a representation of the Sysprep // LicenseFilePrintData key. @@ -40,7 +40,7 @@ type Sysprep struct { LicenseFilePrintData *LicenseFilePrintData `json:"licenseFilePrintData,omitempty"` // UserData is a representation of the Sysprep UserData key. - UserData UserData `json:"userData"` + UserData *UserData `json:"userData"` } // GUIRunOnce maps to the GuiRunOnce key in the sysprep.xml answer file. @@ -55,10 +55,10 @@ type GUIRunOnce struct { // GUIUnattended maps to the GuiUnattended key in the sysprep.xml answer file. type GUIUnattended struct { - // AutoLogon determine whether or not the machine automatically logs on as + // AutoLogon determine whether the machine automatically logs on as // Administrator. // - // Please note if AutoLogin is true, then Password must be set or guest + // Please note if AutoLogon is true, then Password must be set or guest // customization will fail. // // +optional @@ -71,7 +71,7 @@ type GUIUnattended struct { // you may want to increase it. This number may be determined by the list of // commands executed by the GuiRunOnce command. // - // Please note this field only matters if AutoLogin is true. + // Please note this field only matters if AutoLogon is true. // // +optional AutoLogonCount int32 `json:"autoLogonCount,omitempty"` @@ -90,6 +90,9 @@ type GUIUnattended struct { // plainText attribute to true, so that the customization process does not // attempt to decrypt the string. // + // When not explicitly specified, the Key field for the selector defaults to + // `password`. + // // +optional Password *common.SecretKeySelector `json:"password,omitempty"` @@ -117,6 +120,9 @@ type Identification struct { // DomainAdminPassword is the password for the domain user account used for // authentication if the virtual machine is joining a domain. // + // When not explicitly specified, the Key field for the selector defaults to + // `domain_admin_password`. + // // +optional DomainAdminPassword *common.SecretKeySelector `json:"domainAdminPassword,omitempty"` @@ -188,6 +194,9 @@ type UserData struct { // Please note unless the VirtualMachineImage was installed with a volume // license key, ProductID must be set or guest customization will fail. // + // When not explicitly specified, the Key field for the selector defaults to + // `domain_admin_password`. + // // +optional ProductID *common.SecretKeySelector `json:"productID,omitempty"` } diff --git a/api/v1alpha2/sysprep/zz_generated.deepcopy.go b/api/v1alpha2/sysprep/zz_generated.deepcopy.go index b37fa49e9..24b692090 100644 --- a/api/v1alpha2/sysprep/zz_generated.deepcopy.go +++ b/api/v1alpha2/sysprep/zz_generated.deepcopy.go @@ -96,14 +96,26 @@ func (in *LicenseFilePrintData) DeepCopy() *LicenseFilePrintData { func (in *Sysprep) DeepCopyInto(out *Sysprep) { *out = *in in.GUIRunOnce.DeepCopyInto(&out.GUIRunOnce) - in.GUIUnattended.DeepCopyInto(&out.GUIUnattended) - in.Identification.DeepCopyInto(&out.Identification) + if in.GUIUnattended != nil { + in, out := &in.GUIUnattended, &out.GUIUnattended + *out = new(GUIUnattended) + (*in).DeepCopyInto(*out) + } + if in.Identification != nil { + in, out := &in.Identification, &out.Identification + *out = new(Identification) + (*in).DeepCopyInto(*out) + } if in.LicenseFilePrintData != nil { in, out := &in.LicenseFilePrintData, &out.LicenseFilePrintData *out = new(LicenseFilePrintData) (*in).DeepCopyInto(*out) } - in.UserData.DeepCopyInto(&out.UserData) + if in.UserData != nil { + in, out := &in.UserData, &out.UserData + *out = new(UserData) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Sysprep. diff --git a/config/crd/bases/vmoperator.vmware.com_virtualmachines.yaml b/config/crd/bases/vmoperator.vmware.com_virtualmachines.yaml index ebaebdea5..d534c9b9a 100644 --- a/config/crd/bases/vmoperator.vmware.com_virtualmachines.yaml +++ b/config/crd/bases/vmoperator.vmware.com_virtualmachines.yaml @@ -1085,10 +1085,10 @@ spec: Sysprep GUIUnattended key. properties: autoLogon: - description: "AutoLogon determine whether or not the - machine automatically logs on as Administrator. - \n Please note if AutoLogin is true, then Password - must be set or guest customization will fail." + description: "AutoLogon determine whether the machine + automatically logs on as Administrator. \n Please + note if AutoLogon is true, then Password must be + set or guest customization will fail." type: boolean autoLogonCount: description: "AutoLogonCount specifies the number @@ -1098,7 +1098,7 @@ spec: may want to increase it. This number may be determined by the list of commands executed by the GuiRunOnce command. \n Please note this field only matters - if AutoLogin is true." + if AutoLogon is true." format: int32 type: integer password: @@ -1113,7 +1113,9 @@ spec: Wizard, then the password is encrypted. Otherwise, the client should set the plainText attribute to true, so that the customization process does not - attempt to decrypt the string." + attempt to decrypt the string. \n When not explicitly + specified, the Key field for the selector defaults + to `password`." properties: key: description: Key is the key in the secret that @@ -1146,9 +1148,11 @@ spec: domain. type: string domainAdminPassword: - description: DomainAdminPassword is the password for - the domain user account used for authentication - if the virtual machine is joining a domain. + description: "DomainAdminPassword is the password + for the domain user account used for authentication + if the virtual machine is joining a domain. \n When + not explicitly specified, the Key field for the + selector defaults to `domain_admin_password`." properties: key: description: Key is the key in the secret that @@ -1212,7 +1216,9 @@ spec: description: "ProductID is a valid serial number. \n Please note unless the VirtualMachineImage was installed with a volume license key, ProductID must - be set or guest customization will fail." + be set or guest customization will fail. \n When + not explicitly specified, the Key field for the + selector defaults to `domain_admin_password`." properties: key: description: Key is the key in the secret that diff --git a/pkg/util/enc_test.go b/pkg/util/enc_test.go index 6d22f13e5..ff05fd617 100644 --- a/pkg/util/enc_test.go +++ b/pkg/util/enc_test.go @@ -52,9 +52,9 @@ var _ = Describe("TryToDecodeBase64Gzip", func() { gz := func(data []byte) []byte { var w bytes.Buffer gzw := gzip.NewWriter(&w) - //nolint:errcheck,gosec + //nolint:errcheck gzw.Write(data) - //nolint:errcheck,gosec + //nolint:errcheck gzw.Close() return w.Bytes() } diff --git a/pkg/util/secret.go b/pkg/util/secret.go new file mode 100644 index 000000000..1b5dfeff5 --- /dev/null +++ b/pkg/util/secret.go @@ -0,0 +1,45 @@ +// Copyright (c) 2023 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package util + +import ( + "context" + "fmt" + + corev1 "k8s.io/api/core/v1" + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" +) + +func GetSecretData( + ctx context.Context, + k8sClient ctrlclient.Client, + secretNamespace, secretName, secretKey string, + out *string) error { + + secret, err := GetSecretResource(ctx, k8sClient, secretNamespace, secretName) + if err != nil { + return err + } + data := secret.Data[secretKey] + if len(data) == 0 { + return fmt.Errorf( + "no data found for key %q for secret %s/%s", + secretKey, secretNamespace, secretName) + } + *out = string(data) + return nil +} + +func GetSecretResource( + ctx context.Context, + k8sClient ctrlclient.Client, + secretNamespace, secretName string) (*corev1.Secret, error) { + + var secret corev1.Secret + key := ctrlclient.ObjectKey{Name: secretName, Namespace: secretNamespace} + if err := k8sClient.Get(ctx, key, &secret); err != nil { + return nil, err + } + return &secret, nil +} diff --git a/pkg/vmprovider/providers/vsphere/session/session_vm_customization_test.go b/pkg/vmprovider/providers/vsphere/session/session_vm_customization_test.go index ae584d620..fa325935a 100644 --- a/pkg/vmprovider/providers/vsphere/session/session_vm_customization_test.go +++ b/pkg/vmprovider/providers/vsphere/session/session_vm_customization_test.go @@ -798,7 +798,6 @@ data: nicMacAddr: "{{ V1alpha1_FirstNicMacAddr }}" ` -//nolint:gosec const testSecretYAML1 = ` apiVersion: v1 kind: Secret diff --git a/pkg/vmprovider/providers/vsphere/vmprovider_vm_test.go b/pkg/vmprovider/providers/vsphere/vmprovider_vm_test.go index 32884a003..0d16d7cde 100644 --- a/pkg/vmprovider/providers/vsphere/vmprovider_vm_test.go +++ b/pkg/vmprovider/providers/vsphere/vmprovider_vm_test.go @@ -1316,7 +1316,7 @@ func vmTests() { }) It("creates VM in assigned zone", func() { - azName := ctx.ZoneNames[rand.Intn(len(ctx.ZoneNames))] //nolint:gosec + azName := ctx.ZoneNames[rand.Intn(len(ctx.ZoneNames))] vm.Labels[topology.KubernetesTopologyZoneLabelKey] = azName vcVM, err := createOrUpdateAndGetVcVM(ctx, vm) diff --git a/pkg/vmprovider/providers/vsphere2/cloudinit/cloudconfig.go b/pkg/vmprovider/providers/vsphere2/cloudinit/cloudconfig.go index ad34962f3..3e9b1f162 100644 --- a/pkg/vmprovider/providers/vsphere2/cloudinit/cloudconfig.go +++ b/pkg/vmprovider/providers/vsphere2/cloudinit/cloudconfig.go @@ -13,6 +13,7 @@ import ( "github.com/vmware-tanzu/vm-operator/api/v1alpha2/cloudinit" "github.com/vmware-tanzu/vm-operator/api/v1alpha2/common" + "github.com/vmware-tanzu/vm-operator/pkg/util" ) // cloudConfig provides support for marshalling the object to a valid @@ -246,7 +247,7 @@ func GetSecretResources( for i := range in.Users { if v := in.Users[i].HashedPasswd; v != nil { - s, err := getSecretResource( + s, err := util.GetSecretResource( ctx, k8sClient, secretNamespace, @@ -257,7 +258,7 @@ func GetSecretResources( captureSecret(s, v.Name) } if v := in.Users[i].Passwd; v != nil { - s, err := getSecretResource( + s, err := util.GetSecretResource( ctx, k8sClient, secretNamespace, @@ -273,7 +274,7 @@ func GetSecretResources( if v := in.WriteFiles[i].Content; len(v) > 0 { var sks common.SecretKeySelector if err := yaml.Unmarshal(v, &sks); err == nil { - s, err := getSecretResource( + s, err := util.GetSecretResource( ctx, k8sClient, secretNamespace, diff --git a/pkg/vmprovider/providers/vsphere2/cloudinit/cloudconfig_secret.go b/pkg/vmprovider/providers/vsphere2/cloudinit/cloudconfig_secret.go index ff88335a4..0bd1daf8a 100644 --- a/pkg/vmprovider/providers/vsphere2/cloudinit/cloudconfig_secret.go +++ b/pkg/vmprovider/providers/vsphere2/cloudinit/cloudconfig_secret.go @@ -8,11 +8,11 @@ import ( "fmt" "gopkg.in/yaml.v3" - corev1 "k8s.io/api/core/v1" ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" "github.com/vmware-tanzu/vm-operator/api/v1alpha2/cloudinit" "github.com/vmware-tanzu/vm-operator/api/v1alpha2/common" + "github.com/vmware-tanzu/vm-operator/pkg/util" ) // CloudConfigSecretData is used to provide the sensitive data that may have @@ -93,7 +93,7 @@ func getSecretDataForUser( } if v := in.HashedPasswd; v != nil { - if err := getSecretData( + if err := util.GetSecretData( ctx, k8sClient, secretNamespace, v.Name, v.Key, &out.HashPasswd); err != nil { @@ -102,7 +102,7 @@ func getSecretDataForUser( } } if v := in.Passwd; v != nil { - if err := getSecretData( + if err := util.GetSecretData( ctx, k8sClient, secretNamespace, v.Name, v.Key, &out.Passwd); err != nil { @@ -140,7 +140,7 @@ func getSecretDataForWriteFile( return err } - if err := getSecretData( + if err := util.GetSecretData( ctx, k8sClient, secretNamespace, @@ -152,36 +152,3 @@ func getSecretDataForWriteFile( return nil } - -func getSecretData( - ctx context.Context, - k8sClient ctrlclient.Client, - secretNamespace, secretName, secretKey string, - out *string) error { - - secret, err := getSecretResource(ctx, k8sClient, secretNamespace, secretName) - if err != nil { - return err - } - data := secret.Data[secretKey] - if len(data) == 0 { - return fmt.Errorf( - "no data found for key %q for secret %s/%s", - secretKey, secretNamespace, secretName) - } - *out = string(data) - return nil -} - -func getSecretResource( - ctx context.Context, - k8sClient ctrlclient.Client, - secretNamespace, secretName string) (*corev1.Secret, error) { - - var secret corev1.Secret - key := ctrlclient.ObjectKey{Name: secretName, Namespace: secretNamespace} - if err := k8sClient.Get(ctx, key, &secret); err != nil { - return nil, err - } - return &secret, nil -} diff --git a/pkg/vmprovider/providers/vsphere2/sysprep/secret.go b/pkg/vmprovider/providers/vsphere2/sysprep/secret.go new file mode 100644 index 000000000..078551b0b --- /dev/null +++ b/pkg/vmprovider/providers/vsphere2/sysprep/secret.go @@ -0,0 +1,66 @@ +// Copyright (c) 2023 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package sysprep + +import ( + "context" + + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + + "github.com/vmware-tanzu/vm-operator/api/v1alpha2/sysprep" + "github.com/vmware-tanzu/vm-operator/pkg/util" +) + +type SecretData struct { + ProductID, Password, DomainPassword string +} + +func GetSysprepSecretData( + ctx context.Context, + k8sClient ctrlclient.Client, + secretNamespace string, + in *sysprep.Sysprep) (SecretData, error) { + + var ( + productID, password, domainPwd string + ) + + if userData := in.UserData; userData != nil && userData.ProductID != nil { + // this is an optional secret key selector even when FullName or OrgName are set. + err := util.GetSecretData(ctx, k8sClient, secretNamespace, userData.ProductID.Name, userData.ProductID.Key, &productID) + if err != nil { + return SecretData{}, err + } + } + + if guiUnattended := in.GUIUnattended; guiUnattended != nil && guiUnattended.AutoLogon { + err := util.GetSecretData(ctx, + k8sClient, + secretNamespace, + guiUnattended.Password.Name, + guiUnattended.Password.Key, + &password) + if err != nil { + return SecretData{}, err + } + } + + if identification := in.Identification; identification != nil && identification.JoinDomain != "" { + err := util.GetSecretData(ctx, + k8sClient, + secretNamespace, + identification.DomainAdminPassword.Name, + identification.DomainAdminPassword.Key, + &domainPwd) + if err != nil { + return SecretData{}, err + } + } + + return SecretData{ + ProductID: productID, + Password: password, + DomainPassword: domainPwd, + }, nil +} diff --git a/pkg/vmprovider/providers/vsphere2/sysprep/secret_test.go b/pkg/vmprovider/providers/vsphere2/sysprep/secret_test.go new file mode 100644 index 000000000..47aa4aba7 --- /dev/null +++ b/pkg/vmprovider/providers/vsphere2/sysprep/secret_test.go @@ -0,0 +1,221 @@ +// Copyright (c) 2023 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package sysprep_test + +import ( + "context" + "fmt" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + + ctrlclient "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/client/fake" + + "github.com/vmware-tanzu/vm-operator/api/v1alpha2/common" + vmopv1sysprep "github.com/vmware-tanzu/vm-operator/api/v1alpha2/sysprep" + "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/sysprep" +) + +var _ = Describe("CloudConfig GetCloudConfigSecretData", func() { + var ( + err error + ctx context.Context + k8sClient ctrlclient.Client + initialObjects []ctrlclient.Object + secretNamespace string + sysprepSecretData sysprep.SecretData + inlineSysprep vmopv1sysprep.Sysprep + ) + + const anotherKey = "some_other_key" + + BeforeEach(func() { + err = nil + initialObjects = nil + ctx = context.Background() + secretNamespace = "default" + }) + + JustBeforeEach(func() { + k8sClient = fake.NewClientBuilder().WithObjects(initialObjects...).Build() + sysprepSecretData, err = sysprep.GetSysprepSecretData( + ctx, + k8sClient, + secretNamespace, + &inlineSysprep) + }) + + Context("for UserData", func() { + productIDSecretName := "product_id_secret" + BeforeEach(func() { + inlineSysprep = vmopv1sysprep.Sysprep{ + UserData: &vmopv1sysprep.UserData{ + ProductID: &common.SecretKeySelector{ + Name: productIDSecretName, + Key: "product_id", + }, + }, + } + }) + + When("secret is present", func() { + BeforeEach(func() { + initialObjects = append(initialObjects, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: productIDSecretName, + Namespace: secretNamespace, + }, + Data: map[string][]byte{ + "product_id": []byte("foo_product_id"), + }, + }) + }) + + It("returns success", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(sysprepSecretData.ProductID).To(Equal("foo_product_id")) + }) + + When("key from selector is not present", func() { + BeforeEach(func() { + inlineSysprep.UserData.ProductID.Key = anotherKey + }) + + It("returns an error", func() { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal(fmt.Sprintf(`no data found for key "%s" for secret default/%s`, anotherKey, productIDSecretName))) + }) + }) + }) + + When("secret is not present", func() { + It("returns an error", func() { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal(fmt.Sprintf(`secrets "%s" not found`, productIDSecretName))) + }) + }) + + When("secret selector is absent", func() { + + BeforeEach(func() { + inlineSysprep.UserData.FullName = "foo" + inlineSysprep.UserData.ProductID = nil + }) + + It("does not return an error", func() { + Expect(err).ToNot(HaveOccurred()) + }) + }) + }) + + Context("for GUIUnattended", func() { + pwdSecretName := "password_secret" + + BeforeEach(func() { + inlineSysprep = vmopv1sysprep.Sysprep{ + GUIUnattended: &vmopv1sysprep.GUIUnattended{ + AutoLogon: true, + Password: &common.SecretKeySelector{ + Name: pwdSecretName, + Key: "password", + }, + }, + } + }) + + When("secret is present", func() { + BeforeEach(func() { + initialObjects = append(initialObjects, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: pwdSecretName, + Namespace: secretNamespace, + }, + Data: map[string][]byte{ + "password": []byte("foo_bar123"), + }, + }) + }) + + It("returns success", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(sysprepSecretData.Password).To(Equal("foo_bar123")) + }) + + When("key from selector is not present", func() { + BeforeEach(func() { + inlineSysprep.GUIUnattended.Password.Key = anotherKey + }) + + It("returns an error", func() { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal(fmt.Sprintf(`no data found for key "%s" for secret default/%s`, anotherKey, pwdSecretName))) + }) + }) + }) + + When("secret is not present", func() { + It("returns an error", func() { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal(fmt.Sprintf(`secrets "%s" not found`, pwdSecretName))) + }) + }) + }) + + Context("for Identification", func() { + pwdSecretName := "domain_password_secret" + + BeforeEach(func() { + inlineSysprep = vmopv1sysprep.Sysprep{ + Identification: &vmopv1sysprep.Identification{ + JoinDomain: "foo", + DomainAdminPassword: &common.SecretKeySelector{ + Name: pwdSecretName, + Key: "domain_password", + }, + }, + } + }) + + When("secret is present", func() { + BeforeEach(func() { + initialObjects = append(initialObjects, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: pwdSecretName, + Namespace: secretNamespace, + }, + Data: map[string][]byte{ + "domain_password": []byte("foo_bar_fizz123"), + }, + }) + }) + + It("returns success", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(sysprepSecretData.DomainPassword).To(Equal("foo_bar_fizz123")) + }) + + When("key from selector is not present", func() { + BeforeEach(func() { + inlineSysprep.Identification.DomainAdminPassword.Key = anotherKey + }) + + It("returns an error", func() { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal(fmt.Sprintf(`no data found for key "%s" for secret default/%s`, anotherKey, pwdSecretName))) + }) + }) + }) + + When("secret is not present", func() { + It("returns an error", func() { + Expect(err).To(HaveOccurred()) + Expect(err.Error()).To(Equal(fmt.Sprintf(`secrets "%s" not found`, pwdSecretName))) + }) + }) + }) +}) diff --git a/pkg/vmprovider/providers/vsphere2/sysprep/suite_test.go b/pkg/vmprovider/providers/vsphere2/sysprep/suite_test.go new file mode 100644 index 000000000..faf40bb0b --- /dev/null +++ b/pkg/vmprovider/providers/vsphere2/sysprep/suite_test.go @@ -0,0 +1,22 @@ +// Copyright (c) 2023 VMware, Inc. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +package sysprep_test + +import ( + "testing" + + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "github.com/vmware-tanzu/vm-operator/test/builder" +) + +var suite = builder.NewTestSuite() +var _ = BeforeSuite(suite.BeforeSuite) +var _ = AfterSuite(suite.AfterSuite) + +func TestCloudInit(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "vSphere Provider Sysprep Suite") +} diff --git a/pkg/vmprovider/providers/vsphere2/vmlifecycle/bootstrap.go b/pkg/vmprovider/providers/vsphere2/vmlifecycle/bootstrap.go index 0ab2f31b9..63486b427 100644 --- a/pkg/vmprovider/providers/vsphere2/vmlifecycle/bootstrap.go +++ b/pkg/vmprovider/providers/vsphere2/vmlifecycle/bootstrap.go @@ -19,6 +19,7 @@ import ( "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/constants" "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/network" "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/resources" + "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/sysprep" ) const ( @@ -33,6 +34,7 @@ type BootstrapData struct { VAppExData map[string]map[string]string CloudConfig *cloudinit.CloudConfigSecretData + Sysprep *sysprep.SecretData } type TemplateRenderFunc func(string, string) string diff --git a/pkg/vmprovider/providers/vsphere2/vmlifecycle/bootstrap_sysprep.go b/pkg/vmprovider/providers/vsphere2/vmlifecycle/bootstrap_sysprep.go index 87e49f8b0..c1e0e06f9 100644 --- a/pkg/vmprovider/providers/vsphere2/vmlifecycle/bootstrap_sysprep.go +++ b/pkg/vmprovider/providers/vsphere2/vmlifecycle/bootstrap_sysprep.go @@ -10,18 +10,22 @@ import ( vimTypes "github.com/vmware/govmomi/vim25/types" vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha2" + vmopv1sysprep "github.com/vmware-tanzu/vm-operator/api/v1alpha2/sysprep" "github.com/vmware-tanzu/vm-operator/pkg/util" "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/network" ) func BootstrapSysPrep( - ctx goctx.Context, + _ goctx.Context, config *vimTypes.VirtualMachineConfigInfo, sysPrepSpec *vmopv1.VirtualMachineBootstrapSysprepSpec, vAppConfigSpec *vmopv1.VirtualMachineBootstrapVAppConfigSpec, bsArgs *BootstrapArgs) (*vimTypes.VirtualMachineConfigSpec, *vimTypes.CustomizationSpec, error) { - var data string + var ( + data string + identity vimTypes.BaseCustomizationIdentitySettings + ) key := "unattend" if sysPrepSpec.RawSysprep != nil { @@ -42,12 +46,15 @@ func BootstrapSysPrep( return nil, nil, fmt.Errorf("decoding Sysprep unattend XML failed: %w", err) } - } else if sysPrepSpec.Sysprep != nil { - return nil, nil, fmt.Errorf("TODO: inlined Sysprep") - } + if bsArgs.TemplateRenderFn != nil { + data = bsArgs.TemplateRenderFn(key, data) + } - if bsArgs.TemplateRenderFn != nil { - data = bsArgs.TemplateRenderFn(key, data) + identity = &vimTypes.CustomizationSysprepText{ + Value: data, + } + } else if sysPrep := sysPrepSpec.Sysprep; sysPrep != nil { + identity = convertTo(sysPrep, bsArgs.BootstrapData) } nicSettingMap, err := network.GuestOSCustomization(bsArgs.NetworkResults) @@ -56,9 +63,7 @@ func BootstrapSysPrep( } customSpec := &vimTypes.CustomizationSpec{ - Identity: &vimTypes.CustomizationSysprepText{ - Value: data, - }, + Identity: identity, GlobalIPSettings: vimTypes.CustomizationGlobalIPSettings{ DnsSuffixList: bsArgs.SearchSuffixes, DnsServerList: bsArgs.DNSServers, @@ -79,3 +84,78 @@ func BootstrapSysPrep( return configSpec, customSpec, nil } + +func convertTo(from *vmopv1sysprep.Sysprep, bootstrapData BootstrapData) *vimTypes.CustomizationSysprep { + sysprepCustomization := &vimTypes.CustomizationSysprep{} + + if from.GUIUnattended != nil { + sysprepCustomization.GuiUnattended = vimTypes.CustomizationGuiUnattended{ + TimeZone: from.GUIUnattended.TimeZone, + AutoLogon: from.GUIUnattended.AutoLogon, + AutoLogonCount: from.GUIUnattended.AutoLogonCount, + } + if bootstrapData.Sysprep.Password != "" { + sysprepCustomization.GuiUnattended.Password = &vimTypes.CustomizationPassword{ + Value: bootstrapData.Sysprep.Password, + PlainText: true, + } + } + } + + if from.UserData != nil { + sysprepCustomization.UserData = vimTypes.CustomizationUserData{ + FullName: from.UserData.FullName, + OrgName: from.UserData.OrgName, + } + // In the case of a VMI with volume license key, this might not be set. + // Hence add a check to see if the productID is set to empty. + if bootstrapData.Sysprep.ProductID != "" { + sysprepCustomization.UserData.ProductId = bootstrapData.Sysprep.ProductID + } + } + + sysprepCustomization.GuiRunOnce = &vimTypes.CustomizationGuiRunOnce{ + CommandList: from.GUIRunOnce.Commands, + } + + if from.Identification != nil { + sysprepCustomization.Identification = vimTypes.CustomizationIdentification{ + JoinWorkgroup: from.Identification.JoinWorkgroup, + JoinDomain: from.Identification.JoinDomain, + DomainAdmin: from.Identification.DomainAdmin, + } + if bootstrapData.Sysprep.DomainPassword != "" { + sysprepCustomization.Identification.DomainAdminPassword = &vimTypes.CustomizationPassword{ + Value: bootstrapData.Sysprep.DomainPassword, + PlainText: true, + } + } + } + + if from.LicenseFilePrintData != nil { + sysprepCustomization.LicenseFilePrintData = convertLicenseFilePrintDataTo(from.LicenseFilePrintData) + } + + return sysprepCustomization +} + +func convertLicenseFilePrintDataTo(from *vmopv1sysprep.LicenseFilePrintData) *vimTypes.CustomizationLicenseFilePrintData { + custLicenseFilePrintData := &vimTypes.CustomizationLicenseFilePrintData{ + AutoMode: parseLicenseDataMode(from.AutoMode), + } + if from.AutoUsers != nil { + custLicenseFilePrintData.AutoUsers = *from.AutoUsers + } + return custLicenseFilePrintData +} + +func parseLicenseDataMode(mode vmopv1sysprep.CustomizationLicenseDataMode) vimTypes.CustomizationLicenseDataMode { + switch mode { + case vmopv1sysprep.CustomizationLicenseDataModePerServer: + return vimTypes.CustomizationLicenseDataModePerServer + case vmopv1sysprep.CustomizationLicenseDataModePerSeat: + return vimTypes.CustomizationLicenseDataModePerSeat + default: + return "" + } +} diff --git a/pkg/vmprovider/providers/vsphere2/vmlifecycle/bootstrap_sysprep_test.go b/pkg/vmprovider/providers/vsphere2/vmlifecycle/bootstrap_sysprep_test.go index 8d8ec25e2..4c2a7f87b 100644 --- a/pkg/vmprovider/providers/vsphere2/vmlifecycle/bootstrap_sysprep_test.go +++ b/pkg/vmprovider/providers/vsphere2/vmlifecycle/bootstrap_sysprep_test.go @@ -8,16 +8,16 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - "github.com/vmware/govmomi/vim25/types" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/utils/pointer" vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha2" "github.com/vmware-tanzu/vm-operator/api/v1alpha2/common" - "github.com/vmware-tanzu/vm-operator/api/v1alpha2/sysprep" + vmopv1sysprep "github.com/vmware-tanzu/vm-operator/api/v1alpha2/sysprep" "github.com/vmware-tanzu/vm-operator/pkg/context" "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/network" + "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/sysprep" "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/vmlifecycle" ) @@ -98,13 +98,74 @@ var _ = Describe("SysPrep Bootstrap", func() { }) Context("Inlined Sysprep", func() { + autoUsers := int32(5) + password, domainPassword, productID := "password_foo", "admin_password_foo", "product_id_foo" + BeforeEach(func() { - sysPrepSpec.Sysprep = &sysprep.Sysprep{} + sysPrepSpec.Sysprep = &vmopv1sysprep.Sysprep{ + GUIUnattended: &vmopv1sysprep.GUIUnattended{ + AutoLogon: true, + AutoLogonCount: 2, + Password: &common.SecretKeySelector{ + // omitting the name of the secret, since it does not get used + // in this function + Key: "pwd_key", + }, + TimeZone: 4, + }, + UserData: &vmopv1sysprep.UserData{ + FullName: "foo-bar", + OrgName: "foo-org", + ProductID: &common.SecretKeySelector{Key: "product_id_key"}, + }, + GUIRunOnce: vmopv1sysprep.GUIRunOnce{ + Commands: []string{"blah", "boom"}, + }, + Identification: &vmopv1sysprep.Identification{ + DomainAdmin: "[Foo/Administrator]", + JoinDomain: "foo.local", + JoinWorkgroup: "foo.local.wg", + DomainAdminPassword: &common.SecretKeySelector{Key: "admin_pwd_key"}, + }, + LicenseFilePrintData: &vmopv1sysprep.LicenseFilePrintData{ + AutoMode: vmopv1sysprep.CustomizationLicenseDataModePerServer, + AutoUsers: &autoUsers, + }, + } + + // secret data gets populated into the bootstrapArgs + bsArgs.Sysprep = &sysprep.SecretData{ + ProductID: productID, + Password: password, + DomainPassword: domainPassword, + } }) - It("Returns TODO", func() { - Expect(err).To(HaveOccurred()) - Expect(err.Error()).To(ContainSubstring("TODO")) + It("should return expected customization spec", func() { + Expect(err).ToNot(HaveOccurred()) + Expect(custSpec).ToNot(BeNil()) + + sysPrep, ok := custSpec.Identity.(*types.CustomizationSysprep) + Expect(ok).To(BeTrue()) + + Expect(sysPrep.GuiUnattended.TimeZone).To(Equal(int32(4))) + Expect(sysPrep.GuiUnattended.AutoLogonCount).To(Equal(int32(2))) + Expect(sysPrep.GuiUnattended.AutoLogon).To(BeTrue()) + Expect(sysPrep.GuiUnattended.Password.Value).To(Equal(password)) + + Expect(sysPrep.UserData.FullName).To(Equal("foo-bar")) + Expect(sysPrep.UserData.OrgName).To(Equal("foo-org")) + Expect(sysPrep.UserData.ProductId).To(Equal(productID)) + + Expect(sysPrep.GuiRunOnce.CommandList).To(HaveLen(2)) + + Expect(sysPrep.Identification.DomainAdmin).To(Equal("[Foo/Administrator]")) + Expect(sysPrep.Identification.JoinDomain).To(Equal("foo.local")) + Expect(sysPrep.Identification.DomainAdminPassword.Value).To(Equal(domainPassword)) + Expect(sysPrep.Identification.JoinWorkgroup).To(Equal("foo.local.wg")) + + Expect(sysPrep.LicenseFilePrintData.AutoMode).To(Equal(types.CustomizationLicenseDataModePerServer)) + Expect(sysPrep.LicenseFilePrintData.AutoUsers).To(Equal(autoUsers)) }) }) diff --git a/pkg/vmprovider/providers/vsphere2/vmlifecycle/bootstrap_vappconfig.go b/pkg/vmprovider/providers/vsphere2/vmlifecycle/bootstrap_vappconfig.go index 241f28928..4fc319037 100644 --- a/pkg/vmprovider/providers/vsphere2/vmlifecycle/bootstrap_vappconfig.go +++ b/pkg/vmprovider/providers/vsphere2/vmlifecycle/bootstrap_vappconfig.go @@ -12,7 +12,7 @@ import ( ) func BootstrapVAppConfig( - ctx goctx.Context, + _ goctx.Context, config *vimTypes.VirtualMachineConfigInfo, vAppConfigSpec *vmopv1.VirtualMachineBootstrapVAppConfigSpec, bsArgs *BootstrapArgs) (*vimTypes.VirtualMachineConfigSpec, *vimTypes.CustomizationSpec, error) { diff --git a/pkg/vmprovider/providers/vsphere2/vmprovider_vm_test.go b/pkg/vmprovider/providers/vsphere2/vmprovider_vm_test.go index ac16f9a85..c3018aa14 100644 --- a/pkg/vmprovider/providers/vsphere2/vmprovider_vm_test.go +++ b/pkg/vmprovider/providers/vsphere2/vmprovider_vm_test.go @@ -1211,7 +1211,7 @@ func vmTests() { }) It("creates VM in assigned zone", func() { - azName := ctx.ZoneNames[rand.Intn(len(ctx.ZoneNames))] //nolint:gosec + azName := ctx.ZoneNames[rand.Intn(len(ctx.ZoneNames))] vm.Labels[topology.KubernetesTopologyZoneLabelKey] = azName vcVM, err := createOrUpdateAndGetVcVM(ctx, vm) diff --git a/pkg/vmprovider/providers/vsphere2/vmprovider_vm_utils.go b/pkg/vmprovider/providers/vsphere2/vmprovider_vm_utils.go index a3a074c59..d85bc4e7f 100644 --- a/pkg/vmprovider/providers/vsphere2/vmprovider_vm_utils.go +++ b/pkg/vmprovider/providers/vsphere2/vmprovider_vm_utils.go @@ -20,6 +20,7 @@ import ( "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/cloudinit" "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/constants" "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/instancestorage" + "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/sysprep" "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/vmlifecycle" ) @@ -164,6 +165,7 @@ func GetVirtualMachineBootstrap( var data, vAppData map[string]string var vAppExData map[string]map[string]string var cloudConfigSecretData *cloudinit.CloudConfigSecretData + var sysprepSecretData *sysprep.SecretData if v := bootstrapSpec.CloudInit; v != nil { if cooked := v.CloudConfig; cooked != nil { @@ -189,7 +191,17 @@ func GetVirtualMachineBootstrap( } } else if v := bootstrapSpec.Sysprep; v != nil { if cooked := v.Sysprep; cooked != nil { - _ = cooked // TODO Add support for in-line sysprep + out, err := sysprep.GetSysprepSecretData( + vmCtx, + k8sClient, + vmCtx.VM.Namespace, + cooked) + if err != nil { + reason, msg := errToConditionReasonAndMessage(err) + conditions.MarkFalse(vmCtx.VM, vmopv1.VirtualMachineConditionBootstrapReady, reason, msg) + return vmlifecycle.BootstrapData{}, err + } + sysprepSecretData = &out } else if raw := v.RawSysprep; raw != nil { var err error data, err = getSecretData(vmCtx, k8sClient, raw.Name, raw.Key, true) @@ -252,6 +264,7 @@ func GetVirtualMachineBootstrap( VAppData: vAppData, VAppExData: vAppExData, CloudConfig: cloudConfigSecretData, + Sysprep: sysprepSecretData, }, nil } diff --git a/pkg/vmprovider/providers/vsphere2/vmprovider_vm_utils_test.go b/pkg/vmprovider/providers/vsphere2/vmprovider_vm_utils_test.go index c32542460..ffd0e882e 100644 --- a/pkg/vmprovider/providers/vsphere2/vmprovider_vm_utils_test.go +++ b/pkg/vmprovider/providers/vsphere2/vmprovider_vm_utils_test.go @@ -9,7 +9,6 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" - "github.com/vmware/govmomi/vim25/types" corev1 "k8s.io/api/core/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -17,6 +16,7 @@ import ( vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha2" "github.com/vmware-tanzu/vm-operator/api/v1alpha2/common" + "github.com/vmware-tanzu/vm-operator/api/v1alpha2/sysprep" conditions "github.com/vmware-tanzu/vm-operator/pkg/conditions2" "github.com/vmware-tanzu/vm-operator/pkg/context" "github.com/vmware-tanzu/vm-operator/pkg/lib" @@ -321,7 +321,7 @@ func vmUtilTests() { }) - When("Bootstrap via Sysprep", func() { + When("Bootstrap via RawSysprep", func() { BeforeEach(func() { vmCtx.VM.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ Sysprep: &vmopv1.VirtualMachineBootstrapSysprepSpec{}, @@ -365,6 +365,200 @@ func vmUtilTests() { }) }) + Context("Bootstrap via inline Sysprep", func() { + anotherKey := "some_other_key" + + BeforeEach(func() { + vmCtx.VM.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ + Sysprep: &vmopv1.VirtualMachineBootstrapSysprepSpec{}, + } + }) + + Context("for UserData", func() { + productIDSecretName := "product_id_secret" + BeforeEach(func() { + vmCtx.VM.Spec.Bootstrap.Sysprep.Sysprep = &sysprep.Sysprep{ + UserData: &sysprep.UserData{ + ProductID: &common.SecretKeySelector{ + Name: productIDSecretName, + Key: "product_id", + }, + }, + } + }) + + When("secret is present", func() { + BeforeEach(func() { + initObjects = append(initObjects, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: productIDSecretName, + Namespace: vmCtx.VM.Namespace, + }, + Data: map[string][]byte{ + "product_id": []byte("foo_product_id"), + }, + }) + }) + + It("returns success", func() { + bsData, err := vsphere.GetVirtualMachineBootstrap(vmCtx, k8sClient) + Expect(err).ToNot(HaveOccurred()) + Expect(bsData.Sysprep.ProductID).To(Equal("foo_product_id")) + }) + + When("key from selector is not present", func() { + BeforeEach(func() { + vmCtx.VM.Spec.Bootstrap.Sysprep.Sysprep.UserData.ProductID.Key = anotherKey + }) + + It("returns an error", func() { + _, err := vsphere.GetVirtualMachineBootstrap(vmCtx, k8sClient) + Expect(err).To(HaveOccurred()) + Expect(conditions.IsFalse(vmCtx.VM, vmopv1.VirtualMachineConditionBootstrapReady)).To(BeTrue()) + }) + }) + }) + + When("secret is not present", func() { + It("returns an error", func() { + _, err := vsphere.GetVirtualMachineBootstrap(vmCtx, k8sClient) + Expect(err).To(HaveOccurred()) + Expect(conditions.IsFalse(vmCtx.VM, vmopv1.VirtualMachineConditionBootstrapReady)).To(BeTrue()) + }) + }) + + When("secret selector is absent", func() { + + BeforeEach(func() { + vmCtx.VM.Spec.Bootstrap.Sysprep.Sysprep.UserData.FullName = "foo" + vmCtx.VM.Spec.Bootstrap.Sysprep.Sysprep.UserData.ProductID = nil + }) + + It("does not return an error", func() { + bsData, err := vsphere.GetVirtualMachineBootstrap(vmCtx, k8sClient) + Expect(err).ToNot(HaveOccurred()) + Expect(bsData.Sysprep.ProductID).To(Equal("")) + }) + }) + }) + + Context("for GUIUnattended", func() { + pwdSecretName := "password_secret" + + BeforeEach(func() { + vmCtx.VM.Spec.Bootstrap.Sysprep.Sysprep = &sysprep.Sysprep{ + GUIUnattended: &sysprep.GUIUnattended{ + AutoLogon: true, + Password: &common.SecretKeySelector{ + Name: pwdSecretName, + Key: "password", + }, + }, + } + }) + + When("secret is present", func() { + BeforeEach(func() { + initObjects = append(initObjects, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: pwdSecretName, + Namespace: vmCtx.VM.Namespace, + }, + Data: map[string][]byte{ + "password": []byte("foo_bar123"), + }, + }) + }) + + It("returns success", func() { + bsData, err := vsphere.GetVirtualMachineBootstrap(vmCtx, k8sClient) + Expect(err).ToNot(HaveOccurred()) + Expect(bsData.Sysprep.Password).To(Equal("foo_bar123")) + }) + + When("key from selector is not present", func() { + BeforeEach(func() { + vmCtx.VM.Spec.Bootstrap.Sysprep.Sysprep.GUIUnattended.Password.Key = anotherKey + }) + + It("returns an error", func() { + bsData, err := vsphere.GetVirtualMachineBootstrap(vmCtx, k8sClient) + Expect(err).To(HaveOccurred()) + Expect(conditions.IsFalse(vmCtx.VM, vmopv1.VirtualMachineConditionBootstrapReady)).To(BeTrue()) + Expect(bsData.Sysprep).To(BeNil()) + }) + }) + }) + + When("secret is not present", func() { + It("returns an error", func() { + bsData, err := vsphere.GetVirtualMachineBootstrap(vmCtx, k8sClient) + Expect(err).To(HaveOccurred()) + Expect(conditions.IsFalse(vmCtx.VM, vmopv1.VirtualMachineConditionBootstrapReady)).To(BeTrue()) + Expect(bsData.Sysprep).To(BeNil()) + }) + }) + }) + + Context("for Identification", func() { + pwdSecretName := "domain_password_secret" + + BeforeEach(func() { + vmCtx.VM.Spec.Bootstrap.Sysprep.Sysprep = &sysprep.Sysprep{ + Identification: &sysprep.Identification{ + JoinDomain: "foo", + DomainAdminPassword: &common.SecretKeySelector{ + Name: pwdSecretName, + Key: "domain_password", + }, + }, + } + }) + + When("secret is present", func() { + BeforeEach(func() { + initObjects = append(initObjects, &corev1.Secret{ + ObjectMeta: metav1.ObjectMeta{ + Name: pwdSecretName, + Namespace: vmCtx.VM.Namespace, + }, + Data: map[string][]byte{ + "domain_password": []byte("foo_bar_fizz123"), + }, + }) + }) + + It("returns success", func() { + bsData, err := vsphere.GetVirtualMachineBootstrap(vmCtx, k8sClient) + Expect(err).ToNot(HaveOccurred()) + Expect(bsData.Sysprep.DomainPassword).To(Equal("foo_bar_fizz123")) + }) + + When("key from selector is not present", func() { + BeforeEach(func() { + vmCtx.VM.Spec.Bootstrap.Sysprep.Sysprep.Identification.DomainAdminPassword.Key = anotherKey + }) + + It("returns an error", func() { + bsData, err := vsphere.GetVirtualMachineBootstrap(vmCtx, k8sClient) + Expect(err).To(HaveOccurred()) + Expect(conditions.IsFalse(vmCtx.VM, vmopv1.VirtualMachineConditionBootstrapReady)).To(BeTrue()) + Expect(bsData.Sysprep).To(BeNil()) + }) + }) + }) + + When("secret is not present", func() { + It("returns an error", func() { + bsData, err := vsphere.GetVirtualMachineBootstrap(vmCtx, k8sClient) + Expect(err).To(HaveOccurred()) + Expect(conditions.IsFalse(vmCtx.VM, vmopv1.VirtualMachineConditionBootstrapReady)).To(BeTrue()) + Expect(bsData.Sysprep).To(BeNil()) + }) + }) + }) + }) + When("Bootstrap with vAppConfig", func() { BeforeEach(func() { diff --git a/webhooks/virtualmachine/v1alpha2/mutation/virtualmachine_mutator.go b/webhooks/virtualmachine/v1alpha2/mutation/virtualmachine_mutator.go index b655523e6..efaab2b66 100644 --- a/webhooks/virtualmachine/v1alpha2/mutation/virtualmachine_mutator.go +++ b/webhooks/virtualmachine/v1alpha2/mutation/virtualmachine_mutator.go @@ -119,6 +119,9 @@ func (m mutator) Mutate(ctx *context.WebhookRequestContext) admission.Response { } else if mutated { wasMutated = true } + if modified.Spec.Bootstrap != nil && SetDefaultSysprepKeys(modified) { + wasMutated = true + } case admissionv1.Update: oldVM, err := m.vmFromUnstructured(ctx.OldObj) if err != nil { @@ -327,3 +330,36 @@ func ResolveImageName( vm.Spec.ImageName = determinedImageName return true, nil } + +func SetDefaultSysprepKeys(vm *vmopv1.VirtualMachine) bool { + if vm.Spec.Bootstrap.Sysprep == nil || vm.Spec.Bootstrap.Sysprep.Sysprep == nil { + return false + } + + wasMutated := false + inlineSysprep := vm.Spec.Bootstrap.Sysprep.Sysprep + if inlineSysprep.GUIUnattended != nil { + if inlineSysprep.GUIUnattended.Password != nil && inlineSysprep.GUIUnattended.Password.Key == "" { + inlineSysprep.GUIUnattended.Password.Key = "password" + wasMutated = true + } + } + + if inlineSysprep.Identification != nil { + if inlineSysprep.Identification.JoinDomain != "" && + inlineSysprep.Identification.DomainAdminPassword != nil && + inlineSysprep.Identification.DomainAdminPassword.Key == "" { + inlineSysprep.Identification.DomainAdminPassword.Key = "domain_admin_password" + wasMutated = true + } + } + + if inlineSysprep.UserData != nil && inlineSysprep.UserData.ProductID != nil { + if inlineSysprep.UserData.ProductID.Key == "" { + inlineSysprep.UserData.ProductID.Key = "product_id" + wasMutated = true + } + } + + return wasMutated +} diff --git a/webhooks/virtualmachine/v1alpha2/mutation/virtualmachine_mutator_unit_test.go b/webhooks/virtualmachine/v1alpha2/mutation/virtualmachine_mutator_unit_test.go index ed3fc6820..42b5e06e5 100644 --- a/webhooks/virtualmachine/v1alpha2/mutation/virtualmachine_mutator_unit_test.go +++ b/webhooks/virtualmachine/v1alpha2/mutation/virtualmachine_mutator_unit_test.go @@ -19,6 +19,8 @@ import ( "github.com/vmware-tanzu/vm-operator/api/v1alpha1" vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha2" + "github.com/vmware-tanzu/vm-operator/api/v1alpha2/common" + "github.com/vmware-tanzu/vm-operator/api/v1alpha2/sysprep" "github.com/vmware-tanzu/vm-operator/pkg/lib" "github.com/vmware-tanzu/vm-operator/pkg/vmprovider/providers/vsphere2/config" "github.com/vmware-tanzu/vm-operator/test/builder" @@ -498,4 +500,76 @@ func unitTestsMutating() { ) }) }) + + Describe("Sysprep", func() { + Context("When GUIUnattended is set", func() { + BeforeEach(func() { + ctx.vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ + Sysprep: &vmopv1.VirtualMachineBootstrapSysprepSpec{ + Sysprep: &sysprep.Sysprep{ + GUIUnattended: &sysprep.GUIUnattended{ + AutoLogon: true, + Password: &common.SecretKeySelector{Name: "pwd_secret"}, + }, + }, + }, + } + }) + + It("should mutate Password selector key", func() { + Expect(mutation.SetDefaultSysprepKeys(ctx.vm)).To(BeTrue()) + Expect(ctx.vm.Spec.Bootstrap.Sysprep.Sysprep.GUIUnattended.Password.Key).To(Equal("password")) + }) + }) + + Context("When Identification is set", func() { + BeforeEach(func() { + ctx.vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ + Sysprep: &vmopv1.VirtualMachineBootstrapSysprepSpec{ + Sysprep: &sysprep.Sysprep{ + Identification: &sysprep.Identification{ + JoinDomain: "foo-domain", + DomainAdminPassword: &common.SecretKeySelector{Name: "pwd_secret"}, + }, + }, + }, + } + }) + + It("should mutate DomainAdminPassword selector key", func() { + Expect(mutation.SetDefaultSysprepKeys(ctx.vm)).To(BeTrue()) + Expect(ctx.vm.Spec.Bootstrap.Sysprep.Sysprep.Identification.DomainAdminPassword.Key).To(Equal("domain_admin_password")) + }) + }) + + Context("When UserData is set", func() { + BeforeEach(func() { + ctx.vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ + Sysprep: &vmopv1.VirtualMachineBootstrapSysprepSpec{ + Sysprep: &sysprep.Sysprep{ + UserData: &sysprep.UserData{ + OrgName: "foo-org", + ProductID: &common.SecretKeySelector{Name: "product_id_secret"}, + }, + }, + }, + } + }) + + It("should mutate ProductID selector key", func() { + Expect(mutation.SetDefaultSysprepKeys(ctx.vm)).To(BeTrue()) + Expect(ctx.vm.Spec.Bootstrap.Sysprep.Sysprep.UserData.ProductID.Key).To(Equal("product_id")) + }) + + Context("When ProductID selector is not set", func() { + BeforeEach(func() { + ctx.vm.Spec.Bootstrap.Sysprep.Sysprep.UserData.ProductID = nil + }) + + It("should not mutate ProductID selector key", func() { + Expect(mutation.SetDefaultSysprepKeys(ctx.vm)).To(BeFalse()) + }) + }) + }) + }) } diff --git a/webhooks/virtualmachine/v1alpha2/validation/virtualmachine_validator.go b/webhooks/virtualmachine/v1alpha2/validation/virtualmachine_validator.go index 59abb9ddc..a3d9d98f7 100644 --- a/webhooks/virtualmachine/v1alpha2/validation/virtualmachine_validator.go +++ b/webhooks/virtualmachine/v1alpha2/validation/virtualmachine_validator.go @@ -29,6 +29,7 @@ import ( "github.com/pkg/errors" vmopv1 "github.com/vmware-tanzu/vm-operator/api/v1alpha2" + "github.com/vmware-tanzu/vm-operator/api/v1alpha2/sysprep" volume "github.com/vmware-tanzu/vm-operator/controllers/volume/v1alpha2" "github.com/vmware-tanzu/vm-operator/pkg/builder" "github.com/vmware-tanzu/vm-operator/pkg/context" @@ -242,6 +243,10 @@ func (v validator) validateBootstrap( allErrs = append(allErrs, field.Invalid(p, "sysPrep", "sysprep and rawSysprep are mutually exclusive")) } + + if sysPrep.Sysprep != nil { + allErrs = append(allErrs, v.validateInlineSysprep(p, sysPrep.Sysprep)...) + } } else { allErrs = append(allErrs, field.Invalid(p, "Sysprep", fmt.Sprintf(featureNotEnabled, "Sysprep"))) } @@ -276,6 +281,43 @@ func (v validator) validateBootstrap( return allErrs } +func (v validator) validateInlineSysprep(p *field.Path, sysprep *sysprep.Sysprep) field.ErrorList { + var allErrs field.ErrorList + + s := p.Child("sysprep") + if guiUnattended := sysprep.GUIUnattended; guiUnattended != nil { + if guiUnattended.AutoLogon && guiUnattended.Password.Name == "" { + allErrs = append(allErrs, field.Invalid(s, "guiUnattended", + "autoLogon requires password selector to be set")) + } + } + + if identification := sysprep.Identification; identification != nil { + if identification.JoinDomain != "" && identification.JoinWorkgroup != "" { + allErrs = append(allErrs, field.Invalid(s, "identification", + "joinDomain and joinWorkgroup are mutually exclusive")) + } + + if identification.JoinDomain != "" { + if identification.DomainAdmin == "" || + identification.DomainAdminPassword == nil || + identification.DomainAdminPassword.Name == "" { + allErrs = append(allErrs, field.Invalid(s, "identification", + "joinDomain requires domainAdmin and domainAdminPassword selector to be set")) + } + } + + if identification.JoinWorkgroup != "" { + if identification.DomainAdmin != "" || identification.DomainAdminPassword != nil { + allErrs = append(allErrs, field.Invalid(s, "identification", + "joinWorkgroup and domainAdmin/domainAdminPassword are mutually exclusive")) + } + } + } + + return allErrs +} + func (v validator) validateImage(ctx *context.WebhookRequestContext, vm *vmopv1.VirtualMachine) field.ErrorList { var allErrs field.ErrorList diff --git a/webhooks/virtualmachine/v1alpha2/validation/virtualmachine_validator_unit_test.go b/webhooks/virtualmachine/v1alpha2/validation/virtualmachine_validator_unit_test.go index ee56cb227..37fe6adc4 100644 --- a/webhooks/virtualmachine/v1alpha2/validation/virtualmachine_validator_unit_test.go +++ b/webhooks/virtualmachine/v1alpha2/validation/virtualmachine_validator_unit_test.go @@ -538,6 +538,47 @@ func unitTestsValidateCreate() { ), }, ), + Entry("disallow Sysprep mixing inline Sysprep identification when FSS is enabled", + testParams{ + setup: func(ctx *unitValidatingWebhookContext) { + Expect(os.Setenv(lib.WindowsSysprepFSS, "true")).To(Succeed()) + ctx.vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ + Sysprep: &vmopv1.VirtualMachineBootstrapSysprepSpec{ + Sysprep: &sysprep.Sysprep{ + Identification: &sysprep.Identification{ + JoinDomain: "foo-domain", + JoinWorkgroup: "foo-wg", + }, + }, + }, + } + }, + validate: doValidateWithMsg( + `spec.bootstrap.sysprep.sysprep: Invalid value: "identification": joinDomain and joinWorkgroup are mutually exclusive`, + `spec.bootstrap.sysprep.sysprep: Invalid value: "identification": joinDomain requires domainAdmin and domainAdminPassword selector to be set`, + ), + }, + ), + Entry("disallow Sysprep mixing inline Sysprep identification when FSS is enabled", + testParams{ + setup: func(ctx *unitValidatingWebhookContext) { + Expect(os.Setenv(lib.WindowsSysprepFSS, "true")).To(Succeed()) + ctx.vm.Spec.Bootstrap = &vmopv1.VirtualMachineBootstrapSpec{ + Sysprep: &vmopv1.VirtualMachineBootstrapSysprepSpec{ + Sysprep: &sysprep.Sysprep{ + Identification: &sysprep.Identification{ + JoinWorkgroup: "foo-wg", + DomainAdmin: "admin@os.local", + }, + }, + }, + } + }, + validate: doValidateWithMsg( + `spec.bootstrap.sysprep.sysprep: Invalid value: "identification": joinWorkgroup and domainAdmin/domainAdminPassword are mutually exclusive`, + ), + }, + ), Entry("disallow vAppConfig mixing inline Properties and RawProperties", testParams{ setup: func(ctx *unitValidatingWebhookContext) {