diff --git a/PROJECT b/PROJECT index 85521c1cf3d..ccfcfeaf51d 100644 --- a/PROJECT +++ b/PROJECT @@ -92,3 +92,9 @@ resources: - group: azure version: v1alpha1 kind: AzureVirtualMachine +- group: azure + version: v1alpha1 + kind: AzureLoadBalancer +- group: azure + version: v1alpha1 + kind: AzureVMScaleSet diff --git a/api/v1alpha1/azureloadbalancer_types.go b/api/v1alpha1/azureloadbalancer_types.go new file mode 100644 index 00000000000..d30872091db --- /dev/null +++ b/api/v1alpha1/azureloadbalancer_types.go @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// AzureLoadBalancerSpec defines the desired state of AzureLoadBalancer +type AzureLoadBalancerSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + Location string `json:"location"` + ResourceGroup string `json:"resourceGroup"` + PublicIPAddressName string `json:"publicIPAddressName"` + BackendAddressPoolName string `json:"backendAddressPoolName"` + InboundNatPoolName string `json:"inboundNatPoolName"` + FrontendPortRangeStart int `json:"frontendPortRangeStart"` + FrontendPortRangeEnd int `json:"frontendPortRangeEnd"` + BackendPort int `json:"backendPort"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// AzureLoadBalancer is the Schema for the azureloadbalancers API +// +kubebuilder:printcolumn:name="Provisioned",type="string",JSONPath=".status.provisioned" +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.message" +type AzureLoadBalancer struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AzureLoadBalancerSpec `json:"spec,omitempty"` + Status ASOStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// AzureLoadBalancerList contains a list of AzureLoadBalancer +type AzureLoadBalancerList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AzureLoadBalancer `json:"items"` +} + +func init() { + SchemeBuilder.Register(&AzureLoadBalancer{}, &AzureLoadBalancerList{}) +} diff --git a/api/v1alpha1/azureloadbalancer_types_test.go b/api/v1alpha1/azureloadbalancer_types_test.go new file mode 100644 index 00000000000..dcd5e1c5e01 --- /dev/null +++ b/api/v1alpha1/azureloadbalancer_types_test.go @@ -0,0 +1,74 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package v1alpha1 + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "golang.org/x/net/context" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +// These tests are written in BDD-style using Ginkgo framework. Refer to +// http://onsi.github.io/ginkgo to learn more. + +var _ = Describe("AzureLoadBalancer", func() { + var ( + key types.NamespacedName + created, fetched *AzureLoadBalancer + ) + + BeforeEach(func() { + // Add any setup steps that needs to be executed before each test + }) + + AfterEach(func() { + // Add any teardown steps that needs to be executed after each test + }) + + // Add Tests for OpenAPI validation (or additonal CRD features) specified in + // your API definition. + // Avoid adding tests for vanilla CRUD operations because they would + // test Kubernetes API server, which isn't the goal here. + Context("Create API", func() { + + It("should create an object successfully", func() { + + key = types.NamespacedName{ + Name: "foo", + Namespace: "default", + } + created = &AzureLoadBalancer{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "default", + }, + Spec: AzureLoadBalancerSpec{ + Location: "westus", + ResourceGroup: "testlb", + PublicIPAddressName: "test", + BackendAddressPoolName: "test", + InboundNatPoolName: "test", + FrontendPortRangeStart: 1000, + FrontendPortRangeEnd: 2000, + BackendPort: 3000, + }} + + By("creating an API obj") + Expect(k8sClient.Create(context.TODO(), created)).To(Succeed()) + + fetched = &AzureLoadBalancer{} + Expect(k8sClient.Get(context.TODO(), key, fetched)).To(Succeed()) + Expect(fetched).To(Equal(created)) + + By("deleting the created object") + Expect(k8sClient.Delete(context.TODO(), created)).To(Succeed()) + Expect(k8sClient.Get(context.TODO(), key, created)).ToNot(Succeed()) + }) + + }) + +}) diff --git a/api/v1alpha1/azurevmscaleset_types.go b/api/v1alpha1/azurevmscaleset_types.go new file mode 100644 index 00000000000..e4572f91a4b --- /dev/null +++ b/api/v1alpha1/azurevmscaleset_types.go @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +// AzureVMScaleSetSpec defines the desired state of AzureVMScaleSet +type AzureVMScaleSetSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + Location string `json:"location"` + ResourceGroup string `json:"resourceGroup"` + VMSize string `json:"vmSize"` + Capacity int `json:"capacity"` + OSType OSType `json:"osType"` + AdminUserName string `json:"adminUserName"` + SSHPublicKeyData string `json:"sshPublicKeyData,omitempty"` + PlatformImageURN string `json:"platformImageURN"` + VirtualNetworkName string `json:"virtualNetworkName"` + SubnetName string `json:"subnetName"` + LoadBalancerName string `json:"loadBalancerName"` + BackendAddressPoolName string `json:"backendAddressPoolName"` + InboundNatPoolName string `json:"inboundNatPoolName"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// AzureVMScaleSet is the Schema for the azurevmscalesets API +// +kubebuilder:printcolumn:name="Provisioned",type="string",JSONPath=".status.provisioned" +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.message" +type AzureVMScaleSet struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AzureVMScaleSetSpec `json:"spec,omitempty"` + Status ASOStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// AzureVMScaleSetList contains a list of AzureVMScaleSet +type AzureVMScaleSetList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AzureVMScaleSet `json:"items"` +} + +func init() { + SchemeBuilder.Register(&AzureVMScaleSet{}, &AzureVMScaleSetList{}) +} diff --git a/api/v1alpha1/azurevmscaleset_types_test.go b/api/v1alpha1/azurevmscaleset_types_test.go new file mode 100644 index 00000000000..ae7ac4d89ff --- /dev/null +++ b/api/v1alpha1/azurevmscaleset_types_test.go @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package v1alpha1 + +import ( + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + "golang.org/x/net/context" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" +) + +// These tests are written in BDD-style using Ginkgo framework. Refer to +// http://onsi.github.io/ginkgo to learn more. + +var _ = Describe("AzureVMScaleSet", func() { + var ( + key types.NamespacedName + created, fetched *AzureVMScaleSet + ) + + BeforeEach(func() { + // Add any setup steps that needs to be executed before each test + }) + + AfterEach(func() { + // Add any teardown steps that needs to be executed after each test + }) + + // Add Tests for OpenAPI validation (or additonal CRD features) specified in + // your API definition. + // Avoid adding tests for vanilla CRUD operations because they would + // test Kubernetes API server, which isn't the goal here. + Context("Create API", func() { + + It("should create an object successfully", func() { + + key = types.NamespacedName{ + Name: "foo", + Namespace: "default", + } + created = &AzureVMScaleSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo", + Namespace: "default", + }, + Spec: AzureVMScaleSetSpec{ + Location: "westus", + ResourceGroup: "foo-vm", + VMSize: "test", + Capacity: 2, + OSType: OSType("Linux"), + AdminUserName: "test", + SSHPublicKeyData: "test", + PlatformImageURN: "w:x:y:z", + VirtualNetworkName: "test", + SubnetName: "test", + LoadBalancerName: "test", + BackendAddressPoolName: "test", + InboundNatPoolName: "test", + }} + + By("creating an API obj") + Expect(k8sClient.Create(context.TODO(), created)).To(Succeed()) + + fetched = &AzureVMScaleSet{} + Expect(k8sClient.Get(context.TODO(), key, fetched)).To(Succeed()) + Expect(fetched).To(Equal(created)) + + By("deleting the created object") + Expect(k8sClient.Delete(context.TODO(), created)).To(Succeed()) + Expect(k8sClient.Get(context.TODO(), key, created)).ToNot(Succeed()) + }) + + }) + +}) diff --git a/api/v1alpha1/postgresqlserver_types.go b/api/v1alpha1/postgresqlserver_types.go index 9dd64dd1b83..8161bae34e2 100644 --- a/api/v1alpha1/postgresqlserver_types.go +++ b/api/v1alpha1/postgresqlserver_types.go @@ -18,6 +18,8 @@ type PostgreSQLServerSpec struct { ServerVersion ServerVersion `json:"serverVersion,omitempty"` SSLEnforcement SslEnforcementEnum `json:"sslEnforcement,omitempty"` KeyVaultToStoreSecrets string `json:"keyVaultToStoreSecrets,omitempty"` + CreateMode string `json:"createMode,omitempty"` + ReplicaProperties ReplicaProperties `json:"replicaProperties,omitempty"` } type AzureDBsSQLSku struct { diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 5923dfd3bb8..0798741de63 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -35,6 +35,8 @@ resources: - bases/azure.microsoft.com_azurenetworkinterfaces.yaml - bases/azure.microsoft.com_mysqlvnetrules.yaml - bases/azure.microsoft.com_azurevirtualmachines.yaml +- bases/azure.microsoft.com_azureloadbalancers.yaml +- bases/azure.microsoft.com_azurevmscalesets.yaml # +kubebuilder:scaffold:crdkustomizeresource #patches: @@ -69,6 +71,8 @@ resources: #- patches/webhook_in_azurenetworkinterfaces.yaml #- patches/webhook_in_mysqlvnetrules.yaml #- patches/webhook_in_azurevirtualmachines.yaml +#- patches/webhook_in_azureloadbalancers.yaml +#- patches/webhook_in_azurevmscalesets.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch # [CAINJECTION] patches here are for enabling the CA injection for each CRD @@ -102,6 +106,8 @@ resources: #- patches/cainjection_in_azurenetworkinterfaces.yaml #- patches/cainjection_in_mysqlvnetrules.yaml #- patches/cainjection_in_azurevirtualmachines.yaml +#- patches/cainjection_in_azureloadbalancers.yaml +#- patches/cainjection_in_azurevmscalesets.yaml # +kubebuilder:scaffold:crdkustomizecainjectionpatch # the following config is for teaching kustomize how to do kustomization for CRDs. diff --git a/config/crd/patches/cainjection_in_azureloadbalancers.yaml b/config/crd/patches/cainjection_in_azureloadbalancers.yaml new file mode 100644 index 00000000000..bd84c6d6109 --- /dev/null +++ b/config/crd/patches/cainjection_in_azureloadbalancers.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: azureloadbalancers.azure.microsoft.com diff --git a/config/crd/patches/cainjection_in_azurevmscalesets.yaml b/config/crd/patches/cainjection_in_azurevmscalesets.yaml new file mode 100644 index 00000000000..8bdc131fb03 --- /dev/null +++ b/config/crd/patches/cainjection_in_azurevmscalesets.yaml @@ -0,0 +1,8 @@ +# The following patch adds a directive for certmanager to inject CA into the CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + certmanager.k8s.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: azurevmscalesets.azure.microsoft.com diff --git a/config/crd/patches/webhook_in_azureloadbalancers.yaml b/config/crd/patches/webhook_in_azureloadbalancers.yaml new file mode 100644 index 00000000000..d9a53e1c152 --- /dev/null +++ b/config/crd/patches/webhook_in_azureloadbalancers.yaml @@ -0,0 +1,17 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: azureloadbalancers.azure.microsoft.com +spec: + conversion: + strategy: Webhook + webhookClientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/crd/patches/webhook_in_azurevmscalesets.yaml b/config/crd/patches/webhook_in_azurevmscalesets.yaml new file mode 100644 index 00000000000..6f579f03b5e --- /dev/null +++ b/config/crd/patches/webhook_in_azurevmscalesets.yaml @@ -0,0 +1,17 @@ +# The following patch enables conversion webhook for CRD +# CRD conversion requires k8s 1.13 or later. +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + name: azurevmscalesets.azure.microsoft.com +spec: + conversion: + strategy: Webhook + webhookClientConfig: + # this is "\n" used as a placeholder, otherwise it will be rejected by the apiserver for being blank, + # but we're going to set it later using the cert-manager (or potentially a patch if not using cert-manager) + caBundle: Cg== + service: + namespace: system + name: webhook-service + path: /convert diff --git a/config/samples/azure_v1alpha1_azureloadbalancer.yaml b/config/samples/azure_v1alpha1_azureloadbalancer.yaml new file mode 100644 index 00000000000..ea065e249a4 --- /dev/null +++ b/config/samples/azure_v1alpha1_azureloadbalancer.yaml @@ -0,0 +1,13 @@ +apiVersion: azure.microsoft.com/v1alpha1 +kind: AzureLoadBalancer +metadata: + name: lb-sample-01 +spec: + location: SouthCentralUS + resourceGroup: resourcegroup-azure-operators + publicIPAddressName: lb-pip-sample-01 + backendAddressPoolName: test + inboundNatPoolName: test + frontendPortRangeStart: 1001 + frontendPortRangeEnd: 2000 + backendPort: 3000 \ No newline at end of file diff --git a/config/samples/azure_v1alpha1_azurenetworkinterface.yaml b/config/samples/azure_v1alpha1_azurenetworkinterface.yaml index ddb7e4cc8f7..9b6742274b0 100644 --- a/config/samples/azure_v1alpha1_azurenetworkinterface.yaml +++ b/config/samples/azure_v1alpha1_azurenetworkinterface.yaml @@ -1,10 +1,10 @@ apiVersion: azure.microsoft.com/v1alpha1 kind: AzureNetworkInterface metadata: - name: azurenetworkinterface-sample18 + name: nic-sample-01 spec: - location: westus + location: SouthCentralUS resourceGroup: resourcegroup-azure-operators - vnetName: vnet-sample-hpf-1 - subnetName: test2 - publicIPAddressName: azurepublicipaddress-sample-7 \ No newline at end of file + vnetName: vnet-sample-01 + subnetName: test1 + publicIPAddressName: nic-pip-sample-01 \ No newline at end of file diff --git a/config/samples/azure_v1alpha1_azurepublicipaddress.yaml b/config/samples/azure_v1alpha1_azurepublicipaddress_loadbalancer.yaml similarity index 81% rename from config/samples/azure_v1alpha1_azurepublicipaddress.yaml rename to config/samples/azure_v1alpha1_azurepublicipaddress_loadbalancer.yaml index 6e0ff4f88df..1f2c1544919 100644 --- a/config/samples/azure_v1alpha1_azurepublicipaddress.yaml +++ b/config/samples/azure_v1alpha1_azurepublicipaddress_loadbalancer.yaml @@ -1,9 +1,9 @@ apiVersion: azure.microsoft.com/v1alpha1 kind: AzurePublicIPAddress metadata: - name: azurepublicipaddress-sample11 + name: lb-pip-sample-01 spec: - location: westus + location: SouthCentralUS resourceGroup: resourcegroup-azure-operators publicIPAllocationMethod: "Static" idleTimeoutInMinutes: 10 diff --git a/config/samples/azure_v1alpha1_azurepublicipaddress_nic.yaml b/config/samples/azure_v1alpha1_azurepublicipaddress_nic.yaml new file mode 100644 index 00000000000..f750ada6959 --- /dev/null +++ b/config/samples/azure_v1alpha1_azurepublicipaddress_nic.yaml @@ -0,0 +1,11 @@ +apiVersion: azure.microsoft.com/v1alpha1 +kind: AzurePublicIPAddress +metadata: + name: nic-pip-sample-01 +spec: + location: SouthCentralUS + resourceGroup: resourcegroup-azure-operators + publicIPAllocationMethod: "Static" + idleTimeoutInMinutes: 10 + publicIPAddressVersion: "IPv4" + skuName: "Basic" diff --git a/config/samples/azure_v1alpha1_azurevmscaleset.yaml b/config/samples/azure_v1alpha1_azurevmscaleset.yaml new file mode 100644 index 00000000000..88e8064cca6 --- /dev/null +++ b/config/samples/azure_v1alpha1_azurevmscaleset.yaml @@ -0,0 +1,19 @@ +apiVersion: azure.microsoft.com/v1alpha1 +kind: AzureVMScaleSet +metadata: + name: vmss-sample-01 +spec: + location: SouthCentralUS + resourceGroup: resourcegroup-azure-operators + vmSize: Standard_DS1_v2 + capacity: 3 + osType: Linux + adminUserName: azureuser7 + # SSH public key to be used with VMSS (eg cat ~/.ssh/id_rsa.pub) + sshPublicKeyData: "{ssh public key}" + platformImageURN: Canonical:UbuntuServer:16.04-LTS:latest + virtualNetworkName: vnet-sample-01 + subnetName: test1 + loadBalancerName: lb-sample-01 + backendAddressPoolName: test + inboundNatPoolName: test \ No newline at end of file diff --git a/config/samples/azure_v1alpha1_postgresqlserver.yaml b/config/samples/azure_v1alpha1_postgresqlserver.yaml index 47c6b8aebf4..08b04c75228 100644 --- a/config/samples/azure_v1alpha1_postgresqlserver.yaml +++ b/config/samples/azure_v1alpha1_postgresqlserver.yaml @@ -7,9 +7,10 @@ spec: resourceGroup: resourcegroup-azure-operators serverVersion: "10" sslEnforcement: Enabled + createMode: Default # Possible values include: Default, Replica, PointInTimeRestore (not implemented), GeoRestore (not implemented) sku: - name: GP_Gen5_4 # tier + family + cores eg. - B_Gen4_1, GP_Gen5_4 - tier: GeneralPurpose # possible values - 'Basic', 'GeneralPurpose', 'MemoryOptimized' + name: GP_Gen5_4 # Name - The name of the sku, typically, tier + family + cores, e.g. B_Gen4_1, GP_Gen5_8. + tier: GeneralPurpose # possible values - 'Basic', 'GeneralPurpose', 'MemoryOptimized' family: Gen5 size: "51200" capacity: 4 diff --git a/config/samples/azure_v1alpha1_postgresqlserver_replica.yaml b/config/samples/azure_v1alpha1_postgresqlserver_replica.yaml new file mode 100644 index 00000000000..b020afcb72c --- /dev/null +++ b/config/samples/azure_v1alpha1_postgresqlserver_replica.yaml @@ -0,0 +1,11 @@ +apiVersion: azure.microsoft.com/v1alpha1 +kind: PostgreSQLServer +metadata: + name: postgresqlserver-replica3 +spec: + location: eastus + resourceGroup: resourcegroup-azure-operators + createMode: Replica # Possible values include: Default, Replica, PointInTimeRestore (not implemented), GeoRestore (not implemented) + replicaProperties: + # sourceServer tier should be "GeneralPurpose" or "MemoryOptimized" tier for replica support + sourceServerId: /subscriptions/{subscription ID}/resourceGroups/resourcegroup-azure-operators/providers/Microsoft.DBforPostgreSQL/servers/postgresqlserver-sample diff --git a/config/samples/azure_v1alpha1_virtualnetwork.yaml b/config/samples/azure_v1alpha1_virtualnetwork.yaml index 875c5ef3c20..95a4e9a9d69 100644 --- a/config/samples/azure_v1alpha1_virtualnetwork.yaml +++ b/config/samples/azure_v1alpha1_virtualnetwork.yaml @@ -1,9 +1,9 @@ apiVersion: azure.microsoft.com/v1alpha1 kind: VirtualNetwork metadata: - name: virtualnetwork-sample + name: vnet-sample-01 spec: - location: westus + location: SouthCentralUS resourceGroup: resourcegroup-azure-operators addressSpace: "10.0.0.0/8" subnets: diff --git a/controllers/azureloadbalancer_controller.go b/controllers/azureloadbalancer_controller.go new file mode 100644 index 00000000000..1c33f590ed9 --- /dev/null +++ b/controllers/azureloadbalancer_controller.go @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package controllers + +import ( + ctrl "sigs.k8s.io/controller-runtime" + + azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" +) + +// AzureLoadBalancerReconciler reconciles a AzureLoadBalancer object +type AzureLoadBalancerReconciler struct { + Reconciler *AsyncReconciler +} + +// +kubebuilder:rbac:groups=azure.microsoft.com,resources=azureloadbalancers,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=azure.microsoft.com,resources=azureloadbalancers/status,verbs=get;update;patch + +func (r *AzureLoadBalancerReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + return r.Reconciler.Reconcile(req, &azurev1alpha1.AzureLoadBalancer{}) +} + +func (r *AzureLoadBalancerReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&azurev1alpha1.AzureLoadBalancer{}). + Complete(r) +} diff --git a/controllers/azureloadbalancer_controller_test.go b/controllers/azureloadbalancer_controller_test.go new file mode 100644 index 00000000000..3f8b6392e4d --- /dev/null +++ b/controllers/azureloadbalancer_controller_test.go @@ -0,0 +1,172 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// +build all azureloadbalancer + +package controllers + +import ( + "context" + "testing" + + azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/pkg/errhelp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestLoadBalancerNonExistingSubResources(t *testing.T) { + t.Parallel() + defer PanicRecover(t) + ctx := context.Background() + + lbName := GenerateTestResourceNameWithRandom("lb", 10) + pipName := GenerateTestResourceNameWithRandom("pip", 10) + bpName := "test" + natName := "test" + portRangeStart := 100 + portRangeEnd := 200 + backendPort := 300 + + // Create an LB instance + lbInstance := &azurev1alpha1.AzureLoadBalancer{ + ObjectMeta: metav1.ObjectMeta{ + Name: lbName, + Namespace: "default", + }, + Spec: azurev1alpha1.AzureLoadBalancerSpec{ + Location: tc.resourceGroupLocation, + ResourceGroup: tc.resourceGroupName, + PublicIPAddressName: pipName, + BackendAddressPoolName: bpName, + InboundNatPoolName: natName, + FrontendPortRangeStart: portRangeStart, + FrontendPortRangeEnd: portRangeEnd, + BackendPort: backendPort, + }, + } + + EnsureInstanceWithResult(ctx, t, tc, lbInstance, errhelp.InvalidResourceReference, false) + + EnsureDelete(ctx, t, tc, lbInstance) +} + +func TestLaodBalancerHappyPathWithPublicIPAddress(t *testing.T) { + t.Parallel() + defer PanicRecover(t) + ctx := context.Background() + + // Create a Public IP Address + pipName := GenerateTestResourceNameWithRandom("lb-pip1", 10) + publicIPAllocationMethod := "Static" + idleTimeoutInMinutes := 10 + publicIPAddressVersion := "IPv4" + skuName := "Basic" + pipInstance := &azurev1alpha1.AzurePublicIPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: pipName, + Namespace: "default", + }, + Spec: azurev1alpha1.AzurePublicIPAddressSpec{ + Location: tc.resourceGroupLocation, + ResourceGroup: tc.resourceGroupName, + PublicIPAllocationMethod: publicIPAllocationMethod, + IdleTimeoutInMinutes: idleTimeoutInMinutes, + PublicIPAddressVersion: publicIPAddressVersion, + SkuName: skuName, + }, + } + + EnsureInstance(ctx, t, tc, pipInstance) + + // Create a Load Balancer + lbName := GenerateTestResourceNameWithRandom("lb1", 10) + bpName := "test" + natName := "test" + portRangeStart := 100 + portRangeEnd := 200 + backendPort := 300 + lbInstance := &azurev1alpha1.AzureLoadBalancer{ + ObjectMeta: metav1.ObjectMeta{ + Name: lbName, + Namespace: "default", + }, + Spec: azurev1alpha1.AzureLoadBalancerSpec{ + Location: tc.resourceGroupLocation, + ResourceGroup: tc.resourceGroupName, + PublicIPAddressName: pipName, + BackendAddressPoolName: bpName, + InboundNatPoolName: natName, + FrontendPortRangeStart: portRangeStart, + FrontendPortRangeEnd: portRangeEnd, + BackendPort: backendPort, + }, + } + + EnsureInstance(ctx, t, tc, lbInstance) + + EnsureDelete(ctx, t, tc, lbInstance) + + EnsureDelete(ctx, t, tc, pipInstance) +} + +func TestLoadBalancerControllerNoResourceGroup(t *testing.T) { + t.Parallel() + defer PanicRecover(t) + ctx := context.Background() + + // Add any setup steps that needs to be executed before each test + rgName := GenerateTestResourceNameWithRandom("lb-rand-rg", 10) + + // Create a Public IP Address + pipName := GenerateTestResourceNameWithRandom("lb-pip2", 10) + publicIPAllocationMethod := "Static" + idleTimeoutInMinutes := 10 + publicIPAddressVersion := "IPv4" + skuName := "Basic" + pipInstance := &azurev1alpha1.AzurePublicIPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: pipName, + Namespace: "default", + }, + Spec: azurev1alpha1.AzurePublicIPAddressSpec{ + Location: tc.resourceGroupLocation, + ResourceGroup: tc.resourceGroupName, + PublicIPAllocationMethod: publicIPAllocationMethod, + IdleTimeoutInMinutes: idleTimeoutInMinutes, + PublicIPAddressVersion: publicIPAddressVersion, + SkuName: skuName, + }, + } + + EnsureInstance(ctx, t, tc, pipInstance) + + // Create a Load Balancer + lbName := GenerateTestResourceNameWithRandom("lb2", 10) + bpName := "test" + natName := "test" + portRangeStart := 100 + portRangeEnd := 200 + backendPort := 300 + lbInstance := &azurev1alpha1.AzureLoadBalancer{ + ObjectMeta: metav1.ObjectMeta{ + Name: lbName, + Namespace: "default", + }, + Spec: azurev1alpha1.AzureLoadBalancerSpec{ + Location: tc.resourceGroupLocation, + ResourceGroup: rgName, + PublicIPAddressName: pipName, + BackendAddressPoolName: bpName, + InboundNatPoolName: natName, + FrontendPortRangeStart: portRangeStart, + FrontendPortRangeEnd: portRangeEnd, + BackendPort: backendPort, + }, + } + + EnsureInstanceWithResult(ctx, t, tc, lbInstance, errhelp.ResourceGroupNotFoundErrorCode, false) + + EnsureDelete(ctx, t, tc, lbInstance) + + EnsureDelete(ctx, t, tc, pipInstance) +} diff --git a/controllers/azurevirtualmachine_controller_test.go b/controllers/azurevirtualmachine_controller_test.go index f9e38720f71..7ae5bb7cd41 100644 --- a/controllers/azurevirtualmachine_controller_test.go +++ b/controllers/azurevirtualmachine_controller_test.go @@ -7,23 +7,13 @@ package controllers import ( "context" - "crypto/rand" - "crypto/rsa" "testing" azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" "github.com/Azure/azure-service-operator/pkg/errhelp" - "golang.org/x/crypto/ssh" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func generateRandomSshPublicKeyString() string { - privateKey, _ := rsa.GenerateKey(rand.Reader, 2048) - publicRsaKey, _ := ssh.NewPublicKey(&privateKey.PublicKey) - sshPublicKeyData := string(ssh.MarshalAuthorizedKey(publicRsaKey)) - return sshPublicKeyData -} - func TestVirtualMachineControllerNoResourceGroup(t *testing.T) { t.Parallel() defer PanicRecover(t) @@ -136,7 +126,7 @@ func TestVirtualMachineHappyPathWithNicPipVNetAndSubnet(t *testing.T) { vmImageUrn := "Canonical:UbuntuServer:16.04-LTS:latest" userName := "azureuser" - sshPublicKeyData := generateRandomSshPublicKeyString() + sshPublicKeyData := GenerateRandomSshPublicKeyString() vmInstance := &azurev1alpha1.AzureVirtualMachine{ ObjectMeta: metav1.ObjectMeta{ diff --git a/controllers/azurevmscaleset_controller.go b/controllers/azurevmscaleset_controller.go new file mode 100644 index 00000000000..bb3625c5a33 --- /dev/null +++ b/controllers/azurevmscaleset_controller.go @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package controllers + +import ( + ctrl "sigs.k8s.io/controller-runtime" + + azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" +) + +// AzureVMScaleSetReconciler reconciles a AzureVMScaleSet object +type AzureVMScaleSetReconciler struct { + Reconciler *AsyncReconciler +} + +// +kubebuilder:rbac:groups=azure.microsoft.com,resources=azurevmscalesets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=azure.microsoft.com,resources=azurevmscalesets/status,verbs=get;update;patch + +func (r *AzureVMScaleSetReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + return r.Reconciler.Reconcile(req, &azurev1alpha1.AzureVMScaleSet{}) +} + +func (r *AzureVMScaleSetReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&azurev1alpha1.AzureVMScaleSet{}). + Complete(r) +} diff --git a/controllers/azurevmscaleset_controller_test.go b/controllers/azurevmscaleset_controller_test.go new file mode 100644 index 00000000000..9391a7f7d23 --- /dev/null +++ b/controllers/azurevmscaleset_controller_test.go @@ -0,0 +1,180 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// +build all azurevmscaleset + +package controllers + +import ( + "context" + "testing" + + azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/pkg/errhelp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestVMScaleSetControllerNoResourceGroup(t *testing.T) { + t.Parallel() + defer PanicRecover(t) + ctx := context.Background() + + rgName := GenerateTestResourceNameWithRandom("rg", 10) + vmName := GenerateTestResourceNameWithRandom("vm", 10) + vmSize := "Standard_DS1_v2" + capacity := 2 + osType := azurev1alpha1.OSType("Linux") + adminUserName := GenerateTestResourceNameWithRandom("u", 10) + sshPublicKeyData := GenerateTestResourceNameWithRandom("ssh", 10) + platformImageUrn := "Canonical:UbuntuServer:16.04-LTS:latest" + vnetName := GenerateTestResourceNameWithRandom("vn", 10) + subnetName := "test" + lbName := GenerateTestResourceNameWithRandom("lb", 10) + beName := GenerateTestResourceNameWithRandom("be", 10) + natName := GenerateTestResourceNameWithRandom("nat", 10) + + // Create a VMSS + vmssInstance := &azurev1alpha1.AzureVMScaleSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: vmName, + Namespace: "default", + }, + Spec: azurev1alpha1.AzureVMScaleSetSpec{ + Location: tc.resourceGroupLocation, + ResourceGroup: rgName, + VMSize: vmSize, + Capacity: capacity, + OSType: osType, + AdminUserName: adminUserName, + SSHPublicKeyData: sshPublicKeyData, + PlatformImageURN: platformImageUrn, + VirtualNetworkName: vnetName, + SubnetName: subnetName, + LoadBalancerName: lbName, + BackendAddressPoolName: beName, + InboundNatPoolName: natName, + }, + } + + EnsureInstanceWithResult(ctx, t, tc, vmssInstance, errhelp.ResourceGroupNotFoundErrorCode, false) + + EnsureDelete(ctx, t, tc, vmssInstance) +} + +func TestVMScaleSetHappyPathWithLbPipVNetAndSubnet(t *testing.T) { + t.Parallel() + defer PanicRecover(t) + ctx := context.Background() + + // Create a vnet with a subnet + vnetName := GenerateTestResourceNameWithRandom("vnt", 10) + subnetName := GenerateTestResourceNameWithRandom("snt", 10) + vnetSubNetInstance := azurev1alpha1.VNetSubnets{ + SubnetName: subnetName, + SubnetAddressPrefix: "110.1.0.0/16", + } + pipName := GenerateTestResourceNameWithRandom("pip3", 10) + vnetInstance := &azurev1alpha1.VirtualNetwork{ + ObjectMeta: metav1.ObjectMeta{ + Name: vnetName, + Namespace: "default", + }, + Spec: azurev1alpha1.VirtualNetworkSpec{ + Location: tc.resourceGroupLocation, + ResourceGroup: tc.resourceGroupName, + AddressSpace: "110.0.0.0/8", + Subnets: []azurev1alpha1.VNetSubnets{vnetSubNetInstance}, + }, + } + + EnsureInstance(ctx, t, tc, vnetInstance) + + // Create a Public IP Address + publicIPAllocationMethod := "Static" + idleTimeoutInMinutes := 10 + publicIPAddressVersion := "IPv4" + skuName := "Basic" + pipInstance := &azurev1alpha1.AzurePublicIPAddress{ + ObjectMeta: metav1.ObjectMeta{ + Name: pipName, + Namespace: "default", + }, + Spec: azurev1alpha1.AzurePublicIPAddressSpec{ + Location: tc.resourceGroupLocation, + ResourceGroup: tc.resourceGroupName, + PublicIPAllocationMethod: publicIPAllocationMethod, + IdleTimeoutInMinutes: idleTimeoutInMinutes, + PublicIPAddressVersion: publicIPAddressVersion, + SkuName: skuName, + }, + } + + EnsureInstance(ctx, t, tc, pipInstance) + + // Create a Load Balancer + lbName := GenerateTestResourceNameWithRandom("lb", 10) + bpName := "test" + natName := "test" + portRangeStart := 100 + portRangeEnd := 200 + backendPort := 300 + lbInstance := &azurev1alpha1.AzureLoadBalancer{ + ObjectMeta: metav1.ObjectMeta{ + Name: lbName, + Namespace: "default", + }, + Spec: azurev1alpha1.AzureLoadBalancerSpec{ + Location: tc.resourceGroupLocation, + ResourceGroup: tc.resourceGroupName, + PublicIPAddressName: pipName, + BackendAddressPoolName: bpName, + InboundNatPoolName: natName, + FrontendPortRangeStart: portRangeStart, + FrontendPortRangeEnd: portRangeEnd, + BackendPort: backendPort, + }, + } + + EnsureInstance(ctx, t, tc, lbInstance) + + // Create a VMSS + vmName := GenerateTestResourceNameWithRandom("vm", 10) + vmSize := "Standard_DS1_v2" + capacity := 3 + osType := azurev1alpha1.OSType("Linux") + vmImageUrn := "Canonical:UbuntuServer:16.04-LTS:latest" + userName := "azureuser" + sshPublicKeyData := GenerateRandomSshPublicKeyString() + + vmssInstance := &azurev1alpha1.AzureVMScaleSet{ + ObjectMeta: metav1.ObjectMeta{ + Name: vmName, + Namespace: "default", + }, + Spec: azurev1alpha1.AzureVMScaleSetSpec{ + Location: tc.resourceGroupLocation, + ResourceGroup: tc.resourceGroupName, + VMSize: vmSize, + Capacity: capacity, + OSType: osType, + AdminUserName: userName, + SSHPublicKeyData: sshPublicKeyData, + PlatformImageURN: vmImageUrn, + VirtualNetworkName: vnetName, + SubnetName: subnetName, + LoadBalancerName: lbName, + BackendAddressPoolName: bpName, + InboundNatPoolName: natName, + }, + } + + EnsureInstance(ctx, t, tc, vmssInstance) + + EnsureDelete(ctx, t, tc, vmssInstance) + + EnsureDelete(ctx, t, tc, lbInstance) + + EnsureDelete(ctx, t, tc, pipInstance) + + EnsureDelete(ctx, t, tc, vnetInstance) +} diff --git a/controllers/helpers.go b/controllers/helpers.go index a542b41c9a0..b4ea2174bba 100644 --- a/controllers/helpers.go +++ b/controllers/helpers.go @@ -5,6 +5,8 @@ package controllers import ( "context" + "crypto/rand" + "crypto/rsa" "encoding/json" "fmt" @@ -18,6 +20,7 @@ import ( "github.com/go-logr/logr" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "golang.org/x/crypto/ssh" apierrors "k8s.io/apimachinery/pkg/api/errors" apierrs "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" @@ -433,3 +436,11 @@ func GenerateAlphaNumTestResourceName(id string) string { func GenerateAlphaNumTestResourceNameWithRandom(id string, rc int) string { return helpers.RemoveNonAlphaNumeric(GenerateTestResourceName(id) + helpers.RandomString(rc)) } + +// GenerateRandomSshPublicKeyString returns a random string of SSH public key data +func GenerateRandomSshPublicKeyString() string { + privateKey, _ := rsa.GenerateKey(rand.Reader, 2048) + publicRsaKey, _ := ssh.NewPublicKey(&privateKey.PublicKey) + sshPublicKeyData := string(ssh.MarshalAuthorizedKey(publicRsaKey)) + return sshPublicKeyData +} diff --git a/controllers/postgresql_combined_controller_test.go b/controllers/postgresql_combined_controller_test.go index befc6787633..5ee9509d6e9 100644 --- a/controllers/postgresql_combined_controller_test.go +++ b/controllers/postgresql_combined_controller_test.go @@ -38,12 +38,13 @@ func TestPSQLDatabaseController(t *testing.T) { Spec: azurev1alpha1.PostgreSQLServerSpec{ Location: rgLocation, ResourceGroup: rgName, + CreateMode: "Default", Sku: azurev1alpha1.AzureDBsSQLSku{ - Name: "B_Gen5_2", - Tier: azurev1alpha1.SkuTier("Basic"), + Name: "GP_Gen5_4", + Tier: azurev1alpha1.SkuTier("GeneralPurpose"), Family: "Gen5", Size: "51200", - Capacity: 2, + Capacity: 4, }, ServerVersion: azurev1alpha1.ServerVersion("10"), SSLEnforcement: azurev1alpha1.SslEnforcementEnumEnabled, @@ -92,7 +93,6 @@ func TestPSQLDatabaseController(t *testing.T) { EnsureDelete(ctx, t, tc, postgreSQLFirewallRuleInstance) - // Add any teardown steps that needs to be executed after each test EnsureDelete(ctx, t, tc, postgreSQLServerInstance) } diff --git a/controllers/postgresqlserver_controller_test.go b/controllers/postgresqlserver_controller_test.go index c97948d06d6..db123fe98b0 100644 --- a/controllers/postgresqlserver_controller_test.go +++ b/controllers/postgresqlserver_controller_test.go @@ -50,3 +50,34 @@ func TestPSQLServerControllerNoResourceGroup(t *testing.T) { EnsureDelete(ctx, t, tc, postgreSQLServerInstance) } + +func TestPSQLServerControllerReplicaNoSourceServer(t *testing.T) { + t.Parallel() + defer PanicRecover(t) + ctx := context.Background() + + // Add any setup steps that needs to be executed before each test + rgName := tc.resourceGroupName + rgLocation := tc.resourceGroupLocation + + postgreSQLServerReplicaName := GenerateTestResourceNameWithRandom("psql-rep", 10) + + // Create the PostgreSQL Replica Server object and expect the Reconcile to be created + + postgreSQLServerReplicaInstance := &azurev1alpha1.PostgreSQLServer{ + ObjectMeta: metav1.ObjectMeta{ + Name: postgreSQLServerReplicaName, + Namespace: "default", + }, + Spec: azurev1alpha1.PostgreSQLServerSpec{ + Location: rgLocation, + ResourceGroup: rgName, + CreateMode: "Replica", + }, + } + + errMessage := "Replica requested but source server unspecified" + EnsureInstanceWithResult(ctx, t, tc, postgreSQLServerReplicaInstance, errMessage, false) + EnsureDelete(ctx, t, tc, postgreSQLServerReplicaInstance) + +} diff --git a/controllers/suite_test.go b/controllers/suite_test.go index e64f5ce1e92..4d39ae0f1bd 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -31,6 +31,7 @@ import ( resourcemanagercosmosdb "github.com/Azure/azure-service-operator/pkg/resourcemanager/cosmosdbs" resourcemanagereventhub "github.com/Azure/azure-service-operator/pkg/resourcemanager/eventhubs" resourcemanagerkeyvaults "github.com/Azure/azure-service-operator/pkg/resourcemanager/keyvaults" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/loadbalancer" mysqlDatabaseManager "github.com/Azure/azure-service-operator/pkg/resourcemanager/mysql/database" mysqlFirewallManager "github.com/Azure/azure-service-operator/pkg/resourcemanager/mysql/firewallrule" mysqlServerManager "github.com/Azure/azure-service-operator/pkg/resourcemanager/mysql/server" @@ -45,6 +46,7 @@ import ( resourcemanagerblobcontainer "github.com/Azure/azure-service-operator/pkg/resourcemanager/storages/blobcontainer" resourcemanagerstorageaccount "github.com/Azure/azure-service-operator/pkg/resourcemanager/storages/storageaccount" "github.com/Azure/azure-service-operator/pkg/resourcemanager/vm" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/vmss" resourcemanagervnet "github.com/Azure/azure-service-operator/pkg/resourcemanager/vnet" telemetry "github.com/Azure/azure-service-operator/pkg/telemetry" @@ -474,6 +476,44 @@ func setup() error { return err } + err = (&AzureLoadBalancerReconciler{ + Reconciler: &AsyncReconciler{ + Client: k8sManager.GetClient(), + AzureClient: loadbalancer.NewAzureLoadBalancerClient( + secretClient, + k8sManager.GetScheme(), + ), + Telemetry: telemetry.InitializeTelemetryDefault( + "LoadBalancer", + ctrl.Log.WithName("controllers").WithName("LoadBalancer"), + ), + Recorder: k8sManager.GetEventRecorderFor("LoadBalancer-controller"), + Scheme: scheme.Scheme, + }, + }).SetupWithManager(k8sManager) + if err != nil { + return err + } + + err = (&AzureVMScaleSetReconciler{ + Reconciler: &AsyncReconciler{ + Client: k8sManager.GetClient(), + AzureClient: vmss.NewAzureVMScaleSetClient( + secretClient, + k8sManager.GetScheme(), + ), + Telemetry: telemetry.InitializeTelemetryDefault( + "VMScaleSet", + ctrl.Log.WithName("controllers").WithName("VMScaleSet"), + ), + Recorder: k8sManager.GetEventRecorderFor("VMScaleSet-controller"), + Scheme: scheme.Scheme, + }, + }).SetupWithManager(k8sManager) + if err != nil { + return err + } + err = (&AzureSqlActionReconciler{ Reconciler: &AsyncReconciler{ Client: k8sManager.GetClient(), diff --git a/docs/postgresql/postgresql.md b/docs/postgresql/postgresql.md index 5990445c729..93de0567444 100644 --- a/docs/postgresql/postgresql.md +++ b/docs/postgresql/postgresql.md @@ -33,6 +33,14 @@ This secret contains the following fields. For more information on where and how secrets are stored, look [here](/docs/secrets.md) +##### Read Replicas in Azure Database for PostgreSQL + +The PostgreSQL server operator can also be used to create Read Replicas given the `sourceserverid`. + +The replica inherits the admin account from the master server. All user accounts on the master server are replicated to the read replicas. + +For more information on read replicas, refer [here](https://docs.microsoft.com/en-us/azure/postgresql/concepts-read-replicas) + ### PostgreSQL Database Here is a [sample YAML](/config/samples/azure_v1alpha1_postgresqldatabase.yaml) for PostgreSQL database diff --git a/docs/virtualmachine/virtualmachine.md b/docs/virtualmachine/virtualmachine.md index 1bd1854f60f..6f302ca814e 100644 --- a/docs/virtualmachine/virtualmachine.md +++ b/docs/virtualmachine/virtualmachine.md @@ -2,8 +2,6 @@ This operator deploys an Azure Virtual Machine (VM) into a specified resource group at the specified location. Users can specify platform image, size, user name and public SSH key, etc. for the VM. -Note that in the current version you can only create VM using Linux platform images. - Learn more about Azure Virtual Machine [here](https://docs.microsoft.com/en-us/rest/api/compute/virtualmachines). Here is a [sample YAML](/config/samples/azure_v1alpha1_azurevirtualmachine.yaml) to provision a Virtual Machine. diff --git a/docs/virtualnetwork/loadbalancer.md b/docs/virtualnetwork/loadbalancer.md new file mode 100644 index 00000000000..665767d1f61 --- /dev/null +++ b/docs/virtualnetwork/loadbalancer.md @@ -0,0 +1,37 @@ +# Load Balancer Operator + +This operator deploys an Azure Load Balancer (LB) into a specified resource group at the specified location. + +Learn more about Azure Network Interface [here](https://docs.microsoft.com/en-us/rest/api/load-balancer/loadbalancers/createorupdate). + +Here is a [sample YAML](/config/samples/azure_v1alpha1_azureloadbalancer.yaml) to provision a Load Balancer. + +The spec is comprised of the following fields: + +* Location +* ResourceGroup +* PublicIPAddressName +* BackendAddressPoolName +* InboundNatPoolName +* FrontendPortRangeStart +* FrontendPortRangeEnd +* BackendPort + +### Required Fields + +A Network Interface needs the following fields to deploy, along with a location and resource group. + +* `PublicIPAddressName` specify the name for the public IP address that the network interface uses +* `BackendAddressPoolName` specify the backend address pool name +* `InboundNatPoolName` specify the inbound nat pool name +* `FrontendPortRangeStart` specify the start of the front end port range +* `FrontendPortRangeEnd` specify the end of the front end port range +* `BackendPort` specify the backend port + +### Optional Fields + +Not available. + +## Deploy, view and delete resources + +You can follow the steps [here](/docs/customresource.md) to deploy, view and delete resources. diff --git a/docs/vmscaleset/vmscaleset.md b/docs/vmscaleset/vmscaleset.md new file mode 100644 index 00000000000..ccfda1176f2 --- /dev/null +++ b/docs/vmscaleset/vmscaleset.md @@ -0,0 +1,45 @@ +# Virtual Machine Scale Set Operator + +This operator deploys an Azure Virtual Machine Scale Set (VMSS) into a specified resource group at the specified location. Users can specify platform image, size, user name and public SSH key, etc. for the VMSS. + +Learn more about Azure Virtual Machine [here](https://docs.microsoft.com/en-us/rest/api/compute/virtualmachinescalesets). + +Here is a [sample YAML](/config/samples/azure_v1alpha1_azurevmscaleset.yaml) to provision a VMSS. + +The spec is comprised of the following fields: + +* Location +* ResourceGroup +* VMSize +* Capacity +* AdminUserName +* SshPublicKeyData +* PlatformImageURN +* VirtualNetworkName +* SubnetName +* LoadBalancerName +* BackendAddressPoolName +* InboundNatPoolName + +### Required Fields + +A Virtual Machine needs the following fields to deploy, along with a location and resource group. + +* `VMSize` specify the VM size for the virtual machine scale set +* `Capacity` specify the number of instances in the VMSS +* `AdminUserName` specify the user name for the virtual machine scale set +* `SshPublicKeyData` specify the SSH public key data for loging into the virtual machine scale set +* `PlatformImageURN` specify the platform image's uniform resource name (URN) in the 'publisher:offer:sku:version' format. +* `VirtualNetworkName` specify the virtual network +* `SubnetName` specify the subnet +* `LoadBalancerName` specify the load balancer +* `BackendAddressPoolName` specify the backend address pool +* `InboundNatPoolName` specify the inbound nat pool + +### Optional Fields + +Not available. + +## Deploy, view and delete resources + +You can follow the steps [here](/docs/customresource.md) to deploy, view and delete resources. diff --git a/main.go b/main.go index 3edc9ea740c..23542f70bac 100644 --- a/main.go +++ b/main.go @@ -25,6 +25,7 @@ import ( resourcemanagercosmosdb "github.com/Azure/azure-service-operator/pkg/resourcemanager/cosmosdbs" resourcemanagereventhub "github.com/Azure/azure-service-operator/pkg/resourcemanager/eventhubs" resourcemanagerkeyvault "github.com/Azure/azure-service-operator/pkg/resourcemanager/keyvaults" + loadbalancer "github.com/Azure/azure-service-operator/pkg/resourcemanager/loadbalancer" mysqldatabase "github.com/Azure/azure-service-operator/pkg/resourcemanager/mysql/database" mysqlfirewall "github.com/Azure/azure-service-operator/pkg/resourcemanager/mysql/firewallrule" mysqlserver "github.com/Azure/azure-service-operator/pkg/resourcemanager/mysql/server" @@ -40,6 +41,7 @@ import ( blobContainerManager "github.com/Azure/azure-service-operator/pkg/resourcemanager/storages/blobcontainer" storageaccountManager "github.com/Azure/azure-service-operator/pkg/resourcemanager/storages/storageaccount" vm "github.com/Azure/azure-service-operator/pkg/resourcemanager/vm" + vmss "github.com/Azure/azure-service-operator/pkg/resourcemanager/vmss" vnet "github.com/Azure/azure-service-operator/pkg/resourcemanager/vnet" "github.com/Azure/azure-service-operator/pkg/secrets" keyvaultSecrets "github.com/Azure/azure-service-operator/pkg/secrets/keyvault" @@ -686,6 +688,43 @@ func main() { os.Exit(1) } + if err = (&controllers.AzureLoadBalancerReconciler{ + Reconciler: &controllers.AsyncReconciler{ + Client: mgr.GetClient(), + AzureClient: loadbalancer.NewAzureLoadBalancerClient( + secretClient, + mgr.GetScheme(), + ), + Telemetry: telemetry.InitializeTelemetryDefault( + "LoadBalancer", + ctrl.Log.WithName("controllers").WithName("LoadBalancer"), + ), + Recorder: mgr.GetEventRecorderFor("LoadBalancer-controller"), + Scheme: scheme, + }, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "LoadBalancer") + os.Exit(1) + } + + if err = (&controllers.AzureVMScaleSetReconciler{ + Reconciler: &controllers.AsyncReconciler{ + Client: mgr.GetClient(), + AzureClient: vmss.NewAzureVMScaleSetClient( + secretClient, + mgr.GetScheme(), + ), + Telemetry: telemetry.InitializeTelemetryDefault( + "VMScaleSet", + ctrl.Log.WithName("controllers").WithName("VMScaleSet"), + ), + Recorder: mgr.GetEventRecorderFor("VMScaleSet-controller"), + Scheme: scheme, + }, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "VMScaleSet") + os.Exit(1) + } // +kubebuilder:scaffold:builder setupLog.Info("starting manager") diff --git a/pkg/resourcemanager/loadbalancer/client.go b/pkg/resourcemanager/loadbalancer/client.go new file mode 100644 index 00000000000..6ee5dce8653 --- /dev/null +++ b/pkg/resourcemanager/loadbalancer/client.go @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package loadbalancer + +import ( + "context" + "strings" + + vnetwork "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-09-01/network" + "github.com/Azure/azure-service-operator/pkg/helpers" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/iam" + "github.com/Azure/azure-service-operator/pkg/secrets" + "k8s.io/apimachinery/pkg/runtime" +) + +type AzureLoadBalancerClient struct { + SecretClient secrets.SecretClient + Scheme *runtime.Scheme +} + +func NewAzureLoadBalancerClient(secretclient secrets.SecretClient, scheme *runtime.Scheme) *AzureLoadBalancerClient { + return &AzureLoadBalancerClient{ + SecretClient: secretclient, + Scheme: scheme, + } +} + +func getLoadBalancerClient() vnetwork.LoadBalancersClient { + lbClient := vnetwork.NewLoadBalancersClientWithBaseURI(config.BaseURI(), config.SubscriptionID()) + a, _ := iam.GetResourceManagementAuthorizer() + lbClient.Authorizer = a + lbClient.AddToUserAgent(config.UserAgent()) + return lbClient +} + +func (m *AzureLoadBalancerClient) CreateLoadBalancer(ctx context.Context, location string, resourceGroupName string, resourceName string, publicIPAddressName string, backendAddressPoolName string, inboundNatPoolName string, frontendPortRangeStart int, frontendPortRangeEnd int, backendPort int) (future vnetwork.LoadBalancersCreateOrUpdateFuture, err error) { + + client := getLoadBalancerClient() + + publicIPAddressIDInput := helpers.MakeResourceID( + client.SubscriptionID, + resourceGroupName, + "Microsoft.Network", + "publicIPAddresses", + publicIPAddressName, + "", + "", + ) + + publicIPAddress := vnetwork.PublicIPAddress{ + ID: &publicIPAddressIDInput, + } + + frontEndIPConfigName := strings.Join([]string{resourceName, "IpCfg"}, "-") + frontendIPConfiguration := vnetwork.FrontendIPConfiguration{ + Name: &frontEndIPConfigName, + FrontendIPConfigurationPropertiesFormat: &vnetwork.FrontendIPConfigurationPropertiesFormat{ + PublicIPAddress: &publicIPAddress, + }, + } + + var ipConfigsToAdd []vnetwork.FrontendIPConfiguration + ipConfigsToAdd = append( + ipConfigsToAdd, + frontendIPConfiguration, + ) + + frontendIPConfigId := helpers.MakeResourceID( + client.SubscriptionID, + resourceGroupName, + "Microsoft.Network", + "loadBalancers", + resourceName, + "frontendIPConfigurations", + frontEndIPConfigName, + ) + + frontendIPConfigurationSubResource := vnetwork.SubResource{ + ID: &frontendIPConfigId, + } + + var bcPoolsToAdd []vnetwork.BackendAddressPool + bcPoolsToAdd = append( + bcPoolsToAdd, + vnetwork.BackendAddressPool{ + Name: &backendAddressPoolName, + }, + ) + + frontendPortRangeStartInt32 := int32(frontendPortRangeStart) + frontendPortRangeEndInt32 := int32(frontendPortRangeEnd) + backendPortInt32 := int32(backendPort) + + var natPoolsToAdd []vnetwork.InboundNatPool + natPoolsToAdd = append( + natPoolsToAdd, + vnetwork.InboundNatPool{ + Name: &inboundNatPoolName, + InboundNatPoolPropertiesFormat: &vnetwork.InboundNatPoolPropertiesFormat{ + FrontendIPConfiguration: &frontendIPConfigurationSubResource, + Protocol: vnetwork.TransportProtocolTCP, + FrontendPortRangeStart: &frontendPortRangeStartInt32, + FrontendPortRangeEnd: &frontendPortRangeEndInt32, + BackendPort: &backendPortInt32, + }, + }, + ) + + future, err = client.CreateOrUpdate( + ctx, + resourceGroupName, + resourceName, + vnetwork.LoadBalancer{ + Location: &location, + LoadBalancerPropertiesFormat: &vnetwork.LoadBalancerPropertiesFormat{ + FrontendIPConfigurations: &ipConfigsToAdd, + BackendAddressPools: &bcPoolsToAdd, + InboundNatPools: &natPoolsToAdd, + }, + }, + ) + + return future, err +} + +func (m *AzureLoadBalancerClient) DeleteLoadBalancer(ctx context.Context, loadBalancerName string, resourcegroup string) (status string, err error) { + + client := getLoadBalancerClient() + + _, err = client.Get(ctx, resourcegroup, loadBalancerName, "") + if err == nil { // load balancer present, so go ahead and delete + future, err := client.Delete(ctx, resourcegroup, loadBalancerName) + return future.Status(), err + } + // load balancer not present so return success anyway + return "load balancer not present", nil + +} + +func (m *AzureLoadBalancerClient) GetLoadBalancer(ctx context.Context, resourcegroup string, loadBalancerName string) (lb vnetwork.LoadBalancer, err error) { + + client := getLoadBalancerClient() + + return client.Get(ctx, resourcegroup, loadBalancerName, "") +} diff --git a/pkg/resourcemanager/loadbalancer/manager.go b/pkg/resourcemanager/loadbalancer/manager.go new file mode 100644 index 00000000000..4a2403f25ce --- /dev/null +++ b/pkg/resourcemanager/loadbalancer/manager.go @@ -0,0 +1,35 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package loadbalancer + +import ( + "context" + + network "github.com/Azure/azure-sdk-for-go/services/network/mgmt/2019-09-01/network" + "github.com/Azure/azure-service-operator/pkg/resourcemanager" +) + +type LoadBalancerManager interface { + CreateLoadBalancer(ctx context.Context, + location string, + resourceGroupName string, + resourceName string, + publicIPAddressName string, + backEndPoolName string, + inboundNatPoolName string, + frontEndPortRangeStart int, + frontEndPortRangeEnd int, + backEndPort int) (network.LoadBalancer, error) + + DeleteLoadBalancer(ctx context.Context, + resourceName string, + resourceGroupName string) (string, error) + + GetLoadBalancer(ctx context.Context, + resourceGroupName string, + resourceName string) (network.LoadBalancer, error) + + // also embed async client methods + resourcemanager.ARMClient +} diff --git a/pkg/resourcemanager/loadbalancer/reconcile.go b/pkg/resourcemanager/loadbalancer/reconcile.go new file mode 100644 index 00000000000..04468d8b641 --- /dev/null +++ b/pkg/resourcemanager/loadbalancer/reconcile.go @@ -0,0 +1,190 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package loadbalancer + +import ( + "context" + "fmt" + + azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/pkg/errhelp" + "github.com/Azure/azure-service-operator/pkg/helpers" + "github.com/Azure/azure-service-operator/pkg/resourcemanager" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" +) + +func (g *AzureLoadBalancerClient) Ensure(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { + + instance, err := g.convert(obj) + if err != nil { + return true, err + } + + client := getLoadBalancerClient() + + location := instance.Spec.Location + resourceGroup := instance.Spec.ResourceGroup + resourceName := instance.Name + publicIPAddressName := instance.Spec.PublicIPAddressName + backendAddressPoolName := instance.Spec.BackendAddressPoolName + inboundNatPoolName := instance.Spec.InboundNatPoolName + frontendPortRangeStart := instance.Spec.FrontendPortRangeStart + frontendPortRangeEnd := instance.Spec.FrontendPortRangeEnd + backendPort := instance.Spec.BackendPort + + instance.Status.Provisioning = true + // Check if this item already exists. This is required + // to overcome the issue with the lack of idempotence of the Create call + item, err := g.GetLoadBalancer(ctx, resourceGroup, resourceName) + if err == nil { + instance.Status.Provisioned = true + instance.Status.Provisioning = false + instance.Status.Message = resourcemanager.SuccessMsg + instance.Status.ResourceId = *item.ID + return true, nil + } + future, err := g.CreateLoadBalancer( + ctx, + location, + resourceGroup, + resourceName, + publicIPAddressName, + backendAddressPoolName, + inboundNatPoolName, + frontendPortRangeStart, + frontendPortRangeEnd, + backendPort, + ) + if err != nil { + // let the user know what happened + instance.Status.Message = err.Error() + instance.Status.Provisioning = false + // errors we expect might happen that we are ok with waiting for + catch := []string{ + errhelp.ResourceGroupNotFoundErrorCode, + errhelp.ParentNotFoundErrorCode, + errhelp.NotFoundErrorCode, + errhelp.AsyncOpIncompleteError, + errhelp.ResourceNotFound, + errhelp.InvalidResourceReference, + } + + azerr := errhelp.NewAzureErrorAzureError(err) + if helpers.ContainsString(catch, azerr.Type) { + // most of these error technically mean the resource is actually not provisioning + switch azerr.Type { + case errhelp.AsyncOpIncompleteError: + instance.Status.Provisioning = true + } + // reconciliation is not done but error is acceptable + return false, nil + } + // reconciliation not done and we don't know what happened + return false, err + } + + _, err = future.Result(client) + if err != nil { + // let the user know what happened + instance.Status.Message = err.Error() + instance.Status.Provisioning = false + // errors we expect might happen that we are ok with waiting for + catch := []string{ + errhelp.ResourceGroupNotFoundErrorCode, + errhelp.ParentNotFoundErrorCode, + errhelp.NotFoundErrorCode, + errhelp.AsyncOpIncompleteError, + errhelp.SubscriptionDoesNotHaveServer, + } + + azerr := errhelp.NewAzureErrorAzureError(err) + if helpers.ContainsString(catch, azerr.Type) { + // most of these error technically mean the resource is actually not provisioning + switch azerr.Type { + case errhelp.AsyncOpIncompleteError: + instance.Status.Provisioning = true + } + // reconciliation is not done but error is acceptable + return false, nil + } + // reconciliation not done and we don't know what happened + return false, err + } + + if instance.Status.Provisioning { + instance.Status.Provisioned = true + instance.Status.Provisioning = false + instance.Status.Message = resourcemanager.SuccessMsg + } else { + instance.Status.Provisioned = false + instance.Status.Provisioning = true + } + + return true, nil +} + +func (g *AzureLoadBalancerClient) Delete(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { + + instance, err := g.convert(obj) + if err != nil { + return true, err + } + + resourceGroup := instance.Spec.ResourceGroup + resourceName := instance.Name + + status, err := g.DeleteLoadBalancer( + ctx, + resourceName, + resourceGroup, + ) + if err != nil { + if !errhelp.IsAsynchronousOperationNotComplete(err) { + return true, err + } + } + + if err == nil { + if status != "InProgress" { + return false, nil + } + } + + return true, nil +} +func (g *AzureLoadBalancerClient) GetParents(obj runtime.Object) ([]resourcemanager.KubeParent, error) { + + instance, err := g.convert(obj) + if err != nil { + return nil, err + } + + return []resourcemanager.KubeParent{ + { + Key: types.NamespacedName{ + Namespace: instance.Namespace, + Name: instance.Spec.ResourceGroup, + }, + Target: &azurev1alpha1.ResourceGroup{}, + }, + }, nil +} + +func (g *AzureLoadBalancerClient) GetStatus(obj runtime.Object) (*azurev1alpha1.ASOStatus, error) { + + instance, err := g.convert(obj) + if err != nil { + return nil, err + } + return &instance.Status, nil +} + +func (g *AzureLoadBalancerClient) convert(obj runtime.Object) (*azurev1alpha1.AzureLoadBalancer, error) { + local, ok := obj.(*azurev1alpha1.AzureLoadBalancer) + if !ok { + return nil, fmt.Errorf("failed type assertion on kind: %s", obj.GetObjectKind().GroupVersionKind().String()) + } + return local, nil +} diff --git a/pkg/resourcemanager/psql/database/database_test.go b/pkg/resourcemanager/psql/database/database_test.go index 64e4357ff25..91016ef593e 100644 --- a/pkg/resourcemanager/psql/database/database_test.go +++ b/pkg/resourcemanager/psql/database/database_test.go @@ -53,7 +53,7 @@ var _ = Describe("PSQL database", func() { if err == nil { return true } - _, err = PSQLServerManager.CreateServerIfValid( + _, _, err = PSQLServerManager.CreateServerIfValid( ctx, psqlServer, rgName, @@ -64,6 +64,8 @@ var _ = Describe("PSQL database", func() { pSQLSku, "adm1nus3r", "m@#terU$3r", + "", + "", ) if err != nil { fmt.Println(err.Error()) diff --git a/pkg/resourcemanager/psql/firewallrule/firewallrule_test.go b/pkg/resourcemanager/psql/firewallrule/firewallrule_test.go index 73b205d349b..a9481a326e0 100644 --- a/pkg/resourcemanager/psql/firewallrule/firewallrule_test.go +++ b/pkg/resourcemanager/psql/firewallrule/firewallrule_test.go @@ -53,7 +53,7 @@ var _ = Describe("PSQL server", func() { if err == nil { return true } - _, err = PSQLServerManager.CreateServerIfValid( + _, _, err = PSQLServerManager.CreateServerIfValid( ctx, psqlServer, rgName, @@ -64,6 +64,8 @@ var _ = Describe("PSQL server", func() { pSQLSku, "adm1nus3r", "m@#terU$3r", + "", + "", ) if err != nil { fmt.Println(err.Error()) diff --git a/pkg/resourcemanager/psql/server/server.go b/pkg/resourcemanager/psql/server/server.go index 21c2b1c4a73..54a16d4ffee 100644 --- a/pkg/resourcemanager/psql/server/server.go +++ b/pkg/resourcemanager/psql/server/server.go @@ -6,6 +6,7 @@ package server import ( "context" "fmt" + "strings" psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" @@ -13,6 +14,7 @@ import ( "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" "github.com/Azure/azure-service-operator/pkg/resourcemanager/iam" "github.com/Azure/azure-service-operator/pkg/secrets" + "github.com/Azure/go-autorest/autorest/to" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" ) @@ -72,42 +74,71 @@ func (p *PSQLServerClient) CheckServerNameAvailability(ctx context.Context, serv } -func (p *PSQLServerClient) CreateServerIfValid(ctx context.Context, servername string, resourcegroup string, location string, tags map[string]*string, serverversion psql.ServerVersion, sslenforcement psql.SslEnforcementEnum, skuInfo psql.Sku, adminlogin string, adminpassword string) (server psql.Server, err error) { +func (p *PSQLServerClient) CreateServerIfValid(ctx context.Context, + servername string, + resourcegroup string, + location string, + tags map[string]*string, + serverversion psql.ServerVersion, + sslenforcement psql.SslEnforcementEnum, + skuInfo psql.Sku, adminlogin string, + adminpassword string, + createmode string, + sourceserver string) (pollingURL string, server psql.Server, err error) { client, err := getPSQLServersClient() if err != nil { - return psql.Server{}, err + return "", psql.Server{}, err } // Check if name is valid if this is the first create call valid, err := p.CheckServerNameAvailability(ctx, servername) - if valid == false { - return psql.Server{}, err + if !valid { + return "", psql.Server{}, err + } + + var result psql.ServersCreateFuture + + if strings.EqualFold(createmode, string(psql.CreateModeReplica)) { + result, err = client.Create( + ctx, + resourcegroup, + servername, + psql.ServerForCreate{ + Location: &location, + Tags: tags, + Properties: &psql.ServerPropertiesForReplica{ + SourceServerID: to.StringPtr(sourceserver), + CreateMode: psql.CreateModeReplica, + }, + }, + ) + } else { + result, err = client.Create( + ctx, + resourcegroup, + servername, + psql.ServerForCreate{ + Location: &location, + Tags: tags, + Properties: &psql.ServerPropertiesForDefaultCreate{ + AdministratorLogin: &adminlogin, + AdministratorLoginPassword: &adminpassword, + Version: serverversion, + SslEnforcement: sslenforcement, + CreateMode: psql.CreateModeServerPropertiesForCreate, + }, + Sku: &skuInfo, + }, + ) + } - future, err := client.Create( - ctx, - resourcegroup, - servername, - psql.ServerForCreate{ - Location: &location, - Tags: tags, - Properties: &psql.ServerPropertiesForDefaultCreate{ - AdministratorLogin: &adminlogin, - AdministratorLoginPassword: &adminpassword, - Version: serverversion, - SslEnforcement: sslenforcement, - //StorageProfile: &psql.StorageProfile{}, - CreateMode: psql.CreateModeServerPropertiesForCreate, - }, - Sku: &skuInfo, - }, - ) if err != nil { - return psql.Server{}, err + return "", psql.Server{}, err } - - return future.Result(client) + res, err := result.Result(client) + return result.PollingURL(), res, err } func (p *PSQLServerClient) DeleteServer(ctx context.Context, resourcegroup string, servername string) (status string, err error) { diff --git a/pkg/resourcemanager/psql/server/server_manager.go b/pkg/resourcemanager/psql/server/server_manager.go index 3fc3ad42f8c..9295242289e 100644 --- a/pkg/resourcemanager/psql/server/server_manager.go +++ b/pkg/resourcemanager/psql/server/server_manager.go @@ -26,7 +26,8 @@ type PostgreSQLServerManager interface { sslenforcement psql.SslEnforcementEnum, skuInfo psql.Sku, adminlogin string, - adminpassword string) (psql.Server, error) + adminpassword string, + createmode string, sourceserver string) (string, psql.Server, error) DeleteServer(ctx context.Context, resourcegroup string, diff --git a/pkg/resourcemanager/psql/server/server_reconcile.go b/pkg/resourcemanager/psql/server/server_reconcile.go index 68f4bc235b9..a0ed1f93f73 100644 --- a/pkg/resourcemanager/psql/server/server_reconcile.go +++ b/pkg/resourcemanager/psql/server/server_reconcile.go @@ -6,6 +6,7 @@ package server import ( "context" "fmt" + "strings" psql "github.com/Azure/azure-sdk-for-go/services/postgresql/mgmt/2017-12-01/postgresql" "github.com/Azure/azure-service-operator/api/v1alpha1" @@ -13,6 +14,7 @@ import ( "github.com/Azure/azure-service-operator/pkg/errhelp" "github.com/Azure/azure-service-operator/pkg/helpers" "github.com/Azure/azure-service-operator/pkg/resourcemanager" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/pollclient" "github.com/Azure/go-autorest/autorest/to" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/types" @@ -34,6 +36,19 @@ func (p *PSQLServerClient) Ensure(ctx context.Context, obj runtime.Object, opts return true, err } + createmode := string(psql.CreateModeDefault) + if len(instance.Spec.CreateMode) != 0 { + createmode = instance.Spec.CreateMode + } + + // If a replica is requested, ensure that source server is specified + if strings.EqualFold(createmode, string(psql.CreateModeReplica)) { + if len(instance.Spec.ReplicaProperties.SourceServerId) == 0 { + instance.Status.Message = "Replica requested but source server unspecified" + return true, nil + } + } + // Check to see if secret exists and if yes retrieve the admin login and password secret, err := p.GetOrPrepareSecret(ctx, instance) if err != nil { @@ -46,13 +61,20 @@ func (p *PSQLServerClient) Ensure(ctx context.Context, obj runtime.Object, opts return false, err } + hash := "" // if an error occurs thats ok as it means that it doesn't exist yet getServer, err := p.GetServer(ctx, instance.Spec.ResourceGroup, instance.Name) if err == nil { instance.Status.State = string(getServer.UserVisibleState) + hash = helpers.Hash256(instance.Spec) + if instance.Status.SpecHash == hash && (instance.Status.Provisioned || instance.Status.FailedProvisioning) { + instance.Status.RequestedAt = nil + return true, nil + } + // succeeded! so end reconcilliation successfully - if getServer.UserVisibleState == "Ready" { + if getServer.UserVisibleState == psql.ServerStateReady { // Update the secret with fully qualified server name. Ignore error as we have the admin creds which is critical. p.UpdateSecretWithFullServerName(ctx, instance.Name, secret, instance, *getServer.FullyQualifiedDomainName) @@ -61,12 +83,37 @@ func (p *PSQLServerClient) Ensure(ctx context.Context, obj runtime.Object, opts instance.Status.ResourceId = *getServer.ID instance.Status.Provisioned = true instance.Status.Provisioning = false + instance.Status.FailedProvisioning = false + instance.Status.SpecHash = hash return true, nil } // the database exists but has not provisioned yet - so keep waiting instance.Status.Message = "Postgres server exists but may not be ready" return false, nil + } else { + // handle failures in the async operation + if instance.Status.PollingURL != "" { + pClient := pollclient.NewPollClient() + res, err := pClient.Get(ctx, instance.Status.PollingURL) + if err != nil { + instance.Status.Provisioning = false + return false, err + } + + if res.Status == "Failed" { + instance.Status.Provisioning = false + instance.Status.RequestedAt = nil + ignore := []string{ + errhelp.SubscriptionDoesNotHaveServer, + errhelp.ServiceBusy, + } + if !helpers.ContainsString(ignore, res.Error.Code) { + instance.Status.Message = res.Error.Error() + return true, nil + } + } + } } // setup variables for create call @@ -84,7 +131,7 @@ func (p *PSQLServerClient) Ensure(ctx context.Context, obj runtime.Object, opts // create the server instance.Status.Provisioning = true instance.Status.FailedProvisioning = false - _, err = p.CreateServerIfValid( + pollURL, _, err := p.CreateServerIfValid( ctx, instance.Name, instance.Spec.ResourceGroup, @@ -95,6 +142,8 @@ func (p *PSQLServerClient) Ensure(ctx context.Context, obj runtime.Object, opts skuInfo, adminlogin, adminpassword, + createmode, + instance.Spec.ReplicaProperties.SourceServerId, ) if err != nil { instance.Status.Message = errhelp.StripErrorIDs(err) @@ -111,14 +160,20 @@ func (p *PSQLServerClient) Ensure(ctx context.Context, obj runtime.Object, opts errhelp.ParentNotFoundErrorCode, errhelp.NotFoundErrorCode, errhelp.ServiceBusy, + errhelp.InternalServerError, } // handle the errors if helpers.ContainsString(catchInProgress, azerr.Type) { + if azerr.Type == errhelp.AsyncOpIncompleteError { + instance.Status.PollingURL = pollURL + } instance.Status.Message = "Postgres server exists but may not be ready" instance.Status.Provisioning = true return false, nil - } else if helpers.ContainsString(catchKnownError, azerr.Type) { + } + + if helpers.ContainsString(catchKnownError, azerr.Type) { return false, nil } diff --git a/pkg/resourcemanager/psql/server/server_test.go b/pkg/resourcemanager/psql/server/server_test.go index 6533d8cbd7d..a99e178e934 100644 --- a/pkg/resourcemanager/psql/server/server_test.go +++ b/pkg/resourcemanager/psql/server/server_test.go @@ -64,7 +64,7 @@ var _ = Describe("PSQL server", func() { if err == nil { return true } - _, err = PSQLManager.CreateServerIfValid( + _, _, err = PSQLManager.CreateServerIfValid( ctx, psqlServer, rgName, @@ -75,6 +75,8 @@ var _ = Describe("PSQL server", func() { pSQLSku, "adm1nus3r", "m@#terU$3r", + "", + "", ) if err != nil { fmt.Println(err.Error()) diff --git a/pkg/resourcemanager/vmss/client.go b/pkg/resourcemanager/vmss/client.go new file mode 100644 index 00000000000..6976f46abca --- /dev/null +++ b/pkg/resourcemanager/vmss/client.go @@ -0,0 +1,249 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package vmss + +import ( + "context" + "fmt" + "strings" + + compute "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-10-01/compute" + azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/pkg/helpers" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/iam" + "github.com/Azure/azure-service-operator/pkg/secrets" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" +) + +type AzureVMScaleSetClient struct { + SecretClient secrets.SecretClient + Scheme *runtime.Scheme +} + +func NewAzureVMScaleSetClient(secretclient secrets.SecretClient, scheme *runtime.Scheme) *AzureVMScaleSetClient { + return &AzureVMScaleSetClient{ + SecretClient: secretclient, + Scheme: scheme, + } +} + +func getVMScaleSetClient() compute.VirtualMachineScaleSetsClient { + computeClient := compute.NewVirtualMachineScaleSetsClientWithBaseURI(config.BaseURI(), config.SubscriptionID()) + a, _ := iam.GetResourceManagementAuthorizer() + computeClient.Authorizer = a + computeClient.AddToUserAgent(config.UserAgent()) + return computeClient +} + +func (m *AzureVMScaleSetClient) CreateVMScaleSet(ctx context.Context, location string, resourceGroupName string, resourceName string, vmSize string, capacity int64, osType string, adminUserName string, adminPassword string, sshPublicKeyData string, platformImageURN string, vnetName string, subnetName string, loadBalancerName string, backendAddressPoolName string, inboundNatPoolName string) (future compute.VirtualMachineScaleSetsCreateOrUpdateFuture, err error) { + + client := getVMScaleSetClient() + + // Construct OS Profile + provisionVMAgent := true + platformImageUrnTokens := strings.Split(platformImageURN, ":") + + adminPasswordInput := "" + adminPasswordBase64Decoded := helpers.FromBase64EncodedString(adminPassword) + if adminPasswordBase64Decoded != "" { + adminPasswordInput = adminPasswordBase64Decoded + } + + sshKeyPath := fmt.Sprintf("/home/%s/.ssh/authorized_keys", adminUserName) + sshKeysToAdd := []compute.SSHPublicKey{ + compute.SSHPublicKey{ + Path: &sshKeyPath, + KeyData: &sshPublicKeyData, + }, + } + linuxProfile := compute.VirtualMachineScaleSetOSProfile{ + ComputerNamePrefix: &resourceName, + AdminUsername: &adminUserName, + AdminPassword: &adminPasswordInput, + LinuxConfiguration: &compute.LinuxConfiguration{ + SSH: &compute.SSHConfiguration{ + PublicKeys: &sshKeysToAdd, + }, + ProvisionVMAgent: &provisionVMAgent, + }, + } + + windowsProfile := compute.VirtualMachineScaleSetOSProfile{ + ComputerNamePrefix: &resourceName, + AdminUsername: &adminUserName, + AdminPassword: &adminPasswordInput, + WindowsConfiguration: &compute.WindowsConfiguration{ + ProvisionVMAgent: &provisionVMAgent, + }, + } + + osProfile := linuxProfile + if osType == "Windows" { + osProfile = windowsProfile + } + + // Construct Network Profile + vmssIPConfigName := resourceName + "-ipconfig" + publicIPAddressConfigurationName := resourceName + "-pip" + idleTimeoutInMinutes := int32(15) + subnetIDInput := helpers.MakeResourceID( + client.SubscriptionID, + resourceGroupName, + "Microsoft.Network", + "virtualNetworks", + vnetName, + "subnets", + subnetName, + ) + bePoolIDInput := helpers.MakeResourceID( + client.SubscriptionID, + resourceGroupName, + "Microsoft.Network", + "loadBalancers", + loadBalancerName, + "backendAddressPools", + backendAddressPoolName, + ) + natPoolIDInput := helpers.MakeResourceID( + client.SubscriptionID, + resourceGroupName, + "Microsoft.Network", + "loadBalancers", + loadBalancerName, + "inboundNatPools", + inboundNatPoolName, + ) + ipConfigs := []compute.VirtualMachineScaleSetIPConfiguration{ + compute.VirtualMachineScaleSetIPConfiguration{ + Name: &vmssIPConfigName, + VirtualMachineScaleSetIPConfigurationProperties: &compute.VirtualMachineScaleSetIPConfigurationProperties{ + Subnet: &compute.APIEntityReference{ + ID: &subnetIDInput, + }, + PublicIPAddressConfiguration: &compute.VirtualMachineScaleSetPublicIPAddressConfiguration{ + Name: &publicIPAddressConfigurationName, + VirtualMachineScaleSetPublicIPAddressConfigurationProperties: &compute.VirtualMachineScaleSetPublicIPAddressConfigurationProperties{ + IdleTimeoutInMinutes: &idleTimeoutInMinutes, + }, + }, + LoadBalancerBackendAddressPools: &[]compute.SubResource{ + compute.SubResource{ + ID: &bePoolIDInput, + }, + }, + LoadBalancerInboundNatPools: &[]compute.SubResource{ + compute.SubResource{ + ID: &natPoolIDInput, + }, + }, + }, + }, + } + + isPrimaryNic := true + nicConfigsToAdd := []compute.VirtualMachineScaleSetNetworkConfiguration{ + compute.VirtualMachineScaleSetNetworkConfiguration{ + Name: &resourceName, + VirtualMachineScaleSetNetworkConfigurationProperties: &compute.VirtualMachineScaleSetNetworkConfigurationProperties{ + Primary: &isPrimaryNic, + IPConfigurations: &ipConfigs, + }, + }, + } + + future, err = client.CreateOrUpdate( + ctx, + resourceGroupName, + resourceName, + compute.VirtualMachineScaleSet{ + Location: &location, + Sku: &compute.Sku{ + Name: &vmSize, + Capacity: &capacity, + }, + VirtualMachineScaleSetProperties: &compute.VirtualMachineScaleSetProperties{ + UpgradePolicy: &compute.UpgradePolicy{ + Mode: compute.Automatic, + }, + VirtualMachineProfile: &compute.VirtualMachineScaleSetVMProfile{ + StorageProfile: &compute.VirtualMachineScaleSetStorageProfile{ + OsDisk: &compute.VirtualMachineScaleSetOSDisk{ + Caching: compute.CachingTypesReadOnly, + CreateOption: compute.DiskCreateOptionTypesFromImage, + }, + ImageReference: &compute.ImageReference{ + Publisher: &platformImageUrnTokens[0], + Offer: &platformImageUrnTokens[1], + Sku: &platformImageUrnTokens[2], + Version: &platformImageUrnTokens[3], + }, + }, + OsProfile: &osProfile, + NetworkProfile: &compute.VirtualMachineScaleSetNetworkProfile{ + NetworkInterfaceConfigurations: &nicConfigsToAdd, + }, + }, + }, + }, + ) + + return future, err +} + +func (m *AzureVMScaleSetClient) DeleteVMScaleSet(ctx context.Context, vmssName string, resourcegroup string) (status string, err error) { + + client := getVMScaleSetClient() + + _, err = client.Get(ctx, resourcegroup, vmssName) + if err == nil { // vmss present, so go ahead and delete + future, err := client.Delete(ctx, resourcegroup, vmssName) + return future.Status(), err + } + // VM not present so return success anyway + return "VMSS not present", nil + +} + +func (m *AzureVMScaleSetClient) GetVMScaleSet(ctx context.Context, resourcegroup string, vmssName string) (vmss compute.VirtualMachineScaleSet, err error) { + + client := getVMScaleSetClient() + return client.Get(ctx, resourcegroup, vmssName) +} + +func (p *AzureVMScaleSetClient) AddVMScaleSetCredsToSecrets(ctx context.Context, secretName string, data map[string][]byte, instance *azurev1alpha1.AzureVMScaleSet) error { + key := types.NamespacedName{ + Name: secretName, + Namespace: instance.Namespace, + } + + err := p.SecretClient.Upsert(ctx, + key, + data, + secrets.WithOwner(instance), + secrets.WithScheme(p.Scheme), + ) + if err != nil { + return err + } + + return nil +} + +func (p *AzureVMScaleSetClient) GetOrPrepareSecret(ctx context.Context, instance *azurev1alpha1.AzureVMScaleSet) (map[string][]byte, error) { + name := instance.Name + + secret := map[string][]byte{} + + key := types.NamespacedName{Name: name, Namespace: instance.Namespace} + if stored, err := p.SecretClient.Get(ctx, key); err == nil { + return stored, nil + } + + randomPassword := helpers.NewPassword() + secret["password"] = []byte(randomPassword) + + return secret, nil +} diff --git a/pkg/resourcemanager/vmss/manager.go b/pkg/resourcemanager/vmss/manager.go new file mode 100644 index 00000000000..9e3b8a5f36b --- /dev/null +++ b/pkg/resourcemanager/vmss/manager.go @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package vmss + +import ( + "context" + + compute "github.com/Azure/azure-sdk-for-go/services/compute/mgmt/2018-10-01/compute" + "github.com/Azure/azure-service-operator/pkg/resourcemanager" +) + +type VMScaleSetManager interface { + CreateVMScaleSet(ctx context.Context, + location string, + resourceGroupName string, + resourceName string, + vmSize string, + osType string, + adminUserName string, + adminPassword string, + sshPublicKeyData string, + networkInterfaceName string, + platformImageURN string, + loadBalancerName string) (compute.VirtualMachineScaleSet, error) + + DeleteVMScaleSet(ctx context.Context, + resourceName string, + resourceGroupName string) (string, error) + + GetVMScaleSet(ctx context.Context, + resourceGroupName string, + resourceName string) (compute.VirtualMachineScaleSet, error) + + // also embed async client methods + resourcemanager.ARMClient +} diff --git a/pkg/resourcemanager/vmss/reconcile.go b/pkg/resourcemanager/vmss/reconcile.go new file mode 100644 index 00000000000..1a44a55eb5c --- /dev/null +++ b/pkg/resourcemanager/vmss/reconcile.go @@ -0,0 +1,214 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package vmss + +import ( + "context" + "fmt" + + azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/pkg/errhelp" + "github.com/Azure/azure-service-operator/pkg/helpers" + "github.com/Azure/azure-service-operator/pkg/resourcemanager" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" +) + +func (g *AzureVMScaleSetClient) Ensure(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { + + instance, err := g.convert(obj) + if err != nil { + return true, err + } + + client := getVMScaleSetClient() + + location := instance.Spec.Location + resourceGroup := instance.Spec.ResourceGroup + resourceName := instance.Name + vmSize := instance.Spec.VMSize + capacity := int64(instance.Spec.Capacity) + osType := instance.Spec.OSType + adminUserName := instance.Spec.AdminUserName + sshPublicKeyData := instance.Spec.SSHPublicKeyData + imageURN := instance.Spec.PlatformImageURN + vnetName := instance.Spec.VirtualNetworkName + subnetName := instance.Spec.SubnetName + loadBalancerName := instance.Spec.LoadBalancerName + bePoolName := instance.Spec.BackendAddressPoolName + natPoolName := instance.Spec.InboundNatPoolName + + // Check to see if secret exists and if yes retrieve the admin login and password + secret, err := g.GetOrPrepareSecret(ctx, instance) + if err != nil { + return false, err + } + // Update secret + err = g.AddVMScaleSetCredsToSecrets(ctx, instance.Name, secret, instance) + if err != nil { + return false, err + } + + adminPassword := string(secret["password"]) + + instance.Status.Provisioning = true + // Check if this item already exists. This is required + // to overcome the issue with the lack of idempotence of the Create call + item, err := g.GetVMScaleSet(ctx, resourceGroup, resourceName) + if err == nil { + instance.Status.Provisioned = true + instance.Status.Provisioning = false + instance.Status.Message = resourcemanager.SuccessMsg + instance.Status.ResourceId = *item.ID + return true, nil + } + + future, err := g.CreateVMScaleSet( + ctx, + location, + resourceGroup, + resourceName, + vmSize, + capacity, + string(osType), + adminUserName, + adminPassword, + sshPublicKeyData, + imageURN, + vnetName, + subnetName, + loadBalancerName, + bePoolName, + natPoolName, + ) + if err != nil { + // let the user know what happened + instance.Status.Message = err.Error() + instance.Status.Provisioning = false + // errors we expect might happen that we are ok with waiting for + catch := []string{ + errhelp.ResourceGroupNotFoundErrorCode, + errhelp.ParentNotFoundErrorCode, + errhelp.NotFoundErrorCode, + errhelp.AsyncOpIncompleteError, + errhelp.ResourceNotFound, + errhelp.InvalidResourceReference, + } + + azerr := errhelp.NewAzureErrorAzureError(err) + if helpers.ContainsString(catch, azerr.Type) { + // most of these error technically mean the resource is actually not provisioning + switch azerr.Type { + case errhelp.AsyncOpIncompleteError: + instance.Status.Provisioning = true + } + // reconciliation is not done but error is acceptable + return false, nil + } + // reconciliation not done and we don't know what happened + return false, err + } + + _, err = future.Result(client) + if err != nil { + // let the user know what happened + instance.Status.Message = err.Error() + instance.Status.Provisioning = false + // errors we expect might happen that we are ok with waiting for + catch := []string{ + errhelp.ResourceGroupNotFoundErrorCode, + errhelp.ParentNotFoundErrorCode, + errhelp.NotFoundErrorCode, + errhelp.AsyncOpIncompleteError, + errhelp.SubscriptionDoesNotHaveServer, + } + + azerr := errhelp.NewAzureErrorAzureError(err) + if helpers.ContainsString(catch, azerr.Type) { + // most of these error technically mean the resource is actually not provisioning + switch azerr.Type { + case errhelp.AsyncOpIncompleteError: + instance.Status.Provisioning = true + } + // reconciliation is not done but error is acceptable + return false, nil + } + // reconciliation not done and we don't know what happened + return false, err + } + + if instance.Status.Provisioning { + instance.Status.Provisioned = true + instance.Status.Provisioning = false + instance.Status.Message = resourcemanager.SuccessMsg + } else { + instance.Status.Provisioned = false + instance.Status.Provisioning = true + } + return true, nil +} + +func (g *AzureVMScaleSetClient) Delete(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { + + instance, err := g.convert(obj) + if err != nil { + return true, err + } + + resourceGroup := instance.Spec.ResourceGroup + resourceName := instance.Name + + status, err := g.DeleteVMScaleSet( + ctx, + resourceName, + resourceGroup, + ) + if err != nil { + if !errhelp.IsAsynchronousOperationNotComplete(err) { + return true, err + } + } + + if err == nil { + if status != "InProgress" { + return false, nil + } + } + + return true, nil +} +func (g *AzureVMScaleSetClient) GetParents(obj runtime.Object) ([]resourcemanager.KubeParent, error) { + + instance, err := g.convert(obj) + if err != nil { + return nil, err + } + + return []resourcemanager.KubeParent{ + { + Key: types.NamespacedName{ + Namespace: instance.Namespace, + Name: instance.Spec.ResourceGroup, + }, + Target: &azurev1alpha1.ResourceGroup{}, + }, + }, nil +} + +func (g *AzureVMScaleSetClient) GetStatus(obj runtime.Object) (*azurev1alpha1.ASOStatus, error) { + + instance, err := g.convert(obj) + if err != nil { + return nil, err + } + return &instance.Status, nil +} + +func (g *AzureVMScaleSetClient) convert(obj runtime.Object) (*azurev1alpha1.AzureVMScaleSet, error) { + local, ok := obj.(*azurev1alpha1.AzureVMScaleSet) + if !ok { + return nil, fmt.Errorf("failed type assertion on kind: %s", obj.GetObjectKind().GroupVersionKind().String()) + } + return local, nil +}