diff --git a/api/v1beta1/zz_generated.conversion.go b/api/v1beta1/zz_generated.conversion.go index c3ec54ba..5bd48651 100644 --- a/api/v1beta1/zz_generated.conversion.go +++ b/api/v1beta1/zz_generated.conversion.go @@ -944,8 +944,8 @@ func Convert_v1beta1_CloudStackZoneSpec_To_v1beta3_CloudStackZoneSpec(in *CloudS } func autoConvert_v1beta3_CloudStackZoneSpec_To_v1beta1_CloudStackZoneSpec(in *v1beta3.CloudStackZoneSpec, out *CloudStackZoneSpec, s conversion.Scope) error { - out.Name = in.Name out.ID = in.ID + out.Name = in.Name if err := Convert_v1beta3_Network_To_v1beta1_Network(&in.Network, &out.Network, s); err != nil { return err } @@ -971,8 +971,8 @@ func Convert_v1beta1_Network_To_v1beta3_Network(in *Network, out *v1beta3.Networ func autoConvert_v1beta3_Network_To_v1beta1_Network(in *v1beta3.Network, out *Network, s conversion.Scope) error { out.ID = in.ID - out.Type = in.Type out.Name = in.Name + out.Type = in.Type // WARNING: in.CIDR requires manual conversion: does not exist in peer-type // WARNING: in.Domain requires manual conversion: does not exist in peer-type return nil diff --git a/api/v1beta2/cloudstackfailuredomain_types.go b/api/v1beta2/cloudstackfailuredomain_types.go index 5af6fa88..4b5ce7d7 100644 --- a/api/v1beta2/cloudstackfailuredomain_types.go +++ b/api/v1beta2/cloudstackfailuredomain_types.go @@ -47,23 +47,23 @@ type Network struct { // +optional ID string `json:"id,omitempty"` + // Cloudstack Network Name the cluster is built in. + Name string `json:"name"` + // Cloudstack Network Type the cluster is built in. // +optional Type string `json:"type,omitempty"` - - // Cloudstack Network Name the cluster is built in. - Name string `json:"name"` } // CloudStackZoneSpec specifies a Zone's details. type CloudStackZoneSpec struct { - // Name. + // Zone ID. //+optional - Name string `json:"name,omitempty"` + ID string `json:"id,omitempty"` - // ID. + // Zone Name. //+optional - ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` // The network within the Zone to use. Network Network `json:"network"` @@ -85,6 +85,10 @@ type CloudStackFailureDomainSpec struct { // +optional Domain string `json:"domain,omitempty"` + // CloudStack project. + // +optional + Project string `json:"project,omitempty"` + // Apache CloudStack Endpoint secret reference. ACSEndpoint corev1.SecretReference `json:"acsEndpoint"` } diff --git a/api/v1beta2/zz_generated.conversion.go b/api/v1beta2/zz_generated.conversion.go index ab439e1b..9382799d 100644 --- a/api/v1beta2/zz_generated.conversion.go +++ b/api/v1beta2/zz_generated.conversion.go @@ -681,6 +681,7 @@ func autoConvert_v1beta2_CloudStackFailureDomainSpec_To_v1beta3_CloudStackFailur } out.Account = in.Account out.Domain = in.Domain + out.Project = in.Project out.ACSEndpoint = in.ACSEndpoint return nil } @@ -697,6 +698,7 @@ func autoConvert_v1beta3_CloudStackFailureDomainSpec_To_v1beta2_CloudStackFailur } out.Account = in.Account out.Domain = in.Domain + out.Project = in.Project out.ACSEndpoint = in.ACSEndpoint return nil } @@ -1247,8 +1249,8 @@ func Convert_v1beta3_CloudStackResourceIdentifier_To_v1beta2_CloudStackResourceI } func autoConvert_v1beta2_CloudStackZoneSpec_To_v1beta3_CloudStackZoneSpec(in *CloudStackZoneSpec, out *v1beta3.CloudStackZoneSpec, s conversion.Scope) error { - out.Name = in.Name out.ID = in.ID + out.Name = in.Name if err := Convert_v1beta2_Network_To_v1beta3_Network(&in.Network, &out.Network, s); err != nil { return err } @@ -1261,8 +1263,8 @@ func Convert_v1beta2_CloudStackZoneSpec_To_v1beta3_CloudStackZoneSpec(in *CloudS } func autoConvert_v1beta3_CloudStackZoneSpec_To_v1beta2_CloudStackZoneSpec(in *v1beta3.CloudStackZoneSpec, out *CloudStackZoneSpec, s conversion.Scope) error { - out.Name = in.Name out.ID = in.ID + out.Name = in.Name if err := Convert_v1beta3_Network_To_v1beta2_Network(&in.Network, &out.Network, s); err != nil { return err } @@ -1276,8 +1278,8 @@ func Convert_v1beta3_CloudStackZoneSpec_To_v1beta2_CloudStackZoneSpec(in *v1beta func autoConvert_v1beta2_Network_To_v1beta3_Network(in *Network, out *v1beta3.Network, s conversion.Scope) error { out.ID = in.ID - out.Type = in.Type out.Name = in.Name + out.Type = in.Type return nil } @@ -1288,8 +1290,8 @@ func Convert_v1beta2_Network_To_v1beta3_Network(in *Network, out *v1beta3.Networ func autoConvert_v1beta3_Network_To_v1beta2_Network(in *v1beta3.Network, out *Network, s conversion.Scope) error { out.ID = in.ID - out.Type = in.Type out.Name = in.Name + out.Type = in.Type // WARNING: in.CIDR requires manual conversion: does not exist in peer-type // WARNING: in.Domain requires manual conversion: does not exist in peer-type return nil diff --git a/api/v1beta3/cloudstackfailuredomain_types.go b/api/v1beta3/cloudstackfailuredomain_types.go index a757dd4f..6dd034be 100644 --- a/api/v1beta3/cloudstackfailuredomain_types.go +++ b/api/v1beta3/cloudstackfailuredomain_types.go @@ -47,13 +47,13 @@ type Network struct { //+optional ID string `json:"id,omitempty"` + // Cloudstack Network Name the cluster is built in. + Name string `json:"name"` + // Cloudstack Network Type the cluster is built in. //+optional Type string `json:"type,omitempty"` - // Cloudstack Network Name the cluster is built in. - Name string `json:"name"` - // CIDR is the IP address range of the network. //+optional CIDR string `json:"cidr,omitempty"` @@ -65,13 +65,13 @@ type Network struct { // CloudStackZoneSpec specifies a Zone's details. type CloudStackZoneSpec struct { - // Name. + // Zone ID. //+optional - Name string `json:"name,omitempty"` + ID string `json:"id,omitempty"` - // ID. + // Zone Name. //+optional - ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` // The network within the Zone to use. Network Network `json:"network"` @@ -93,6 +93,10 @@ type CloudStackFailureDomainSpec struct { //+optional Domain string `json:"domain,omitempty"` + // CloudStack project. + //+optional + Project string `json:"project,omitempty"` + // Apache CloudStack Endpoint secret reference. ACSEndpoint corev1.SecretReference `json:"acsEndpoint"` } diff --git a/api/v1beta3/cloudstackmachine_types.go b/api/v1beta3/cloudstackmachine_types.go index 86b6901d..4c9c1cec 100644 --- a/api/v1beta3/cloudstackmachine_types.go +++ b/api/v1beta3/cloudstackmachine_types.go @@ -97,16 +97,6 @@ func (r *CloudStackMachine) CompressUserdata() bool { return r.Spec.UncompressedUserData == nil || !*r.Spec.UncompressedUserData } -type CloudStackResourceIdentifier struct { - // Cloudstack resource ID. - //+optional - ID string `json:"id,omitempty"` - - // Cloudstack resource Name. - //+optional - Name string `json:"name,omitempty"` -} - type CloudStackResourceDiskOffering struct { CloudStackResourceIdentifier `json:",inline"` // Desired disk size. Used if disk offering is customizable as indicated by the ACS field 'Custom Disk Size'. diff --git a/api/v1beta3/types.go b/api/v1beta3/types.go index d2f73a35..33527b44 100644 --- a/api/v1beta3/types.go +++ b/api/v1beta3/types.go @@ -16,6 +16,17 @@ limitations under the License. package v1beta3 +// CloudStackResourceIdentifier is used to identify any CloudStack resource that can be referenced by either ID or Name. +type CloudStackResourceIdentifier struct { + // Cloudstack resource ID. + //+optional + ID string `json:"id,omitempty"` + + // Cloudstack resource Name. + //+optional + Name string `json:"name,omitempty"` +} + // LoadBalancer represents basic information about the associated OpenStack LoadBalancer. type LoadBalancer struct { IPAddress string `json:"ipAddress"` diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackclusters.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackclusters.yaml index 68605579..fbd6cf9e 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackclusters.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackclusters.yaml @@ -253,14 +253,17 @@ spec: name: description: The failure domain unique name. type: string + project: + description: CloudStack project. + type: string zone: description: The ACS Zone for this failure domain. properties: id: - description: ID. + description: Zone ID. type: string name: - description: Name. + description: Zone Name. type: string network: description: The network within the Zone to use. @@ -427,14 +430,17 @@ spec: name: description: The failure domain unique name. type: string + project: + description: CloudStack project. + type: string zone: description: The ACS Zone for this failure domain. properties: id: - description: ID. + description: Zone ID. type: string name: - description: Name. + description: Zone Name. type: string network: description: The network within the Zone to use. diff --git a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackfailuredomains.yaml b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackfailuredomains.yaml index 5c2ddde3..f26bba9a 100644 --- a/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackfailuredomains.yaml +++ b/config/crd/bases/infrastructure.cluster.x-k8s.io_cloudstackfailuredomains.yaml @@ -63,14 +63,17 @@ spec: name: description: The failure domain unique name. type: string + project: + description: CloudStack project. + type: string zone: description: The ACS Zone for this failure domain. properties: id: - description: ID. + description: Zone ID. type: string name: - description: Name. + description: Zone Name. type: string network: description: The network within the Zone to use. @@ -163,14 +166,17 @@ spec: name: description: The failure domain unique name. type: string + project: + description: CloudStack project. + type: string zone: description: The ACS Zone for this failure domain. properties: id: - description: ID. + description: Zone ID. type: string name: - description: Name. + description: Zone Name. type: string network: description: The network within the Zone to use. diff --git a/controllers/cloudstackaffinitygroup_controller_test.go b/controllers/cloudstackaffinitygroup_controller_test.go index ebeecdfc..39dca7a0 100644 --- a/controllers/cloudstackaffinitygroup_controller_test.go +++ b/controllers/cloudstackaffinitygroup_controller_test.go @@ -20,10 +20,12 @@ import ( "github.com/golang/mock/gomock" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" + "k8s.io/apimachinery/pkg/api/errors" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/controller" infrav1 "sigs.k8s.io/cluster-api-provider-cloudstack/api/v1beta3" + "sigs.k8s.io/cluster-api-provider-cloudstack/pkg/cloud" dummies "sigs.k8s.io/cluster-api-provider-cloudstack/test/dummies/v1beta3" ) @@ -38,11 +40,34 @@ var _ = Describe("CloudStackAffinityGroupReconciler", func() { // Modify failure domain name the same way the cluster controller would. dummies.CSAffinityGroup.Spec.FailureDomainName = dummies.CSFailureDomain1.Spec.Name + mockCloudClient.EXPECT().GetOrCreateAffinityGroup(gomock.Any()).AnyTimes() + Ω(k8sClient.Create(ctx, dummies.CSFailureDomain1)).Should(Succeed()) Ω(k8sClient.Create(ctx, dummies.CSAffinityGroup)).Should(Succeed()) + // Test that the AffinityGroup controller sets Status.Ready to true. + Eventually(func() bool { + nameSpaceFilter := &client.ListOptions{Namespace: dummies.ClusterNameSpace} + affinityGroups := &infrav1.CloudStackAffinityGroupList{} + if err := k8sClient.List(ctx, affinityGroups, nameSpaceFilter); err == nil { + if len(affinityGroups.Items) == 1 { + return affinityGroups.Items[0].Status.Ready + } + } + + return false + }, timeout).WithPolling(pollInterval).Should(BeTrue()) + }) + + It("Should remove affinity group finalizer if corresponding affinity group is not present on Cloudstack.", func() { + // Modify failure domain name the same way the cluster controller would. + dummies.CSAffinityGroup.Spec.FailureDomainName = dummies.CSFailureDomain1.Spec.Name + mockCloudClient.EXPECT().GetOrCreateAffinityGroup(gomock.Any()).AnyTimes() + Ω(k8sClient.Create(ctx, dummies.CSFailureDomain1)).Should(Succeed()) + Ω(k8sClient.Create(ctx, dummies.CSAffinityGroup)).Should(Succeed()) + // Test that the AffinityGroup controller sets Status.Ready to true. Eventually(func() bool { nameSpaceFilter := &client.ListOptions{Namespace: dummies.ClusterNameSpace} @@ -52,7 +77,23 @@ var _ = Describe("CloudStackAffinityGroupReconciler", func() { return affinityGroups.Items[0].Status.Ready } } + return false + }, timeout).WithPolling(pollInterval).Should(BeTrue()) + + mockCloudClient.EXPECT().FetchAffinityGroup(gomock.Any()).Do(func(arg1 interface{}) { + arg1.(*cloud.AffinityGroup).ID = "" + }).AnyTimes().Return(nil) + Ω(k8sClient.Delete(ctx, dummies.CSAffinityGroup)).Should(Succeed()) + Ω(k8sClient.Delete(ctx, dummies.CSAffinityGroup)).Should(Succeed()) + + // Once the affinity group id was set to "" the controller should remove the finalizer and unblock deleting affinity group resource + Eventually(func() bool { + retrievedAffinityGroup := &infrav1.CloudStackAffinityGroup{} + affinityGroupKey := client.ObjectKey{Namespace: dummies.ClusterNameSpace, Name: dummies.AffinityGroup.Name} + if err := k8sClient.Get(ctx, affinityGroupKey, retrievedAffinityGroup); err != nil { + return errors.IsNotFound(err) + } return false }, timeout).WithPolling(pollInterval).Should(BeTrue()) }) diff --git a/controllers/utils/failuredomains.go b/controllers/utils/failuredomains.go index 22d535f7..7245d1e8 100644 --- a/controllers/utils/failuredomains.go +++ b/controllers/utils/failuredomains.go @@ -154,12 +154,12 @@ func (c *CloudClientImplementation) AsFailureDomainUser(fdSpec *infrav1.CloudSta _ = c.K8sClient.Get(c.RequestCtx, key, clientConfig) var err error - if c.CSClient, err = cloud.NewClientFromK8sSecret(endpointCredentials, clientConfig); err != nil { + if c.CSClient, err = cloud.NewClientFromK8sSecret(endpointCredentials, clientConfig, cloud.WithProject(fdSpec.Project)); err != nil { return ctrl.Result{}, errors.Wrapf(err, "parsing ACSEndpoint secret with ref: %v", fdSpec.ACSEndpoint) } if fdSpec.Account != "" { // Set r.CSUser CloudStack Client per Account and Domain. - client, err := c.CSClient.NewClientInDomainAndAccount(fdSpec.Domain, fdSpec.Account) + client, err := c.CSClient.NewClientInDomainAndAccount(fdSpec.Domain, fdSpec.Account, cloud.WithProject(fdSpec.Project)) if err != nil { return ctrl.Result{}, err } diff --git a/pkg/cloud/affinity_groups.go b/pkg/cloud/affinity_groups.go index 60281f3c..105a7f9c 100644 --- a/pkg/cloud/affinity_groups.go +++ b/pkg/cloud/affinity_groups.go @@ -17,6 +17,7 @@ limitations under the License. package cloud import ( + "github.com/apache/cloudstack-go/v2/cloudstack" "github.com/pkg/errors" infrav1 "sigs.k8s.io/cluster-api-provider-cloudstack/api/v1beta3" @@ -45,7 +46,7 @@ type AffinityGroupIface interface { func (c *client) FetchAffinityGroup(group *AffinityGroup) error { if group.ID != "" { - affinityGroup, count, err := c.cs.AffinityGroup.GetAffinityGroupByID(group.ID) + affinityGroup, count, err := c.cs.AffinityGroup.GetAffinityGroupByID(group.ID, cloudstack.WithProject(c.user.Project.ID)) if err != nil { // handle via multierr c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) @@ -61,7 +62,7 @@ func (c *client) FetchAffinityGroup(group *AffinityGroup) error { return nil } if group.Name != "" { - affinityGroup, count, err := c.cs.AffinityGroup.GetAffinityGroupByName(group.Name) + affinityGroup, count, err := c.cs.AffinityGroup.GetAffinityGroupByName(group.Name, cloudstack.WithProject(c.user.Project.ID)) if err != nil { // handle via multierr c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) @@ -84,6 +85,7 @@ func (c *client) GetOrCreateAffinityGroup(group *AffinityGroup) error { if err := c.FetchAffinityGroup(group); err != nil { // Group not found? p := c.cs.AffinityGroup.NewCreateAffinityGroupParams(group.Name, group.Type) p.SetName(group.Name) + setIfNotEmpty(c.user.Project.ID, p.SetProjectid) resp, err := c.cs.AffinityGroup.CreateAffinityGroup(p) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) @@ -100,6 +102,7 @@ func (c *client) DeleteAffinityGroup(group *AffinityGroup) error { p := c.cs.AffinityGroup.NewDeleteAffinityGroupParams() setIfNotEmpty(group.ID, p.SetId) setIfNotEmpty(group.Name, p.SetName) + setIfNotEmpty(c.user.Project.ID, p.SetProjectid) _, err := c.cs.AffinityGroup.DeleteAffinityGroup(p) c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) @@ -110,7 +113,7 @@ type affinityGroups []AffinityGroup func (c *client) getCurrentAffinityGroups(csMachine *infrav1.CloudStackMachine) (affinityGroups, error) { // Start by fetching VM details which includes an array of currently associated affinity groups. - virtM, count, err := c.cs.VirtualMachine.GetVirtualMachineByID(*csMachine.Spec.InstanceID) + virtM, count, err := c.cs.VirtualMachine.GetVirtualMachineByID(*csMachine.Spec.InstanceID, cloudstack.WithProject(c.user.Project.ID)) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) diff --git a/pkg/cloud/affinity_groups_test.go b/pkg/cloud/affinity_groups_test.go index 3310f601..706b57f6 100644 --- a/pkg/cloud/affinity_groups_test.go +++ b/pkg/cloud/affinity_groups_test.go @@ -59,13 +59,13 @@ var _ = Describe("AffinityGroup Unit Tests", func() { Context("Fetch or Create Affinity group", func() { It("fetches an affinity group by Name", func() { dummies.AffinityGroup.ID = "" // Force name fetching. - ags.EXPECT().GetAffinityGroupByName(dummies.AffinityGroup.Name).Return(&cloudstack.AffinityGroup{}, 1, nil) + ags.EXPECT().GetAffinityGroupByName(dummies.AffinityGroup.Name, gomock.Any()).Return(&cloudstack.AffinityGroup{}, 1, nil) Ω(client.GetOrCreateAffinityGroup(dummies.AffinityGroup)).Should(Succeed()) }) It("fetches an affinity group by ID", func() { - ags.EXPECT().GetAffinityGroupByID(dummies.AffinityGroup.ID).Return(&cloudstack.AffinityGroup{}, 1, nil) + ags.EXPECT().GetAffinityGroupByID(dummies.AffinityGroup.ID, gomock.Any()).Return(&cloudstack.AffinityGroup{}, 1, nil) Ω(client.GetOrCreateAffinityGroup(dummies.AffinityGroup)).Should(Succeed()) }) @@ -73,7 +73,7 @@ var _ = Describe("AffinityGroup Unit Tests", func() { It("creates an affinity group", func() { // dummies.SetDummyDomainAndAccount() // dummies.SetDummyDomainID() - ags.EXPECT().GetAffinityGroupByID(dummies.AffinityGroup.ID).Return(nil, -1, fakeError) + ags.EXPECT().GetAffinityGroupByID(dummies.AffinityGroup.ID, gomock.Any()).Return(nil, -1, fakeError) ags.EXPECT().NewCreateAffinityGroupParams(dummies.AffinityGroup.Name, dummies.AffinityGroup.Type). Return(&cloudstack.CreateAffinityGroupParams{}) ags.EXPECT().CreateAffinityGroup(ParamMatch(And(NameEquals(dummies.AffinityGroup.Name)))). @@ -85,7 +85,7 @@ var _ = Describe("AffinityGroup Unit Tests", func() { It("creates an affinity group if Name provided returns more than one affinity group", func() { dummies.AffinityGroup.ID = "" // Force name fetching. agp := &cloudstack.CreateAffinityGroupParams{} - ags.EXPECT().GetAffinityGroupByName(dummies.AffinityGroup.Name).Return(&cloudstack.AffinityGroup{}, 2, nil) + ags.EXPECT().GetAffinityGroupByName(dummies.AffinityGroup.Name, gomock.Any()).Return(&cloudstack.AffinityGroup{}, 2, nil) ags.EXPECT().NewCreateAffinityGroupParams(gomock.Any(), gomock.Any()).Return(agp) ags.EXPECT().CreateAffinityGroup(agp).Return(&cloudstack.CreateAffinityGroupResponse{}, nil) @@ -95,7 +95,7 @@ var _ = Describe("AffinityGroup Unit Tests", func() { It("creates an affinity group if getting affinity group by name fails", func() { dummies.AffinityGroup.ID = "" // Force name fetching. agp := &cloudstack.CreateAffinityGroupParams{} - ags.EXPECT().GetAffinityGroupByName(dummies.AffinityGroup.Name).Return(nil, -1, fakeError) + ags.EXPECT().GetAffinityGroupByName(dummies.AffinityGroup.Name, gomock.Any()).Return(nil, -1, fakeError) ags.EXPECT().NewCreateAffinityGroupParams(gomock.Any(), gomock.Any()).Return(agp) ags.EXPECT().CreateAffinityGroup(agp).Return(&cloudstack.CreateAffinityGroupResponse{}, nil) @@ -104,7 +104,7 @@ var _ = Describe("AffinityGroup Unit Tests", func() { It("creates an affinity group if ID provided returns more than one affinity group", func() { agp := &cloudstack.CreateAffinityGroupParams{} - ags.EXPECT().GetAffinityGroupByID(dummies.AffinityGroup.ID).Return(&cloudstack.AffinityGroup{}, 2, nil) + ags.EXPECT().GetAffinityGroupByID(dummies.AffinityGroup.ID, gomock.Any()).Return(&cloudstack.AffinityGroup{}, 2, nil) ags.EXPECT().NewCreateAffinityGroupParams(gomock.Any(), gomock.Any()).Return(agp) ags.EXPECT().CreateAffinityGroup(agp).Return(&cloudstack.CreateAffinityGroupResponse{}, nil) @@ -113,7 +113,7 @@ var _ = Describe("AffinityGroup Unit Tests", func() { It("creates an affinity group if getting affinity group by ID fails", func() { agp := &cloudstack.CreateAffinityGroupParams{} - ags.EXPECT().GetAffinityGroupByID(dummies.AffinityGroup.ID).Return(nil, -1, fakeError) + ags.EXPECT().GetAffinityGroupByID(dummies.AffinityGroup.ID, gomock.Any()).Return(nil, -1, fakeError) ags.EXPECT().NewCreateAffinityGroupParams(gomock.Any(), gomock.Any()).Return(agp) ags.EXPECT().CreateAffinityGroup(agp).Return(&cloudstack.CreateAffinityGroupResponse{}, nil) @@ -164,7 +164,7 @@ var _ = Describe("AffinityGroup Unit Tests", func() { It("Associate affinity group", func() { uagp := &cloudstack.UpdateVMAffinityGroupParams{} vmp := &cloudstack.StartVirtualMachineParams{} - vms.EXPECT().GetVirtualMachineByID(*dummies.CSMachine1.Spec.InstanceID).Return(&cloudstack.VirtualMachine{}, 1, nil) + vms.EXPECT().GetVirtualMachineByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()).Return(&cloudstack.VirtualMachine{}, 1, nil) ags.EXPECT().NewUpdateVMAffinityGroupParams(*dummies.CSMachine1.Spec.InstanceID).Return(uagp) vms.EXPECT().NewStopVirtualMachineParams(*dummies.CSMachine1.Spec.InstanceID).Return(&cloudstack.StopVirtualMachineParams{}) vms.EXPECT().StopVirtualMachine(&cloudstack.StopVirtualMachineParams{}).Return(&cloudstack.StopVirtualMachineResponse{State: "Stopping"}, nil) @@ -177,7 +177,7 @@ var _ = Describe("AffinityGroup Unit Tests", func() { It("Disassociate affinity group", func() { uagp := &cloudstack.UpdateVMAffinityGroupParams{} vmp := &cloudstack.StartVirtualMachineParams{} - vms.EXPECT().GetVirtualMachineByID(*dummies.CSMachine1.Spec.InstanceID).Return(&cloudstack.VirtualMachine{}, 1, nil) + vms.EXPECT().GetVirtualMachineByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()).Return(&cloudstack.VirtualMachine{}, 1, nil) ags.EXPECT().NewUpdateVMAffinityGroupParams(*dummies.CSMachine1.Spec.InstanceID).Return(uagp) vms.EXPECT().NewStopVirtualMachineParams(*dummies.CSMachine1.Spec.InstanceID).Return(&cloudstack.StopVirtualMachineParams{}) vms.EXPECT().StopVirtualMachine(&cloudstack.StopVirtualMachineParams{}).Return(&cloudstack.StopVirtualMachineResponse{State: "Stopping"}, nil) diff --git a/pkg/cloud/client.go b/pkg/cloud/client.go index d57c1134..debfa80c 100644 --- a/pkg/cloud/client.go +++ b/pkg/cloud/client.go @@ -43,15 +43,16 @@ type Client interface { ZoneIFace IsoNetworkIface UserCredIFace - NewClientInDomainAndAccount(domain string, account string) (Client, error) + NewClientInDomainAndAccount(domain string, account string, options ...ClientOption) (Client, error) } -// cloud-config ini structure. +// Config is the cloud-config ini structure. type Config struct { APIUrl string `yaml:"api-url"` APIKey string `yaml:"api-key"` SecretKey string `yaml:"secret-key"` VerifySSL string `yaml:"verify-ssl"` + ProjectID string `yaml:"project-id"` } type client struct { @@ -108,7 +109,7 @@ func UnmarshalAllSecretConfigs(in []byte, out *[]SecretConfig) error { } // NewClientFromK8sSecret returns a client from a k8s secret. -func NewClientFromK8sSecret(endpointSecret *corev1.Secret, clientConfig *corev1.ConfigMap) (Client, error) { +func NewClientFromK8sSecret(endpointSecret *corev1.Secret, clientConfig *corev1.ConfigMap, options ...ClientOption) (Client, error) { endpointSecretStrings := map[string]string{} for k, v := range endpointSecret.Data { endpointSecretStrings[k] = string(v) @@ -118,11 +119,11 @@ func NewClientFromK8sSecret(endpointSecret *corev1.Secret, clientConfig *corev1. return nil, err } - return NewClientFromBytesConfig(bytes, clientConfig) + return NewClientFromBytesConfig(bytes, clientConfig, options...) } // NewClientFromBytesConfig returns a client from a bytes array that unmarshals to a yaml config. -func NewClientFromBytesConfig(conf []byte, clientConfig *corev1.ConfigMap) (Client, error) { +func NewClientFromBytesConfig(conf []byte, clientConfig *corev1.ConfigMap, options ...ClientOption) (Client, error) { r := bytes.NewReader(conf) dec := yaml.NewDecoder(r) var config Config @@ -130,11 +131,11 @@ func NewClientFromBytesConfig(conf []byte, clientConfig *corev1.ConfigMap) (Clie return nil, err } - return NewClientFromConf(config, clientConfig) + return NewClientFromConf(config, clientConfig, options...) } // NewClientFromYamlPath returns a client from a yaml config at path. -func NewClientFromYamlPath(confPath string, secretName string) (Client, error) { +func NewClientFromYamlPath(confPath string, secretName string, options ...ClientOption) (Client, error) { content, err := os.ReadFile(confPath) if err != nil { return nil, err @@ -155,11 +156,11 @@ func NewClientFromYamlPath(confPath string, secretName string) (Client, error) { return nil, errors.Errorf("config with secret name %s not found", secretName) } - return NewClientFromConf(conf, nil) + return NewClientFromConf(conf, nil, options...) } // NewClientFromConf creates a new Cloud Client form a map of strings to strings. -func NewClientFromConf(conf Config, clientConfig *corev1.ConfigMap) (Client, error) { +func NewClientFromConf(conf Config, clientConfig *corev1.ConfigMap, options ...ClientOption) (Client, error) { cacheMutex.Lock() defer cacheMutex.Unlock() @@ -199,6 +200,24 @@ func NewClientFromConf(conf Config, clientConfig *corev1.ConfigMap) (Client, err }, }, } + + // Add project config if a ProjectID is defined in the client config. + if conf.ProjectID != "" { + user.Project = Project{ + ID: conf.ProjectID, + } + } + + oldUser := c.user + c.user = user + for _, fn := range options { + if err := fn(c); err != nil { + return nil, err + } + } + user = c.user + c.user = oldUser + if found, err := c.GetUserWithKeys(user); err != nil { return nil, err } else if !found { @@ -212,10 +231,21 @@ func NewClientFromConf(conf Config, clientConfig *corev1.ConfigMap) (Client, err } // NewClientInDomainAndAccount returns a new client in the specified domain and account. -func (c *client) NewClientInDomainAndAccount(domain string, account string) (Client, error) { +func (c *client) NewClientInDomainAndAccount(domain string, account string, options ...ClientOption) (Client, error) { user := &User{} user.Account.Domain.Path = domain user.Account.Name = account + + oldUser := c.user + c.user = user + for _, fn := range options { + if err := fn(c); err != nil { + return nil, err + } + } + user = c.user + c.user = oldUser + if found, err := c.GetUserWithKeys(user); err != nil { return nil, err } else if !found { @@ -286,3 +316,32 @@ func GetClientCacheTTL(clientConfig *corev1.ConfigMap) time.Duration { return cacheTTL } + +// ClientOption can be passed to new client functions to set custom options. +type ClientOption func(*client) error + +// WithProject takes either a project name or ID and sets the project ID parameter in the user object. +func WithProject(project string) ClientOption { + return func(c *client) error { + if c == nil || c.user == nil { + return errors.New("cannot create client with nil user") + } + + // project arg empty or project ID already set through cloud config. + if project == "" || c.user.Project.ID != "" { + return nil + } + + if !cloudstack.IsID(project) { + id, _, err := c.cs.Project.GetProjectID(project) + if err != nil { + return err + } + project = id + } + + c.user.Project.ID = project + + return nil + } +} diff --git a/pkg/cloud/instance.go b/pkg/cloud/instance.go index b939d593..8ab26318 100644 --- a/pkg/cloud/instance.go +++ b/pkg/cloud/instance.go @@ -73,7 +73,7 @@ func setMachineDataFromVMMetrics(vmResponse *cloudstack.VirtualMachinesMetric, c func (c *client) ResolveVMInstanceDetails(csMachine *infrav1.CloudStackMachine) error { // Attempt to fetch by ID. if csMachine.Spec.InstanceID != nil { - vmResp, count, err := c.cs.VirtualMachine.GetVirtualMachinesMetricByID(*csMachine.Spec.InstanceID) + vmResp, count, err := c.cs.VirtualMachine.GetVirtualMachinesMetricByID(*csMachine.Spec.InstanceID, cloudstack.WithProject(c.user.Project.ID)) if err != nil && !strings.Contains(strings.ToLower(err.Error()), "no match found") { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) @@ -89,7 +89,7 @@ func (c *client) ResolveVMInstanceDetails(csMachine *infrav1.CloudStackMachine) // Attempt fetch by name. if csMachine.Name != "" { - vmResp, count, err := c.cs.VirtualMachine.GetVirtualMachinesMetricByName(csMachine.Name) // add opts usage + vmResp, count, err := c.cs.VirtualMachine.GetVirtualMachinesMetricByName(csMachine.Name, cloudstack.WithProject(c.user.Project.ID)) if err != nil && !strings.Contains(strings.ToLower(err.Error()), "no match") { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) @@ -109,7 +109,7 @@ func (c *client) ResolveVMInstanceDetails(csMachine *infrav1.CloudStackMachine) // resolveServiceOffering attempts to look up the service offering of a CloudStackMachine by ID first and name second. func (c *client) resolveServiceOffering(csMachine *infrav1.CloudStackMachine, zoneID string) (offering *cloudstack.ServiceOffering, retErr error) { if len(csMachine.Spec.Offering.ID) > 0 { - csOffering, count, err := c.cs.ServiceOffering.GetServiceOfferingByID(csMachine.Spec.Offering.ID) + csOffering, count, err := c.cs.ServiceOffering.GetServiceOfferingByID(csMachine.Spec.Offering.ID, cloudstack.WithProject(c.user.Project.ID)) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) @@ -127,7 +127,7 @@ func (c *client) resolveServiceOffering(csMachine *infrav1.CloudStackMachine, zo return csOffering, nil } - csOffering, count, err := c.cs.ServiceOffering.GetServiceOfferingByName(csMachine.Spec.Offering.Name, cloudstack.WithZone(zoneID)) + csOffering, count, err := c.cs.ServiceOffering.GetServiceOfferingByName(csMachine.Spec.Offering.Name, cloudstack.WithZone(zoneID), cloudstack.WithProject(c.user.Project.ID)) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) @@ -147,7 +147,7 @@ func (c *client) resolveTemplate( zoneID string, ) (templateID string, retErr error) { if len(csMachine.Spec.Template.ID) > 0 { - csTemplate, count, err := c.cs.Template.GetTemplateByID(csMachine.Spec.Template.ID, "executable") + csTemplate, count, err := c.cs.Template.GetTemplateByID(csMachine.Spec.Template.ID, "executable", cloudstack.WithProject(c.user.Project.ID)) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) @@ -165,15 +165,17 @@ func (c *client) resolveTemplate( return csMachine.Spec.Template.ID, nil } - templateID, count, err := c.cs.Template.GetTemplateID(csMachine.Spec.Template.Name, "executable", zoneID, func(_ *cloudstack.CloudStackClient, i interface{}) error { - v, ok := i.(*cloudstack.ListTemplatesParams) - if !ok { - return fmt.Errorf("expected a cloudstack.ListTemplatesParams but got a %T", i) - } - v.SetShowunique(true) + templateID, count, err := c.cs.Template.GetTemplateID(csMachine.Spec.Template.Name, "executable", zoneID, + cloudstack.WithProject(c.user.Project.ID), + func(_ *cloudstack.CloudStackClient, i interface{}) error { + v, ok := i.(*cloudstack.ListTemplatesParams) + if !ok { + return fmt.Errorf("expected a cloudstack.ListTemplatesParams but got a %T", i) + } + v.SetShowunique(true) - return nil - }) + return nil + }) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) @@ -196,7 +198,7 @@ func (c *client) resolveDiskOffering(csMachine *infrav1.CloudStackMachine, zoneI } diskOfferingID = csMachine.Spec.DiskOffering.ID if len(csMachine.Spec.DiskOffering.Name) > 0 { - diskID, count, err := c.cs.DiskOffering.GetDiskOfferingID(csMachine.Spec.DiskOffering.Name, cloudstack.WithZone(zoneID)) + diskID, count, err := c.cs.DiskOffering.GetDiskOfferingID(csMachine.Spec.DiskOffering.Name, cloudstack.WithZone(zoneID), cloudstack.WithProject(c.user.Project.ID)) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) @@ -224,7 +226,7 @@ func (c *client) resolveDiskOffering(csMachine *infrav1.CloudStackMachine, zoneI } func verifyDiskoffering(csMachine *infrav1.CloudStackMachine, c *client, diskOfferingID string, retErr error) (string, error) { - csDiskOffering, count, err := c.cs.DiskOffering.GetDiskOfferingByID(diskOfferingID) + csDiskOffering, count, err := c.cs.DiskOffering.GetDiskOfferingByID(diskOfferingID, cloudstack.WithProject(c.user.Project.ID)) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) @@ -302,6 +304,35 @@ func (c *client) checkDomainLimits(offering *cloudstack.ServiceOffering) error { return nil } +// CheckProjectLimits Checks the project's limit of VM, CPU & Memory. +func (c *client) CheckProjectLimits(offering *cloudstack.ServiceOffering) error { + if c.user.Project.ID == "" { + return nil + } + + if c.user.Project.CPUAvailable != LimitUnlimited { + cpuAvailable, err := strconv.ParseInt(c.user.Project.CPUAvailable, 10, 0) + if err == nil && int64(offering.Cpunumber) > cpuAvailable { + return fmt.Errorf("CPU available (%d) in project can't fulfil the requirement: %d", cpuAvailable, offering.Cpunumber) + } + } + + if c.user.Project.MemoryAvailable != LimitUnlimited { + memoryAvailable, err := strconv.ParseInt(c.user.Project.MemoryAvailable, 10, 0) + if err == nil && int64(offering.Memory) > memoryAvailable { + return fmt.Errorf("memory available (%d) in project can't fulfil the requirement: %d", memoryAvailable, offering.Memory) + } + } + + if c.user.Project.VMAvailable != LimitUnlimited { + vmAvailable, err := strconv.ParseInt(c.user.Project.VMAvailable, 10, 0) + if err == nil && vmAvailable < 1 { + return fmt.Errorf("VM Limit in project has reached it's maximum value") + } + } + return nil +} + // checkLimits will check the account & domain limits. func (c *client) checkLimits( offering *cloudstack.ServiceOffering, @@ -320,6 +351,11 @@ func (c *client) checkLimits( return err } + err = c.CheckProjectLimits(offering) + if err != nil { + return err + } + return nil } @@ -346,6 +382,7 @@ func (c *client) deployVM( setIfNotEmpty(csMachine.Name, p.SetName) setIfNotEmpty(capiMachine.Name, p.SetDisplayname) setIfNotEmpty(diskOfferingID, p.SetDiskofferingid) + setIfNotEmpty(c.user.Project.ID, p.SetProjectid) if csMachine.Spec.DiskOffering != nil { setIntIfPositive(csMachine.Spec.DiskOffering.CustomSize, p.SetSize) } @@ -378,7 +415,7 @@ func (c *client) deployVM( // CloudStack may have created the VM even though it reported an error. We attempt to // retrieve the VM so we can populate the CloudStackMachine for the user to manually // clean up. - vm, findErr := findVirtualMachine(c.cs.VirtualMachine, templateID, fd, csMachine) + vm, findErr := findVirtualMachine(c.cs.VirtualMachine, c.user.Project.ID, templateID, fd, csMachine) if findErr != nil { if errors.Is(findErr, ErrNotFound) { // We didn't find a VM so return the original error. @@ -439,6 +476,7 @@ func (c *client) GetOrCreateVMInstance( // domain zone and failure domain network. If no virtual machine is found it returns nil, ErrNotFound. func findVirtualMachine( client cloudstack.VirtualMachineServiceIface, + projectID string, templateID string, failureDomain *infrav1.CloudStackFailureDomain, machine *infrav1.CloudStackMachine, @@ -448,6 +486,7 @@ func findVirtualMachine( params.SetZoneid(failureDomain.Spec.Zone.ID) params.SetNetworkid(failureDomain.Spec.Zone.Network.ID) params.SetName(machine.Name) + setIfNotEmpty(projectID, params.SetProjectid) response, err := client.ListVirtualMachines(params) if err != nil { @@ -518,6 +557,7 @@ func (c *client) listVMInstanceDatadiskVolumeIDs(instanceID string) ([]string, e // - https://github.com/apache/cloudstack/blob/b69cc0272d48f0aea7353627d760c27c284dad84/engine/orchestration/src/main/java/com/cloud/vm/VirtualMachineManagerImpl.java#L524 // - https://github.com/kubernetes-sigs/cluster-api-provider-cloudstack/issues/389 p.SetKeyword("DATA-") + setIfNotEmpty(c.user.Project.ID, p.SetProjectid) listVolResp, err := c.csAsync.Volume.ListVolumes(p) if err != nil { diff --git a/pkg/cloud/instance_test.go b/pkg/cloud/instance_test.go index b5dd8bb0..118e79ed 100644 --- a/pkg/cloud/instance_test.go +++ b/pkg/cloud/instance_test.go @@ -76,42 +76,42 @@ var _ = Describe("Instance", func() { Context("when fetching a VM instance", func() { It("Handles an unknown error when fetching by ID", func() { - vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID).Return(nil, -1, unknownError) + vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()).Return(nil, -1, unknownError) Ω(client.ResolveVMInstanceDetails(dummies.CSMachine1)).To(MatchError(unknownErrorMessage)) }) It("Handles finding more than one VM instance by ID", func() { - vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID).Return(nil, 2, nil) + vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()).Return(nil, 2, nil) Ω(client.ResolveVMInstanceDetails(dummies.CSMachine1)). Should(MatchError("found more than one VM Instance with ID " + *dummies.CSMachine1.Spec.InstanceID)) }) It("sets dummies.CSMachine1 spec and status values when VM instance found by ID", func() { vmsResp := &cloudstack.VirtualMachinesMetric{Id: *dummies.CSMachine1.Spec.InstanceID} - vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID).Return(vmsResp, 1, nil) + vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()).Return(vmsResp, 1, nil) Ω(client.ResolveVMInstanceDetails(dummies.CSMachine1)).Should(Succeed()) Ω(dummies.CSMachine1.Spec.ProviderID).Should(Equal(ptr.To("cloudstack:///" + vmsResp.Id))) Ω(dummies.CSMachine1.Spec.InstanceID).Should(Equal(ptr.To(vmsResp.Id))) }) It("handles an unknown error when fetching by name", func() { - vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID).Return(nil, -1, notFoundError) - vms.EXPECT().GetVirtualMachinesMetricByName(dummies.CSMachine1.Name).Return(nil, -1, unknownError) + vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()).Return(nil, -1, notFoundError) + vms.EXPECT().GetVirtualMachinesMetricByName(dummies.CSMachine1.Name, gomock.Any()).Return(nil, -1, unknownError) Ω(client.ResolveVMInstanceDetails(dummies.CSMachine1)).Should(MatchError(unknownErrorMessage)) }) It("handles finding more than one VM instance by Name", func() { - vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID).Return(nil, -1, notFoundError) - vms.EXPECT().GetVirtualMachinesMetricByName(dummies.CSMachine1.Name).Return(nil, 2, nil) + vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()).Return(nil, -1, notFoundError) + vms.EXPECT().GetVirtualMachinesMetricByName(dummies.CSMachine1.Name, gomock.Any()).Return(nil, 2, nil) Ω(client.ResolveVMInstanceDetails(dummies.CSMachine1)).Should( MatchError("found more than one VM Instance with name " + dummies.CSMachine1.Name)) }) It("sets dummies.CSMachine1 spec and status values when VM instance found by Name", func() { - vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID).Return(nil, -1, notFoundError) - vms.EXPECT().GetVirtualMachinesMetricByName(dummies.CSMachine1.Name). + vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()).Return(nil, -1, notFoundError) + vms.EXPECT().GetVirtualMachinesMetricByName(dummies.CSMachine1.Name, gomock.Any()). Return(&cloudstack.VirtualMachinesMetric{Id: *dummies.CSMachine1.Spec.InstanceID}, -1, nil) Ω(client.ResolveVMInstanceDetails(dummies.CSMachine1)).Should(Succeed()) @@ -125,19 +125,19 @@ var _ = Describe("Instance", func() { vmMetricResp := &cloudstack.VirtualMachinesMetric{} expectVMNotFound := func() { - vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID).Return(nil, -1, notFoundError) - vms.EXPECT().GetVirtualMachinesMetricByName(dummies.CSMachine1.Name).Return(nil, -1, notFoundError) + vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()).Return(nil, -1, notFoundError) + vms.EXPECT().GetVirtualMachinesMetricByName(dummies.CSMachine1.Name, gomock.Any()).Return(nil, -1, notFoundError) } It("doesn't re-create if one already exists.", func() { - vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID).Return(vmMetricResp, -1, nil) + vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()).Return(vmMetricResp, -1, nil) Ω(client.GetOrCreateVMInstance( dummies.CSMachine1, dummies.CAPIMachine, dummies.CSFailureDomain1, dummies.CSAffinityGroup, "")). Should(Succeed()) }) It("returns unknown error while fetching VM instance", func() { - vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID).Return(nil, -1, unknownError) + vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()).Return(nil, -1, unknownError) Ω(client.GetOrCreateVMInstance( dummies.CSMachine1, dummies.CAPIMachine, dummies.CSFailureDomain1, dummies.CSAffinityGroup, "")). Should(MatchError(unknownErrorMessage)) @@ -170,7 +170,7 @@ var _ = Describe("Instance", func() { Id: dummies.CSMachine1.Spec.Offering.ID, Name: dummies.CSMachine1.Spec.Offering.Name, }, 1, nil) - ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any()). + ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any(), gomock.Any()). Return("", -1, unknownError) Ω(client.GetOrCreateVMInstance( dummies.CSMachine1, dummies.CAPIMachine, dummies.CSFailureDomain1, dummies.CSAffinityGroup, "")). @@ -185,7 +185,7 @@ var _ = Describe("Instance", func() { Id: dummies.CSMachine1.Spec.Offering.ID, Name: dummies.CSMachine1.Spec.Offering.Name, }, 1, nil) - ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any()).Return("", 2, nil) + ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any(), gomock.Any()).Return("", 2, nil) Ω(client.GetOrCreateVMInstance( dummies.CSMachine1, dummies.CAPIMachine, dummies.CSFailureDomain1, dummies.CSAffinityGroup, "")). ShouldNot(Succeed()) @@ -199,7 +199,7 @@ var _ = Describe("Instance", func() { Id: dummies.CSMachine1.Spec.Offering.ID, Name: dummies.CSMachine1.Spec.Offering.Name, }, 1, nil) - ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any()).Return(dummies.CSMachine1.Spec.Template.ID, 1, nil) + ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any(), gomock.Any()).Return(dummies.CSMachine1.Spec.Template.ID, 1, nil) dos.EXPECT().GetDiskOfferingID(dummies.CSMachine1.Spec.DiskOffering.Name, gomock.Any()).Return(diskOfferingFakeID, 2, nil) Ω(client.GetOrCreateVMInstance( dummies.CSMachine1, dummies.CAPIMachine, dummies.CSFailureDomain1, dummies.CSAffinityGroup, "")). @@ -213,9 +213,9 @@ var _ = Describe("Instance", func() { Id: dummies.CSMachine1.Spec.Offering.ID, Name: dummies.CSMachine1.Spec.Offering.Name, }, 1, nil) - ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any()).Return(dummies.CSMachine1.Spec.Template.ID, 1, nil) + ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any(), gomock.Any()).Return(dummies.CSMachine1.Spec.Template.ID, 1, nil) dos.EXPECT().GetDiskOfferingID(dummies.CSMachine1.Spec.DiskOffering.Name, gomock.Any()).Return(diskOfferingFakeID, 1, nil) - dos.EXPECT().GetDiskOfferingByID(diskOfferingFakeID).Return(&cloudstack.DiskOffering{Iscustomized: false}, 1, unknownError) + dos.EXPECT().GetDiskOfferingByID(diskOfferingFakeID, gomock.Any()).Return(&cloudstack.DiskOffering{Iscustomized: false}, 1, unknownError) Ω(client.GetOrCreateVMInstance( dummies.CSMachine1, dummies.CAPIMachine, dummies.CSFailureDomain1, dummies.CSAffinityGroup, "")). ShouldNot(Succeed()) @@ -229,9 +229,9 @@ var _ = Describe("Instance", func() { Id: dummies.CSMachine1.Spec.Offering.ID, Name: dummies.CSMachine1.Spec.Offering.Name, }, 1, nil) - ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any()).Return(dummies.CSMachine1.Spec.Template.ID, 1, nil) + ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any(), gomock.Any()).Return(dummies.CSMachine1.Spec.Template.ID, 1, nil) dos.EXPECT().GetDiskOfferingID(dummies.CSMachine1.Spec.DiskOffering.Name, gomock.Any()).Return(diskOfferingFakeID, 1, nil) - dos.EXPECT().GetDiskOfferingByID(diskOfferingFakeID).Return(&cloudstack.DiskOffering{Iscustomized: false}, 1, nil) + dos.EXPECT().GetDiskOfferingByID(diskOfferingFakeID, gomock.Any()).Return(&cloudstack.DiskOffering{Iscustomized: false}, 1, nil) Ω(client.GetOrCreateVMInstance( dummies.CSMachine1, dummies.CAPIMachine, dummies.CSFailureDomain1, dummies.CSAffinityGroup, "")). ShouldNot(Succeed()) @@ -247,9 +247,9 @@ var _ = Describe("Instance", func() { Cpunumber: 1, Memory: 1024, }, 1, nil) - ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any()).Return(dummies.CSMachine1.Spec.Template.ID, 1, nil) + ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any(), gomock.Any()).Return(dummies.CSMachine1.Spec.Template.ID, 1, nil) dos.EXPECT().GetDiskOfferingID(dummies.CSMachine1.Spec.DiskOffering.Name, gomock.Any()).Return(diskOfferingFakeID, 1, nil) - dos.EXPECT().GetDiskOfferingByID(diskOfferingFakeID).Return(&cloudstack.DiskOffering{Iscustomized: true}, 1, nil) + dos.EXPECT().GetDiskOfferingByID(diskOfferingFakeID, gomock.Any()).Return(&cloudstack.DiskOffering{Iscustomized: true}, 1, nil) Ω(client.GetOrCreateVMInstance( dummies.CSMachine1, dummies.CAPIMachine, dummies.CSFailureDomain1, dummies.CSAffinityGroup, "")). ShouldNot(Succeed()) @@ -277,6 +277,12 @@ var _ = Describe("Instance", func() { MemoryAvailable: "2048", VMAvailable: "20", }, + Project: cloud.Project{ + ID: "123", + CPUAvailable: "20", + MemoryAvailable: "2048", + VMAvailable: "20", + }, } c := cloud.NewClientFromCSAPIClient(mockClient, user) Ω(c.GetOrCreateVMInstance( @@ -305,6 +311,12 @@ var _ = Describe("Instance", func() { MemoryAvailable: "2048", VMAvailable: "20", }, + Project: cloud.Project{ + ID: "123", + CPUAvailable: "20", + MemoryAvailable: "2048", + VMAvailable: "20", + }, } c := cloud.NewClientFromCSAPIClient(mockClient, user) Ω(c.GetOrCreateVMInstance( @@ -312,6 +324,40 @@ var _ = Describe("Instance", func() { Should(MatchError(MatchRegexp("CPU available .* in domain can't fulfil the requirement:.*"))) }) + It("returns errors when there are not enough available CPU in project", func() { + expectVMNotFound() + dummies.CSMachine1.Spec.DiskOffering.CustomSize = 0 + sos.EXPECT().GetServiceOfferingByName(dummies.CSMachine1.Spec.Offering.Name, gomock.Any()). + Return(&cloudstack.ServiceOffering{ + Id: dummies.CSMachine1.Spec.Offering.ID, + Name: dummies.CSMachine1.Spec.Offering.Name, + Cpunumber: 2, + Memory: 1024, + }, 1, nil) + user := &cloud.User{ + Account: cloud.Account{ + Domain: cloud.Domain{ + CPUAvailable: "20", + MemoryAvailable: "2048", + VMAvailable: "20", + }, + CPUAvailable: "20", + MemoryAvailable: "2048", + VMAvailable: "20", + }, + Project: cloud.Project{ + ID: "123", + CPUAvailable: "1", + MemoryAvailable: "2048", + VMAvailable: "20", + }, + } + c := cloud.NewClientFromCSAPIClient(mockClient, user) + Ω(c.GetOrCreateVMInstance( + dummies.CSMachine1, dummies.CAPIMachine, dummies.CSFailureDomain1, dummies.CSAffinityGroup, "")). + Should(MatchError(MatchRegexp("CPU available .* in project can't fulfil the requirement:.*"))) + }) + It("returns errors when there is not enough available memory in account", func() { expectVMNotFound() dummies.CSMachine1.Spec.DiskOffering.CustomSize = 0 @@ -333,6 +379,12 @@ var _ = Describe("Instance", func() { MemoryAvailable: "512", VMAvailable: "20", }, + Project: cloud.Project{ + ID: "123", + CPUAvailable: "1", + MemoryAvailable: "2048", + VMAvailable: "20", + }, } c := cloud.NewClientFromCSAPIClient(mockClient, user) Ω(c.GetOrCreateVMInstance( @@ -361,6 +413,12 @@ var _ = Describe("Instance", func() { MemoryAvailable: "2048", VMAvailable: "20", }, + Project: cloud.Project{ + ID: "123", + CPUAvailable: "1", + MemoryAvailable: "2048", + VMAvailable: "20", + }, } c := cloud.NewClientFromCSAPIClient(mockClient, user) Ω(c.GetOrCreateVMInstance( @@ -368,6 +426,40 @@ var _ = Describe("Instance", func() { Should(MatchError(MatchRegexp("memory available .* in domain can't fulfil the requirement:.*"))) }) + It("returns errors when there is not enough available memory in project", func() { + expectVMNotFound() + dummies.CSMachine1.Spec.DiskOffering.CustomSize = 0 + sos.EXPECT().GetServiceOfferingByName(dummies.CSMachine1.Spec.Offering.Name, gomock.Any()). + Return(&cloudstack.ServiceOffering{ + Id: dummies.CSMachine1.Spec.Offering.ID, + Name: dummies.CSMachine1.Spec.Offering.Name, + Cpunumber: 2, + Memory: 1024, + }, 1, nil) + user := &cloud.User{ + Account: cloud.Account{ + Domain: cloud.Domain{ + CPUAvailable: "20", + MemoryAvailable: "2048", + VMAvailable: "20", + }, + CPUAvailable: "20", + MemoryAvailable: "2048", + VMAvailable: "20", + }, + Project: cloud.Project{ + ID: "123", + CPUAvailable: "20", + MemoryAvailable: "512", + VMAvailable: "20", + }, + } + c := cloud.NewClientFromCSAPIClient(mockClient, user) + Ω(c.GetOrCreateVMInstance( + dummies.CSMachine1, dummies.CAPIMachine, dummies.CSFailureDomain1, dummies.CSAffinityGroup, "")). + Should(MatchError(MatchRegexp("memory available .* in project can't fulfil the requirement:.*"))) + }) + It("returns errors when there is not enough available VM limit in account", func() { expectVMNotFound() dummies.CSMachine1.Spec.DiskOffering.CustomSize = 0 @@ -389,6 +481,12 @@ var _ = Describe("Instance", func() { MemoryAvailable: "2048", VMAvailable: "0", }, + Project: cloud.Project{ + ID: "123", + CPUAvailable: "20", + MemoryAvailable: "512", + VMAvailable: "20", + }, } c := cloud.NewClientFromCSAPIClient(mockClient, user) Ω(c.GetOrCreateVMInstance( @@ -417,6 +515,12 @@ var _ = Describe("Instance", func() { MemoryAvailable: "2048", VMAvailable: "10", }, + Project: cloud.Project{ + ID: "123", + CPUAvailable: "20", + MemoryAvailable: "512", + VMAvailable: "20", + }, } c := cloud.NewClientFromCSAPIClient(mockClient, user) Ω(c.GetOrCreateVMInstance( @@ -425,6 +529,40 @@ var _ = Describe("Instance", func() { }) }) + It("returns errors when there is not enough available VM limit in project", func() { + expectVMNotFound() + dummies.CSMachine1.Spec.DiskOffering.CustomSize = 0 + sos.EXPECT().GetServiceOfferingByName(dummies.CSMachine1.Spec.Offering.Name, gomock.Any()). + Return(&cloudstack.ServiceOffering{ + Id: dummies.CSMachine1.Spec.Offering.ID, + Name: dummies.CSMachine1.Spec.Offering.Name, + Cpunumber: 2, + Memory: 1024, + }, 1, nil) + user := &cloud.User{ + Account: cloud.Account{ + Domain: cloud.Domain{ + CPUAvailable: "20", + MemoryAvailable: "2048", + VMAvailable: "10", + }, + CPUAvailable: "20", + MemoryAvailable: "2048", + VMAvailable: "10", + }, + Project: cloud.Project{ + ID: "123", + CPUAvailable: "20", + MemoryAvailable: "2048", + VMAvailable: "0", + }, + } + c := cloud.NewClientFromCSAPIClient(mockClient, user) + Ω(c.GetOrCreateVMInstance( + dummies.CSMachine1, dummies.CAPIMachine, dummies.CSFailureDomain1, dummies.CSAffinityGroup, "")). + Should(MatchError("VM Limit in project has reached it's maximum value")) + }) + It("handles deployment errors", func() { expectVMNotFound() sos.EXPECT().GetServiceOfferingByName(dummies.CSMachine1.Spec.Offering.Name, gomock.Any()). @@ -434,11 +572,11 @@ var _ = Describe("Instance", func() { Cpunumber: 1, Memory: 1024, }, 1, nil) - ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any()). + ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any(), gomock.Any()). Return(templateFakeID, 1, nil) dos.EXPECT().GetDiskOfferingID(dummies.CSMachine1.Spec.DiskOffering.Name, gomock.Any()). Return(diskOfferingFakeID, 1, nil) - dos.EXPECT().GetDiskOfferingByID(diskOfferingFakeID). + dos.EXPECT().GetDiskOfferingByID(diskOfferingFakeID, gomock.Any()). Return(&cloudstack.DiskOffering{Iscustomized: false}, 1, nil) vms.EXPECT().NewDeployVirtualMachineParams(offeringFakeID, templateFakeID, dummies.Zone1.ID). Return(&cloudstack.DeployVirtualMachineParams{}) @@ -453,12 +591,12 @@ var _ = Describe("Instance", func() { Context("when using UUIDs and/or names to locate service offerings and templates", func() { BeforeEach(func() { gomock.InOrder( - vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID). + vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()). Return(nil, -1, notFoundError), - vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID). + vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()). Return(&cloudstack.VirtualMachinesMetric{}, 1, nil)) - vms.EXPECT().GetVirtualMachinesMetricByName(dummies.CSMachine1.Name).Return(nil, -1, notFoundError) + vms.EXPECT().GetVirtualMachinesMetricByName(dummies.CSMachine1.Name, gomock.Any()).Return(nil, -1, notFoundError) }) ActionAndAssert := func() { @@ -504,8 +642,8 @@ var _ = Describe("Instance", func() { Memory: 1024, }, 1, nil) dos.EXPECT().GetDiskOfferingID(dummies.CSMachine1.Spec.DiskOffering.Name, gomock.Any()).Return(diskOfferingFakeID, 1, nil) - dos.EXPECT().GetDiskOfferingByID(dummies.CSMachine1.Spec.DiskOffering.ID).Return(&cloudstack.DiskOffering{Iscustomized: false}, 1, nil) - ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any()). + dos.EXPECT().GetDiskOfferingByID(dummies.CSMachine1.Spec.DiskOffering.ID, gomock.Any()).Return(&cloudstack.DiskOffering{Iscustomized: false}, 1, nil) + ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any(), gomock.Any()). Return(templateFakeID, 1, nil) ActionAndAssert() @@ -523,7 +661,7 @@ var _ = Describe("Instance", func() { Cpunumber: 1, Memory: 1024, }, 1, nil) - ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any()). + ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any(), gomock.Any()). Return(templateFakeID, 1, nil) ActionAndAssert() @@ -536,15 +674,15 @@ var _ = Describe("Instance", func() { dummies.CSMachine1.Spec.Offering.Name = "" dummies.CSMachine1.Spec.Template.Name = templateName - sos.EXPECT().GetServiceOfferingByID(dummies.CSMachine1.Spec.Offering.ID).Return(&cloudstack.ServiceOffering{ + sos.EXPECT().GetServiceOfferingByID(dummies.CSMachine1.Spec.Offering.ID, gomock.Any()).Return(&cloudstack.ServiceOffering{ Id: offeringFakeID, Cpunumber: 1, Memory: 1024, }, 1, nil) - ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any()). + ts.EXPECT().GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any(), gomock.Any()). Return(templateFakeID, 1, nil) dos.EXPECT().GetDiskOfferingID(dummies.CSMachine1.Spec.DiskOffering.Name, gomock.Any()).Return(diskOfferingFakeID, 1, nil) - dos.EXPECT().GetDiskOfferingByID(dummies.CSMachine1.Spec.DiskOffering.ID).Return(&cloudstack.DiskOffering{Iscustomized: false}, 1, nil) + dos.EXPECT().GetDiskOfferingByID(dummies.CSMachine1.Spec.DiskOffering.ID, gomock.Any()).Return(&cloudstack.DiskOffering{Iscustomized: false}, 1, nil) ActionAndAssert() }) @@ -562,9 +700,9 @@ var _ = Describe("Instance", func() { Memory: 1024, }, 1, nil) - ts.EXPECT().GetTemplateByID(dummies.CSMachine1.Spec.Template.ID, executableFilter).Return(&cloudstack.Template{Name: ""}, 1, nil) + ts.EXPECT().GetTemplateByID(dummies.CSMachine1.Spec.Template.ID, executableFilter, gomock.Any()).Return(&cloudstack.Template{Name: ""}, 1, nil) dos.EXPECT().GetDiskOfferingID(dummies.CSMachine1.Spec.DiskOffering.Name, gomock.Any()).Return(diskOfferingFakeID, 1, nil) - dos.EXPECT().GetDiskOfferingByID(dummies.CSMachine1.Spec.DiskOffering.ID).Return(&cloudstack.DiskOffering{Iscustomized: false}, 1, nil) + dos.EXPECT().GetDiskOfferingByID(dummies.CSMachine1.Spec.DiskOffering.ID, gomock.Any()).Return(&cloudstack.DiskOffering{Iscustomized: false}, 1, nil) ActionAndAssert() }) @@ -576,7 +714,7 @@ var _ = Describe("Instance", func() { dummies.CSMachine1.Spec.Offering.Name = "" dummies.CSMachine1.Spec.Template.Name = "" - sos.EXPECT().GetServiceOfferingByID(dummies.CSMachine1.Spec.Offering.ID). + sos.EXPECT().GetServiceOfferingByID(dummies.CSMachine1.Spec.Offering.ID, gomock.Any()). Return(&cloudstack.ServiceOffering{ Id: offeringFakeID, Cpunumber: 1, @@ -584,9 +722,9 @@ var _ = Describe("Instance", func() { }, 1, nil) dos.EXPECT().GetDiskOfferingID(dummies.CSMachine1.Spec.DiskOffering.Name, gomock.Any()). Return(diskOfferingFakeID, 1, nil) - dos.EXPECT().GetDiskOfferingByID(dummies.CSMachine1.Spec.DiskOffering.ID). + dos.EXPECT().GetDiskOfferingByID(dummies.CSMachine1.Spec.DiskOffering.ID, gomock.Any()). Return(&cloudstack.DiskOffering{Iscustomized: false}, 1, nil) - ts.EXPECT().GetTemplateByID(dummies.CSMachine1.Spec.Template.ID, executableFilter). + ts.EXPECT().GetTemplateByID(dummies.CSMachine1.Spec.Template.ID, executableFilter, gomock.Any()). Return(&cloudstack.Template{Name: templateName}, 1, nil) ActionAndAssert() @@ -599,15 +737,15 @@ var _ = Describe("Instance", func() { dummies.CSMachine1.Spec.Offering.Name = offeringName dummies.CSMachine1.Spec.Template.Name = templateName - sos.EXPECT().GetServiceOfferingByID(dummies.CSMachine1.Spec.Offering.ID).Return(&cloudstack.ServiceOffering{ + sos.EXPECT().GetServiceOfferingByID(dummies.CSMachine1.Spec.Offering.ID, gomock.Any()).Return(&cloudstack.ServiceOffering{ Id: dummies.CSMachine1.Spec.Offering.ID, Name: dummies.CSMachine1.Spec.Offering.Name, Cpunumber: 1, Memory: 1024, }, 1, nil) - ts.EXPECT().GetTemplateByID(dummies.CSMachine1.Spec.Template.ID, executableFilter).Return(&cloudstack.Template{Name: templateName}, 1, nil) + ts.EXPECT().GetTemplateByID(dummies.CSMachine1.Spec.Template.ID, executableFilter, gomock.Any()).Return(&cloudstack.Template{Name: templateName}, 1, nil) dos.EXPECT().GetDiskOfferingID(dummies.CSMachine1.Spec.DiskOffering.Name, gomock.Any()).Return(diskOfferingFakeID, 1, nil) - dos.EXPECT().GetDiskOfferingByID(dummies.CSMachine1.Spec.DiskOffering.ID).Return(&cloudstack.DiskOffering{Iscustomized: false}, 1, nil) + dos.EXPECT().GetDiskOfferingByID(dummies.CSMachine1.Spec.DiskOffering.ID, gomock.Any()).Return(&cloudstack.DiskOffering{Iscustomized: false}, 1, nil) ActionAndAssert() }) @@ -615,9 +753,9 @@ var _ = Describe("Instance", func() { Context("when using both UUIDs and names to locate service offerings and templates", func() { BeforeEach(func() { - vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID). + vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()). Return(nil, -1, notFoundError) - vms.EXPECT().GetVirtualMachinesMetricByName(dummies.CSMachine1.Name).Return(nil, -1, notFoundError) + vms.EXPECT().GetVirtualMachinesMetricByName(dummies.CSMachine1.Name, gomock.Any()).Return(nil, -1, notFoundError) }) It("works with Id and name both provided, offering name mismatch", func() { @@ -626,7 +764,7 @@ var _ = Describe("Instance", func() { dummies.CSMachine1.Spec.Offering.Name = offeringName dummies.CSMachine1.Spec.Template.Name = templateName - sos.EXPECT().GetServiceOfferingByID(dummies.CSMachine1.Spec.Offering.ID).Return(&cloudstack.ServiceOffering{Name: "offering-not-match"}, 1, nil) + sos.EXPECT().GetServiceOfferingByID(dummies.CSMachine1.Spec.Offering.ID, gomock.Any()).Return(&cloudstack.ServiceOffering{Name: "offering-not-match"}, 1, nil) requiredRegexp := "offering name %s does not match name %s returned using UUID %s" Ω(client.GetOrCreateVMInstance( dummies.CSMachine1, dummies.CAPIMachine, dummies.CSFailureDomain1, dummies.CSAffinityGroup, "")). @@ -639,8 +777,8 @@ var _ = Describe("Instance", func() { dummies.CSMachine1.Spec.Offering.Name = offeringName dummies.CSMachine1.Spec.Template.Name = templateName - sos.EXPECT().GetServiceOfferingByID(dummies.CSMachine1.Spec.Offering.ID).Return(&cloudstack.ServiceOffering{Name: offeringName}, 1, nil) - ts.EXPECT().GetTemplateByID(dummies.CSMachine1.Spec.Template.ID, executableFilter).Return(&cloudstack.Template{Name: "template-not-match"}, 1, nil) + sos.EXPECT().GetServiceOfferingByID(dummies.CSMachine1.Spec.Offering.ID, gomock.Any()).Return(&cloudstack.ServiceOffering{Name: offeringName}, 1, nil) + ts.EXPECT().GetTemplateByID(dummies.CSMachine1.Spec.Template.ID, executableFilter, gomock.Any()).Return(&cloudstack.Template{Name: "template-not-match"}, 1, nil) requiredRegexp := "template name %s does not match name %s returned using UUID %s" Ω(client.GetOrCreateVMInstance( dummies.CSMachine1, dummies.CAPIMachine, dummies.CSFailureDomain1, dummies.CSAffinityGroup, "")). @@ -655,8 +793,8 @@ var _ = Describe("Instance", func() { dummies.CSMachine1.Spec.Template.Name = templateName dummies.CSMachine1.Spec.DiskOffering.Name = "diskoffering" - sos.EXPECT().GetServiceOfferingByID(dummies.CSMachine1.Spec.Offering.ID).Return(&cloudstack.ServiceOffering{Name: offeringName}, 1, nil) - ts.EXPECT().GetTemplateByID(dummies.CSMachine1.Spec.Template.ID, executableFilter).Return(&cloudstack.Template{Name: templateName}, 1, nil) + sos.EXPECT().GetServiceOfferingByID(dummies.CSMachine1.Spec.Offering.ID, gomock.Any()).Return(&cloudstack.ServiceOffering{Name: offeringName}, 1, nil) + ts.EXPECT().GetTemplateByID(dummies.CSMachine1.Spec.Template.ID, executableFilter, gomock.Any()).Return(&cloudstack.Template{Name: templateName}, 1, nil) dos.EXPECT().GetDiskOfferingID(dummies.CSMachine1.Spec.DiskOffering.Name, gomock.Any()).Return(diskOfferingFakeID+"-not-match", 1, nil) requiredRegexp := "diskOffering ID %s does not match ID %s returned using name %s" Ω(client.GetOrCreateVMInstance( @@ -674,13 +812,13 @@ var _ = Describe("Instance", func() { dummies.CSMachine1.Spec.UncompressedUserData = ptr.To(true) vms.EXPECT(). - GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID). + GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()). Return(nil, -1, notFoundError) vms.EXPECT(). - GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID). + GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()). Return(&cloudstack.VirtualMachinesMetric{}, 1, nil) vms.EXPECT(). - GetVirtualMachinesMetricByName(dummies.CSMachine1.Name). + GetVirtualMachinesMetricByName(dummies.CSMachine1.Name, gomock.Any()). Return(nil, -1, notFoundError) sos.EXPECT(). @@ -694,7 +832,7 @@ var _ = Describe("Instance", func() { GetDiskOfferingID(dummies.CSMachine1.Spec.DiskOffering.Name, gomock.Any()). Return(diskOfferingFakeID, 1, nil) dos.EXPECT(). - GetDiskOfferingByID(dummies.CSMachine1.Spec.DiskOffering.ID). + GetDiskOfferingByID(dummies.CSMachine1.Spec.DiskOffering.ID, gomock.Any()). Return(&cloudstack.DiskOffering{Iscustomized: false}, 1, nil) ts.EXPECT(). GetTemplateID(dummies.CSMachine1.Spec.Template.Name, executableFilter, dummies.Zone1.ID, gomock.Any()). @@ -782,8 +920,8 @@ var _ = Describe("Instance", func() { vms.EXPECT().DestroyVirtualMachine(expungeDestroyParams).Return(nil, nil) vs.EXPECT().NewListVolumesParams().Return(listVolumesParams) vs.EXPECT().ListVolumes(listVolumesParams).Return(listVolumesResponse, nil) - vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID).Return(nil, -1, notFoundError) - vms.EXPECT().GetVirtualMachinesMetricByName(dummies.CSMachine1.Name).Return(nil, -1, notFoundError) + vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()).Return(nil, -1, notFoundError) + vms.EXPECT().GetVirtualMachinesMetricByName(dummies.CSMachine1.Name, gomock.Any()).Return(nil, -1, notFoundError) Ω(client.DestroyVMInstance(dummies.CSMachine1)). Should(Succeed()) }) @@ -797,7 +935,7 @@ var _ = Describe("Instance", func() { vms.EXPECT().DestroyVirtualMachine(expungeDestroyParams).Return(nil, nil) vs.EXPECT().NewListVolumesParams().Return(listVolumesParams) vs.EXPECT().ListVolumes(listVolumesParams).Return(listVolumesResponse, nil) - vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID). + vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()). Return(&cloudstack.VirtualMachinesMetric{ State: "Expunging", }, 1, nil) @@ -814,7 +952,7 @@ var _ = Describe("Instance", func() { vms.EXPECT().DestroyVirtualMachine(expungeDestroyParams).Return(nil, nil) vs.EXPECT().NewListVolumesParams().Return(listVolumesParams) vs.EXPECT().ListVolumes(listVolumesParams).Return(listVolumesResponse, nil) - vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID). + vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()). Return(&cloudstack.VirtualMachinesMetric{ State: "Expunged", }, 1, nil) @@ -831,7 +969,7 @@ var _ = Describe("Instance", func() { vms.EXPECT().DestroyVirtualMachine(expungeDestroyParams).Return(nil, nil) vs.EXPECT().NewListVolumesParams().Return(listVolumesParams) vs.EXPECT().ListVolumes(listVolumesParams).Return(listVolumesResponse, nil) - vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID). + vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()). Return(&cloudstack.VirtualMachinesMetric{ State: "Stopping", }, 1, nil) @@ -859,7 +997,7 @@ var _ = Describe("Instance", func() { }) vs.EXPECT().NewListVolumesParams().Times(0) vs.EXPECT().ListVolumes(gomock.Any()).Times(0) - vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID). + vms.EXPECT().GetVirtualMachinesMetricByID(*dummies.CSMachine1.Spec.InstanceID, gomock.Any()). Return(&cloudstack.VirtualMachinesMetric{ State: "Expunged", }, 1, nil) diff --git a/pkg/cloud/isolated_network.go b/pkg/cloud/isolated_network.go index ed7db51d..586ad6b9 100644 --- a/pkg/cloud/isolated_network.go +++ b/pkg/cloud/isolated_network.go @@ -89,6 +89,7 @@ func (c *client) AssociatePublicIPAddress( p := c.cs.Address.NewAssociateIpAddressParams() p.SetIpaddress(publicAddress.Ipaddress) p.SetNetworkid(isoNet.Spec.ID) + setIfNotEmpty(c.user.Project.ID, p.SetProjectid) if _, err := c.cs.Address.AssociateIpAddress(p); err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) @@ -113,9 +114,8 @@ func (c *client) CreateIsolatedNetwork(fd *infrav1.CloudStackFailureDomain, isoN // Do isolated network creation. p := c.cs.Network.NewCreateNetworkParams(isoNet.Spec.Name, offeringID, fd.Spec.Zone.ID) p.SetDisplaytext(isoNet.Spec.Name) - if isoNet.Spec.Domain != "" { - p.SetNetworkdomain(isoNet.Spec.Domain) - } + setIfNotEmpty(c.user.Project.ID, p.SetProjectid) + setIfNotEmpty(isoNet.Spec.Domain, p.SetNetworkdomain) if isoNet.Spec.CIDR != "" { m, err := parseCIDR(isoNet.Spec.CIDR) if err != nil { @@ -174,6 +174,7 @@ func (c *client) GetPublicIP( p.SetAllocatedonly(false) p.SetZoneid(fd.Spec.Zone.ID) setIfNotEmpty(desiredIP, p.SetIpaddress) + setIfNotEmpty(c.user.Project.ID, p.SetProjectid) publicAddresses, err := c.cs.Address.ListPublicIpAddresses(p) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) @@ -199,7 +200,7 @@ func (c *client) GetPublicIP( // GetIsolatedNetwork gets an isolated network in the relevant Zone. func (c *client) GetIsolatedNetwork(isoNet *infrav1.CloudStackIsolatedNetwork) (retErr error) { - netDetails, count, err := c.cs.Network.GetNetworkByName(isoNet.Spec.Name) + netDetails, count, err := c.cs.Network.GetNetworkByName(isoNet.Spec.Name, cloudstack.WithProject(c.user.Project.ID)) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) retErr = multierror.Append(retErr, errors.Wrapf(err, "could not get Network ID from %s", isoNet.Spec.Name)) @@ -213,7 +214,7 @@ func (c *client) GetIsolatedNetwork(isoNet *infrav1.CloudStackIsolatedNetwork) ( return nil } - netDetails, count, err = c.cs.Network.GetNetworkByID(isoNet.Spec.ID) + netDetails, count, err = c.cs.Network.GetNetworkByID(isoNet.Spec.ID, cloudstack.WithProject(c.user.Project.ID)) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) @@ -231,6 +232,7 @@ func (c *client) GetIsolatedNetwork(isoNet *infrav1.CloudStackIsolatedNetwork) ( func (c *client) GetLoadBalancerRules(isoNet *infrav1.CloudStackIsolatedNetwork) ([]*cloudstack.LoadBalancerRule, error) { p := c.cs.LoadBalancer.NewListLoadBalancerRulesParams() p.SetPublicipid(isoNet.Status.APIServerLoadBalancer.IPAddressID) + setIfNotEmpty(c.user.Project.ID, p.SetProjectid) loadBalancerRules, err := c.cs.LoadBalancer.ListLoadBalancerRules(p) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) @@ -440,6 +442,7 @@ func (c *client) GetFirewallRules(isoNet *infrav1.CloudStackIsolatedNetwork) ([] p := c.cs.Firewall.NewListFirewallRulesParams() p.SetIpaddressid(isoNet.Status.APIServerLoadBalancer.IPAddressID) p.SetNetworkid(isoNet.Spec.ID) + setIfNotEmpty(c.user.Project.ID, p.SetProjectid) fwRules, err := c.cs.Firewall.ListFirewallRules(p) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) @@ -883,7 +886,7 @@ func (c *client) DisassociatePublicIPAddressIfNotInUse(ipAddressID string) (bool } if tagsAllowDisposal, err := c.DoClusterTagsAllowDisposal(ResourceTypeIPAddress, ipAddressID); err != nil { return false, err - } else if publicIP, _, err := c.cs.Address.GetPublicIpAddressByID(ipAddressID); err != nil { + } else if publicIP, _, err := c.cs.Address.GetPublicIpAddressByID(ipAddressID, cloudstack.WithProject(c.user.Project.ID)); err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) return false, err diff --git a/pkg/cloud/isolated_network_test.go b/pkg/cloud/isolated_network_test.go index 7d089b64..0eeed2fd 100644 --- a/pkg/cloud/isolated_network_test.go +++ b/pkg/cloud/isolated_network_test.go @@ -75,8 +75,8 @@ var _ = Describe("Network", func() { nos.EXPECT().GetNetworkOfferingID(gomock.Any()).Return("someOfferingID", 1, nil) ns.EXPECT().NewCreateNetworkParams(gomock.Any(), gomock.Any(), gomock.Any()). Return(&csapi.CreateNetworkParams{}) - ns.EXPECT().GetNetworkByName(dummies.ISONet1.Name).Return(nil, 0, nil) - ns.EXPECT().GetNetworkByID(dummies.ISONet1.ID).Return(nil, 0, nil) + ns.EXPECT().GetNetworkByName(dummies.ISONet1.Name, gomock.Any()).Return(nil, 0, nil) + ns.EXPECT().GetNetworkByID(dummies.ISONet1.ID, gomock.Any()).Return(nil, 0, nil) ns.EXPECT().CreateNetwork(gomock.Any()).Return(&csapi.CreateNetworkResponse{Id: dummies.ISONet1.ID}, nil) fs.EXPECT().NewCreateEgressFirewallRuleParams(dummies.ISONet1.ID, gomock.Any()). @@ -111,7 +111,7 @@ var _ = Describe("Network", func() { It("resolves the existing isolated network", func() { dummies.SetClusterSpecToNet(&dummies.ISONet1) - ns.EXPECT().GetNetworkByName(dummies.ISONet1.Name).Return(dummies.CAPCNetToCSAPINet(&dummies.ISONet1), 1, nil) + ns.EXPECT().GetNetworkByName(dummies.ISONet1.Name, gomock.Any()).Return(dummies.CAPCNetToCSAPINet(&dummies.ISONet1), 1, nil) fs.EXPECT().NewCreateEgressFirewallRuleParams(dummies.ISONet1.ID, gomock.Any()). DoAndReturn(func(_ string, protocol string) *csapi.CreateEgressFirewallRuleParams { @@ -138,8 +138,8 @@ var _ = Describe("Network", func() { }) It("fails to get network offering from CloudStack", func() { - ns.EXPECT().GetNetworkByName(dummies.ISONet1.Name).Return(nil, 0, nil) - ns.EXPECT().GetNetworkByID(dummies.ISONet1.ID).Return(nil, 0, nil) + ns.EXPECT().GetNetworkByName(dummies.ISONet1.Name, gomock.Any()).Return(nil, 0, nil) + ns.EXPECT().GetNetworkByID(dummies.ISONet1.ID, gomock.Any()).Return(nil, 0, nil) nos.EXPECT().GetNetworkOfferingID(gomock.Any()).Return("", -1, fakeError) err := client.GetOrCreateIsolatedNetwork(dummies.CSFailureDomain1, dummies.CSISONet1) @@ -378,7 +378,7 @@ var _ = Describe("Network", func() { }) fs.EXPECT().DeleteFirewallRule(gomock.Any()).Return(&csapi.DeleteFirewallRuleResponse{Success: true}, nil).Times(1) - as.EXPECT().GetPublicIpAddressByID(dummies.CSISONet1.Status.APIServerLoadBalancer.IPAddressID).Return(&csapi.PublicIpAddress{}, 1, nil) + as.EXPECT().GetPublicIpAddressByID(dummies.CSISONet1.Status.APIServerLoadBalancer.IPAddressID, gomock.Any()).Return(&csapi.PublicIpAddress{}, 1, nil) rtdp := &csapi.DeleteTagsParams{} rs.EXPECT().NewDeleteTagsParams(gomock.Any(), gomock.Any()).Return(rtdp) @@ -1027,7 +1027,7 @@ var _ = Describe("Network", func() { rtlp := &csapi.ListTagsParams{} rs.EXPECT().NewListTagsParams().Return(rtlp).Times(4) rs.EXPECT().ListTags(rtlp).Return(&csapi.ListTagsResponse{}, nil).Times(4) - as.EXPECT().GetPublicIpAddressByID(dummies.CSISONet1.Status.PublicIPID).Return(&csapi.PublicIpAddress{}, 1, nil) + as.EXPECT().GetPublicIpAddressByID(dummies.CSISONet1.Status.PublicIPID, gomock.Any()).Return(&csapi.PublicIpAddress{}, 1, nil) Ω(client.DisposeIsoNetResources(dummies.CSISONet1, dummies.CSCluster)).Should(Succeed()) }) @@ -1043,7 +1043,7 @@ var _ = Describe("Network", func() { rs.EXPECT().NewListTagsParams().Return(rtlp).Times(4) rs.EXPECT().ListTags(rtlp).Return(createdByCAPCResponse, nil).Times(3) rs.EXPECT().ListTags(rtlp).Return(&csapi.ListTagsResponse{}, nil).Times(1) - as.EXPECT().GetPublicIpAddressByID(dummies.CSISONet1.Status.PublicIPID).Return(&csapi.PublicIpAddress{}, 1, nil) + as.EXPECT().GetPublicIpAddressByID(dummies.CSISONet1.Status.PublicIPID, gomock.Any()).Return(&csapi.PublicIpAddress{}, 1, nil) as.EXPECT().NewDisassociateIpAddressParams(dummies.CSISONet1.Status.PublicIPID).Return(dap) as.EXPECT().DisassociateIpAddress(dap).Return(&csapi.DisassociateIpAddressResponse{}, nil) @@ -1060,7 +1060,7 @@ var _ = Describe("Network", func() { rs.EXPECT().DeleteTags(rtdp).Return(&csapi.DeleteTagsResponse{}, nil).Times(2) rs.EXPECT().NewListTagsParams().Return(rtlp).Times(2) rs.EXPECT().ListTags(rtlp).Return(createdByCAPCResponse, nil).Times(2) - as.EXPECT().GetPublicIpAddressByID(dummies.CSISONet1.Status.PublicIPID).Return(&csapi.PublicIpAddress{}, 1, nil) + as.EXPECT().GetPublicIpAddressByID(dummies.CSISONet1.Status.PublicIPID, gomock.Any()).Return(&csapi.PublicIpAddress{}, 1, nil) as.EXPECT().NewDisassociateIpAddressParams(dummies.CSISONet1.Status.PublicIPID).Return(dap) as.EXPECT().DisassociateIpAddress(dap).Return(nil, fakeError) diff --git a/pkg/cloud/network.go b/pkg/cloud/network.go index dc09258c..de31f121 100644 --- a/pkg/cloud/network.go +++ b/pkg/cloud/network.go @@ -17,6 +17,7 @@ limitations under the License. package cloud import ( + "github.com/apache/cloudstack-go/v2/cloudstack" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" @@ -42,7 +43,7 @@ func (c *client) ResolveNetwork(net *infrav1.Network) (retErr error) { // TODO rebuild this to consider cases with networks in many zones. // Use ListNetworks instead. netName := net.Name - netDetails, count, err := c.cs.Network.GetNetworkByName(netName) + netDetails, count, err := c.cs.Network.GetNetworkByName(netName, cloudstack.WithProject(c.user.Project.ID)) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) retErr = multierror.Append(retErr, errors.Wrapf(err, "could not get Network ID from %s", netName)) @@ -59,7 +60,7 @@ func (c *client) ResolveNetwork(net *infrav1.Network) (retErr error) { } // Now get network details. - netDetails, count, err = c.cs.Network.GetNetworkByID(net.ID) + netDetails, count, err = c.cs.Network.GetNetworkByID(net.ID, cloudstack.WithProject(c.user.Project.ID)) if err != nil { return multierror.Append(retErr, errors.Wrapf(err, "could not get Network by ID %s", net.ID)) } else if count != 1 { diff --git a/pkg/cloud/network_test.go b/pkg/cloud/network_test.go index 79f04610..9135dfdd 100644 --- a/pkg/cloud/network_test.go +++ b/pkg/cloud/network_test.go @@ -54,21 +54,21 @@ var _ = Describe("Network", func() { Context("for an existing network", func() { It("resolves network by ID", func() { - ns.EXPECT().GetNetworkByName(dummies.ISONet1.Name).Return(nil, 0, nil) - ns.EXPECT().GetNetworkByID(dummies.ISONet1.ID).Return(dummies.CAPCNetToCSAPINet(&dummies.ISONet1), 1, nil) + ns.EXPECT().GetNetworkByName(dummies.ISONet1.Name, gomock.Any()).Return(nil, 0, nil) + ns.EXPECT().GetNetworkByID(dummies.ISONet1.ID, gomock.Any()).Return(dummies.CAPCNetToCSAPINet(&dummies.ISONet1), 1, nil) Ω(client.ResolveNetwork(&dummies.ISONet1)).Should(Succeed()) }) It("resolves network by Name", func() { - ns.EXPECT().GetNetworkByName(dummies.ISONet1.Name).Return(dummies.CAPCNetToCSAPINet(&dummies.ISONet1), 1, nil) + ns.EXPECT().GetNetworkByName(dummies.ISONet1.Name, gomock.Any()).Return(dummies.CAPCNetToCSAPINet(&dummies.ISONet1), 1, nil) Ω(client.ResolveNetwork(&dummies.ISONet1)).Should(Succeed()) }) It("When there exists more than one network with the same name", func() { - ns.EXPECT().GetNetworkByName(dummies.ISONet1.Name).Return(dummies.CAPCNetToCSAPINet(&dummies.ISONet1), 2, nil) - ns.EXPECT().GetNetworkByID(dummies.ISONet1.ID).Return(nil, 2, errors.New("There is more then one result for Network UUID")) + ns.EXPECT().GetNetworkByName(dummies.ISONet1.Name, gomock.Any()).Return(dummies.CAPCNetToCSAPINet(&dummies.ISONet1), 2, nil) + ns.EXPECT().GetNetworkByID(dummies.ISONet1.ID, gomock.Any()).Return(nil, 2, errors.New("There is more then one result for Network UUID")) err := client.ResolveNetwork(&dummies.ISONet1) Ω(err).ShouldNot(Succeed()) Ω(err.Error()).Should(ContainSubstring(fmt.Sprintf("expected 1 Network with name %s, but got %d", dummies.ISONet1.Name, 2))) diff --git a/pkg/cloud/tags.go b/pkg/cloud/tags.go index c569a882..0515c523 100644 --- a/pkg/cloud/tags.go +++ b/pkg/cloud/tags.go @@ -139,6 +139,7 @@ func (c *client) GetTags(rType ResourceType, rID string) (map[string]string, err p.SetResourceid(rID) p.SetResourcetype(string(rType)) p.SetListall(true) + setIfNotEmpty(c.user.Project.ID, p.SetProjectid) listTagResponse, err := c.cs.Resourcetags.ListTags(p) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) diff --git a/pkg/cloud/user_credentials.go b/pkg/cloud/user_credentials.go index 9acd2818..8c5d64c5 100644 --- a/pkg/cloud/user_credentials.go +++ b/pkg/cloud/user_credentials.go @@ -55,6 +55,15 @@ type Account struct { VMAvailable string } +// Project contains specifications that identify a project. +type Project struct { + Name string + ID string + CPUAvailable string + MemoryAvailable string + VMAvailable string +} + // User contains information uniquely identifying and scoping a user. type User struct { ID string @@ -62,6 +71,7 @@ type User struct { APIKey string SecretKey string Account + Project } // ResolveDomain resolves a domain's information. @@ -174,6 +184,37 @@ func (c *client) ResolveAccount(account *Account) error { return nil } +// ResolveProject resolves a project's information. +func (c *client) ResolveProject(user *User) error { + if user.Project.ID == "" { + return nil + } + + p := c.cs.Project.NewListProjectsParams() + p.SetListall(true) + p.SetDomainid(c.user.Domain.ID) + p.SetAccount(c.user.Account.Name) + p.SetId(user.Project.ID) + resp, err := c.cs.Project.ListProjects(p) + if err != nil { + c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) + + return err + } else if resp.Count == 0 { + return errors.Errorf("could not find project with ID '%s'", user.Project.ID) + } else if resp.Count != 1 { + return errors.Errorf("expected 1 Project with ID '%s' in domain ID '%s', but got %d", + user.Project.ID, c.user.Domain.ID, resp.Count) + } + c.user.Project.ID = resp.Projects[0].Id + c.user.Project.Name = resp.Projects[0].Name + c.user.Project.CPUAvailable = resp.Projects[0].Cpuavailable + c.user.Project.MemoryAvailable = resp.Projects[0].Memoryavailable + c.user.Project.VMAvailable = resp.Projects[0].Vmavailable + + return nil +} + // ResolveUser resolves a user's information. func (c *client) ResolveUser(user *User) error { // Resolve account prior to any user resolution activity. @@ -229,6 +270,10 @@ func (c *client) GetUserWithKeys(user *User) (bool, error) { return false, errors.Wrapf(err, "resolving account %s details", user.Account.Name) } + if err := c.ResolveProject(user); err != nil { + return false, errors.Wrapf(err, "resolving project %s details", user.Project.Name) + } + // List users and take first user that has already has api keys. p := c.cs.User.NewListUsersParams() p.SetAccount(user.Account.Name) diff --git a/pkg/cloud/zone.go b/pkg/cloud/zone.go index 9147e207..f6f2cb5a 100644 --- a/pkg/cloud/zone.go +++ b/pkg/cloud/zone.go @@ -17,6 +17,7 @@ limitations under the License. package cloud import ( + "github.com/apache/cloudstack-go/v2/cloudstack" "github.com/hashicorp/go-multierror" "github.com/pkg/errors" @@ -57,7 +58,7 @@ func (c *client) ResolveZone(zSpec *infrav1.CloudStackZoneSpec) (retErr error) { // ResolveNetworkForZone fetches details on Zone's specified network. func (c *client) ResolveNetworkForZone(zSpec *infrav1.CloudStackZoneSpec) (retErr error) { netName := zSpec.Network.Name - netDetails, count, err := c.cs.Network.GetNetworkByName(netName) + netDetails, count, err := c.cs.Network.GetNetworkByName(netName, cloudstack.WithProject(c.user.Project.ID)) if err != nil { c.customMetrics.EvaluateErrorAndIncrementAcsReconciliationErrorCounter(err) retErr = multierror.Append(retErr, errors.Wrapf(err, "could not get Network ID from %v", netName)) @@ -72,7 +73,7 @@ func (c *client) ResolveNetworkForZone(zSpec *infrav1.CloudStackZoneSpec) (retEr } // Now get network details. - netDetails, count, err = c.cs.Network.GetNetworkByID(zSpec.Network.ID) + netDetails, count, err = c.cs.Network.GetNetworkByID(zSpec.Network.ID, cloudstack.WithProject(c.user.Project.ID)) if err != nil { return multierror.Append(retErr, errors.Wrapf(err, "could not get Network by ID %s", zSpec.Network.ID)) } else if count != 1 { diff --git a/pkg/cloud/zone_test.go b/pkg/cloud/zone_test.go index 4f91fd2f..2397e7f5 100644 --- a/pkg/cloud/zone_test.go +++ b/pkg/cloud/zone_test.go @@ -85,30 +85,30 @@ var _ = Describe("Zone", func() { }) Context("Resolve network for zone", func() { - It("get network by name specfied in zone spec", func() { - ns.EXPECT().GetNetworkByName(dummies.Zone1.Network.Name).Return(&csapi.Network{}, 1, nil) + It("get network by name specified in zone spec", func() { + ns.EXPECT().GetNetworkByName(dummies.Zone1.Network.Name, gomock.Any()).Return(&csapi.Network{}, 1, nil) Ω(client.ResolveNetworkForZone(&dummies.CSFailureDomain1.Spec.Zone)).Should(Succeed()) }) - It("get network by name specfied in zone spec returns > 1 network", func() { - ns.EXPECT().GetNetworkByName(dummies.Zone2.Network.Name).Return(&csapi.Network{}, 2, nil) - ns.EXPECT().GetNetworkByID(dummies.Zone2.Network.ID).Return(&csapi.Network{}, 2, nil) + It("get network by name specified in zone spec returns > 1 network", func() { + ns.EXPECT().GetNetworkByName(dummies.Zone2.Network.Name, gomock.Any()).Return(&csapi.Network{}, 2, nil) + ns.EXPECT().GetNetworkByID(dummies.Zone2.Network.ID, gomock.Any()).Return(&csapi.Network{}, 2, nil) Ω(client.ResolveNetworkForZone(&dummies.CSFailureDomain2.Spec.Zone)).Should(MatchError(And( ContainSubstring(fmt.Sprintf("expected 1 Network with name %s, but got %d", dummies.Zone2.Network.Name, 2)), ContainSubstring(fmt.Sprintf("expected 1 Network with UUID %v, but got %d", dummies.Zone2.Network.ID, 2))))) }) - It("get network by id specfied in zone spec", func() { - ns.EXPECT().GetNetworkByName(dummies.Zone2.Network.Name).Return(nil, -1, fakeError) - ns.EXPECT().GetNetworkByID(dummies.Zone2.Network.ID).Return(&csapi.Network{}, 1, nil) + It("get network by id specified in zone spec", func() { + ns.EXPECT().GetNetworkByName(dummies.Zone2.Network.Name, gomock.Any()).Return(nil, -1, fakeError) + ns.EXPECT().GetNetworkByID(dummies.Zone2.Network.ID, gomock.Any()).Return(&csapi.Network{}, 1, nil) Ω(client.ResolveNetworkForZone(&dummies.CSFailureDomain2.Spec.Zone)).Should(Succeed()) }) It("get network by id fails", func() { - ns.EXPECT().GetNetworkByName(dummies.Zone2.Network.Name).Return(nil, -1, fakeError) - ns.EXPECT().GetNetworkByID(dummies.Zone2.Network.ID).Return(nil, -1, fakeError) + ns.EXPECT().GetNetworkByName(dummies.Zone2.Network.Name, gomock.Any()).Return(nil, -1, fakeError) + ns.EXPECT().GetNetworkByID(dummies.Zone2.Network.ID, gomock.Any()).Return(nil, -1, fakeError) Ω(client.ResolveNetworkForZone(&dummies.CSFailureDomain2.Spec.Zone).Error()).Should(ContainSubstring("could not get Network by ID " + dummies.Zone2.Network.ID)) }) diff --git a/test/e2e/common.go b/test/e2e/common.go index 6586827a..4e4500ef 100644 --- a/test/e2e/common.go +++ b/test/e2e/common.go @@ -280,12 +280,16 @@ func DestroyOneMachine(client *cloudstack.CloudStackClient, clusterName string, } func CheckAffinityGroupsDeleted(client *cloudstack.CloudStackClient, affinityIds []string) error { + return CheckAffinityGroupsDeletedInProject(client, affinityIds, "") +} + +func CheckAffinityGroupsDeletedInProject(client *cloudstack.CloudStackClient, affinityIds []string, project string) error { if len(affinityIds) == 0 { return errors.New("affinityIds are empty") } for _, affinityId := range affinityIds { - affinity, count, _ := client.AffinityGroup.GetAffinityGroupByID(affinityId) + affinity, count, _ := client.AffinityGroup.GetAffinityGroupByID(affinityId, cloudstack.WithProject(project)) if count > 0 { return errors.New("Affinity group " + affinity.Name + " still exists") } @@ -312,9 +316,21 @@ func GetHostCount(client *cloudstack.CloudStackClient, zoneName string) int { } func CheckAffinityGroup(client *cloudstack.CloudStackClient, clusterName string, affinityType string) []string { + return CheckAffinityGroupInProject(client, clusterName, affinityType, "") +} + +func CheckAffinityGroupInProject(client *cloudstack.CloudStackClient, clusterName string, affinityType string, project string) []string { By("Listing all machines") p := client.VirtualMachine.NewListVirtualMachinesParams() p.SetListall(true) + + if project != "" { + projectID, _, err := client.Project.GetProjectID(project) + if err != nil { + Fail("Failed to get project: " + err.Error()) + } + p.SetProjectid(projectID) + } listResp, err := client.VirtualMachine.ListVirtualMachines(p) if err != nil { Fail("Failed to list machines: " + err.Error()) @@ -334,7 +350,7 @@ func CheckAffinityGroup(client *cloudstack.CloudStackClient, clusterName string, for _, affinity := range vm.Affinitygroup { affinityIds = append(affinityIds, affinity.Id) - affinity, _, _ := client.AffinityGroup.GetAffinityGroupByID(affinity.Id) + affinity, _, _ := client.AffinityGroup.GetAffinityGroupByID(affinity.Id, cloudstack.WithProject(project)) if err != nil { Fail("Failed to get affinity group for " + affinity.Id + " : " + err.Error()) } diff --git a/test/e2e/config/cloudstack.yaml b/test/e2e/config/cloudstack.yaml index 2d06d996..79054d55 100644 --- a/test/e2e/config/cloudstack.yaml +++ b/test/e2e/config/cloudstack.yaml @@ -94,6 +94,7 @@ providers: - sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-disk-offering.yaml" - sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-custom-disk-offering.yaml" - sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-subdomain.yaml" + - sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-project.yaml" - sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-invalid-ip.yaml" - sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-kubernetes-version-upgrade-before.yaml" - sourcePath: "../data/infrastructure-cloudstack/v1beta3/cluster-template-kubernetes-version-upgrade-after.yaml" @@ -122,6 +123,7 @@ variables: CLOUDSTACK_INVALID_ZONE_NAME: zoneXXXX CLOUDSTACK_INVALID_NETWORK_NAME: networkXXXX CLOUDSTACK_ACCOUNT_NAME: admin + CLOUDSTACK_PROJECT_NAME: capc-e2e-test CLOUDSTACK_INVALID_ACCOUNT_NAME: accountXXXX CLOUDSTACK_DOMAIN_NAME: ROOT CLOUDSTACK_INVALID_DOMAIN_NAME: domainXXXX diff --git a/test/fakes/fakes.infrastructure.cluster.x-k8s.io_cloudstackmachineowners.yaml b/test/fakes/fakes.infrastructure.cluster.x-k8s.io_cloudstackmachineowners.yaml index f8a7c46e..8d85b4f5 100644 --- a/test/fakes/fakes.infrastructure.cluster.x-k8s.io_cloudstackmachineowners.yaml +++ b/test/fakes/fakes.infrastructure.cluster.x-k8s.io_cloudstackmachineowners.yaml @@ -3,7 +3,7 @@ apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition metadata: annotations: - controller-gen.kubebuilder.io/version: v0.14.0 + controller-gen.kubebuilder.io/version: v0.15.0 name: cloudstackmachineowners.fakes.infrastructure.cluster.x-k8s.io spec: group: fakes.infrastructure.cluster.x-k8s.io