diff --git a/PROJECT b/PROJECT index 06ecad2aa35..84c22ce98dd 100644 --- a/PROJECT +++ b/PROJECT @@ -120,8 +120,14 @@ resources: version: v1alpha2 kind: MySQLServer - group: azure + kind: RedisCacheFirewallRule version: v1alpha1 +- group: azure kind: RedisCacheAction + version: v1alpha1 +- group: azure + kind: AzureVirtualMachineExtension + version: v1alpha1 - group: azure kind: AzureVirtualMachineExtension version: v1alpha1 \ No newline at end of file diff --git a/api/v1alpha1/rediscachefirewallrule_types.go b/api/v1alpha1/rediscachefirewallrule_types.go new file mode 100644 index 00000000000..c5047cc4afe --- /dev/null +++ b/api/v1alpha1/rediscachefirewallrule_types.go @@ -0,0 +1,54 @@ +// 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. + +// RedisCacheFirewallRuleSpec defines the desired state of RedisCacheFirewallRule +type RedisCacheFirewallRuleSpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + ResourceGroup string `json:"resourceGroup"` + CacheName string `json:"redisCache"` + Properties RedisCacheFirewallRuleProperties `json:"properties"` +} + +// RedisCacheFirewallRuleProperties the parameters of the RedisCacheFirewallRule +type RedisCacheFirewallRuleProperties struct { + StartIP string `json:"startIP"` + EndIP string `json:"endIP"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// RedisCacheFirewallRule is the Schema for the rediscachefirewallrules API +// +kubebuilder:printcolumn:name="Provisioned",type="string",JSONPath=".status.provisioned" +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.message" +type RedisCacheFirewallRule struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec RedisCacheFirewallRuleSpec `json:"spec,omitempty"` + Status ASOStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// RedisCacheFirewallRuleList contains a list of RedisCacheFirewallRule +type RedisCacheFirewallRuleList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []RedisCacheFirewallRule `json:"items"` +} + +func init() { + SchemeBuilder.Register(&RedisCacheFirewallRule{}, &RedisCacheFirewallRuleList{}) +} diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index 933ef459fa4..a84cebabf62 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -3065,6 +3065,96 @@ func (in *RedisCacheActionSpec) DeepCopy() *RedisCacheActionSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisCacheFirewallRule) DeepCopyInto(out *RedisCacheFirewallRule) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisCacheFirewallRule. +func (in *RedisCacheFirewallRule) DeepCopy() *RedisCacheFirewallRule { + if in == nil { + return nil + } + out := new(RedisCacheFirewallRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RedisCacheFirewallRule) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisCacheFirewallRuleList) DeepCopyInto(out *RedisCacheFirewallRuleList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]RedisCacheFirewallRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisCacheFirewallRuleList. +func (in *RedisCacheFirewallRuleList) DeepCopy() *RedisCacheFirewallRuleList { + if in == nil { + return nil + } + out := new(RedisCacheFirewallRuleList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *RedisCacheFirewallRuleList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisCacheFirewallRuleProperties) DeepCopyInto(out *RedisCacheFirewallRuleProperties) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisCacheFirewallRuleProperties. +func (in *RedisCacheFirewallRuleProperties) DeepCopy() *RedisCacheFirewallRuleProperties { + if in == nil { + return nil + } + out := new(RedisCacheFirewallRuleProperties) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RedisCacheFirewallRuleSpec) DeepCopyInto(out *RedisCacheFirewallRuleSpec) { + *out = *in + out.Properties = in.Properties +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RedisCacheFirewallRuleSpec. +func (in *RedisCacheFirewallRuleSpec) DeepCopy() *RedisCacheFirewallRuleSpec { + if in == nil { + return nil + } + out := new(RedisCacheFirewallRuleSpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *RedisCacheList) DeepCopyInto(out *RedisCacheList) { *out = *in diff --git a/charts/azure-service-operator-0.1.0.tgz b/charts/azure-service-operator-0.1.0.tgz index 1d4cf361965..afcd4abb0c1 100644 Binary files a/charts/azure-service-operator-0.1.0.tgz and b/charts/azure-service-operator-0.1.0.tgz differ diff --git a/charts/index.yaml b/charts/index.yaml index acd592bfa5f..6a1efbfd9e2 100644 --- a/charts/index.yaml +++ b/charts/index.yaml @@ -3,14 +3,14 @@ entries: azure-service-operator: - apiVersion: v2 appVersion: 0.1.0 - created: "2020-05-18T22:30:28.349199-06:00" + created: "2020-05-20T10:20:46.240488-06:00" dependencies: - condition: azureUseMI name: aad-pod-identity repository: https://raw.githubusercontent.com/Azure/aad-pod-identity/master/charts version: 1.5.5 description: Deploy components and dependencies of azure-service-operator - digest: e5aaaa1c15802982f7a943e737b2ae39a2404d395fec299376fb19e15fc9e6f2 + digest: f381ed2be22005190cffc85883f42a2f13512d7b789ba8a03d8ee4cf06235241 home: https://github.com/Azure/azure-service-operator name: azure-service-operator sources: @@ -18,4 +18,4 @@ entries: urls: - azure-service-operator-0.1.0.tgz version: 0.1.0 -generated: "2020-05-18T22:30:28.343398-06:00" +generated: "2020-05-20T10:20:46.237192-06:00" diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 3519284ee37..55988cf27b4 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -39,6 +39,7 @@ resources: - bases/azure.microsoft.com_azureloadbalancers.yaml - bases/azure.microsoft.com_azurevmscalesets.yaml - bases/azure.microsoft.com_rediscacheactions.yaml +- bases/azure.microsoft.com_rediscachefirewallrules.yaml - bases/azure.microsoft.com_azurevirtualmachineextensions.yaml # +kubebuilder:scaffold:crdkustomizeresource @@ -78,6 +79,7 @@ patches: #- patches/webhook_in_azureloadbalancers.yaml #- patches/webhook_in_azurevmscalesets.yaml #- patches/webhook_in_rediscacheactions.yaml +#- patches/webhook_in_rediscachefirewallrules.yaml #- patches/webhook_in_azurevirtualmachineextensions.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch @@ -116,6 +118,7 @@ patches: #- patches/cainjection_in_azureloadbalancers.yaml #- patches/cainjection_in_azurevmscalesets.yaml #- patches/cainjection_in_rediscacheactions.yaml +#- patches/cainjection_in_rediscachefirewallrules.yaml #- patches/cainjection_in_azurevirtualmachineextensions.yaml # +kubebuilder:scaffold:crdkustomizecainjectionpatch diff --git a/config/crd/patches/cainjection_in_rediscachefirewallrules.yaml b/config/crd/patches/cainjection_in_rediscachefirewallrules.yaml new file mode 100644 index 00000000000..59bed0bc026 --- /dev/null +++ b/config/crd/patches/cainjection_in_rediscachefirewallrules.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: + cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) + name: rediscachefirewallrules.azure.microsoft.com diff --git a/config/crd/patches/webhook_in_rediscachefirewallrules.yaml b/config/crd/patches/webhook_in_rediscachefirewallrules.yaml new file mode 100644 index 00000000000..58fedb6b6ad --- /dev/null +++ b/config/crd/patches/webhook_in_rediscachefirewallrules.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: rediscachefirewallrules.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/rbac/rediscachefirewallrule_editor_role.yaml b/config/rbac/rediscachefirewallrule_editor_role.yaml new file mode 100644 index 00000000000..b463515e218 --- /dev/null +++ b/config/rbac/rediscachefirewallrule_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit rediscachefirewallrules. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: rediscachefirewallrule-editor-role +rules: +- apiGroups: + - azure.microsoft.com + resources: + - rediscachefirewallrules + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - azure.microsoft.com + resources: + - rediscachefirewallrules/status + verbs: + - get diff --git a/config/rbac/rediscachefirewallrule_viewer_role.yaml b/config/rbac/rediscachefirewallrule_viewer_role.yaml new file mode 100644 index 00000000000..5359ccb012e --- /dev/null +++ b/config/rbac/rediscachefirewallrule_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view rediscachefirewallrules. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: rediscachefirewallrule-viewer-role +rules: +- apiGroups: + - azure.microsoft.com + resources: + - rediscachefirewallrules + verbs: + - get + - list + - watch +- apiGroups: + - azure.microsoft.com + resources: + - rediscachefirewallrules/status + verbs: + - get diff --git a/config/samples/azure_v1alpha1_rediscachefirewallrule.yaml b/config/samples/azure_v1alpha1_rediscachefirewallrule.yaml new file mode 100644 index 00000000000..aa44f605f55 --- /dev/null +++ b/config/samples/azure_v1alpha1_rediscachefirewallrule.yaml @@ -0,0 +1,11 @@ +apiVersion: azure.microsoft.com/v1alpha1 +kind: RedisCacheFirewallRule +metadata: + name: rediscachefirewallrule +spec: + resourceGroup: resourcegroup-azure-operators + redisCache: rediscache-sample-1 + properties: + # this IP range enables Azure Service access + startIP: 0.0.0.0 + endIP: 0.0.0.0 \ No newline at end of file diff --git a/config/samples/azure_v1alpha2_mysqlserver.yaml b/config/samples/azure_v1alpha2_mysqlserver.yaml index 676cbde19da..720c7592d7a 100644 --- a/config/samples/azure_v1alpha2_mysqlserver.yaml +++ b/config/samples/azure_v1alpha2_mysqlserver.yaml @@ -2,6 +2,9 @@ apiVersion: azure.microsoft.com/v1alpha2 kind: MySQLServer metadata: name: mysqlserver-sample + labels: # Provide tags to add to the KeyVault as labels + tag1: value1 + tag2: value2 spec: location: eastus2 resourceGroup: resourcegroup-azure-operators diff --git a/config/samples/azure_v1alpha2_postgresqlserver.yaml b/config/samples/azure_v1alpha2_postgresqlserver.yaml index a0c4a3d3a0d..67f117de7f4 100644 --- a/config/samples/azure_v1alpha2_postgresqlserver.yaml +++ b/config/samples/azure_v1alpha2_postgresqlserver.yaml @@ -2,6 +2,9 @@ apiVersion: azure.microsoft.com/v1alpha2 kind: PostgreSQLServer metadata: name: postgresqlserver-sample + labels: # Provide tags to add to the KeyVault as labels + tag1: value1 + tag2: value2 spec: location: eastus resourceGroup: resourcegroup-azure-operators diff --git a/controllers/helpers.go b/controllers/helpers.go index b4ea2174bba..734534e4fd8 100644 --- a/controllers/helpers.go +++ b/controllers/helpers.go @@ -40,7 +40,7 @@ import ( resourcemanagerpsqldatabase "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/database" resourcemanagerpsqlfirewallrule "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/firewallrule" resourcemanagerpsqlserver "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/server" - resourcemanagerrediscaches "github.com/Azure/azure-service-operator/pkg/resourcemanager/rediscaches" + resourcemanagerrediscaches "github.com/Azure/azure-service-operator/pkg/resourcemanager/rediscaches/redis" resourcegroupsresourcemanager "github.com/Azure/azure-service-operator/pkg/resourcemanager/resourcegroups" resourcemanagerstorages "github.com/Azure/azure-service-operator/pkg/resourcemanager/storages" ) diff --git a/controllers/rediscache_controller_test.go b/controllers/rediscache_controller_test.go index 1be7f6b5e0c..cdd352c8544 100644 --- a/controllers/rediscache_controller_test.go +++ b/controllers/rediscache_controller_test.go @@ -7,20 +7,13 @@ package controllers import ( "context" - "fmt" - "log" - "strings" "testing" "time" azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" - "github.com/Azure/azure-service-operator/pkg/helpers" "github.com/stretchr/testify/assert" - v1 "k8s.io/api/core/v1" - apierrors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - "k8s.io/apimachinery/pkg/types" ) const longRunningTimeout = 25 * time.Minute @@ -36,7 +29,7 @@ func TestRedisCacheControllerHappyPath(t *testing.T) { var redisCacheName string var err error - rgName = tc.resourceGroupName + rgName = tc.resourceGroup rgLocation = tc.resourceGroupLocation redisCacheName = GenerateTestResourceNameWithRandom("rediscache", 10) @@ -47,8 +40,8 @@ func TestRedisCacheControllerHappyPath(t *testing.T) { Namespace: "default", }, Spec: azurev1alpha1.RedisCacheSpec{ - Location: rgLocation, - ResourceGroupName: rgName, + Location: rgLocation, + ResourceGroup: rgName, Properties: azurev1alpha1.RedisCacheProperties{ Sku: azurev1alpha1.RedisCacheSku{ Name: "Basic", diff --git a/controllers/rediscachefirewallrule_controller.go b/controllers/rediscachefirewallrule_controller.go new file mode 100644 index 00000000000..5ea568bde12 --- /dev/null +++ b/controllers/rediscachefirewallrule_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" +) + +// RedisCacheFirewallRuleReconciler reconciles a RedisCacheFirewallRule object +type RedisCacheFirewallRuleReconciler struct { + Reconciler *AsyncReconciler +} + +// +kubebuilder:rbac:groups=azure.microsoft.com,resources=rediscachefirewallrules,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=azure.microsoft.com,resources=rediscachefirewallrules/status,verbs=get;update;patch + +func (r *RedisCacheFirewallRuleReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + return r.Reconciler.Reconcile(req, &azurev1alpha1.RedisCacheFirewallRule{}) +} + +func (r *RedisCacheFirewallRuleReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&azurev1alpha1.RedisCacheFirewallRule{}). + Complete(r) +} diff --git a/controllers/rediscachefirewallrule_controller_test.go b/controllers/rediscachefirewallrule_controller_test.go new file mode 100644 index 00000000000..802a53aefa1 --- /dev/null +++ b/controllers/rediscachefirewallrule_controller_test.go @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// +build all resourcegroup + +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 TestRedisCacheFirewallRuleControllerNoResourceGroup(t *testing.T) { + t.Parallel() + defer PanicRecover(t) + ctx := context.Background() + + var rgName string + var redisCache string + var redisCacheFirewallRule string + + rgName = GenerateTestResourceNameWithRandom("rcfwr-rg", 10) + redisCache = GenerateTestResourceNameWithRandom("rediscache", 10) + redisCacheFirewallRule = GenerateTestResourceNameWithRandom("rediscachefirewallrule", 10) + + redisCacheFirewallRuleInstance := &azurev1alpha1.RedisCacheFirewallRule{ + ObjectMeta: metav1.ObjectMeta{ + Name: redisCacheFirewallRule, + Namespace: "default", + }, + Spec: azurev1alpha1.RedisCacheFirewallRuleSpec{ + ResourceGroup: rgName, + CacheName: redisCache, + Properties: azurev1alpha1.RedisCacheFirewallRuleProperties{ + StartIP: "0.0.0.0", + EndIP: "0.0.0.0", + }, + }, + } + + EnsureInstanceWithResult(ctx, t, tc, redisCacheFirewallRuleInstance, errhelp.ResourceGroupNotFoundErrorCode, false) + EnsureDelete(ctx, t, tc, redisCacheFirewallRuleInstance) +} + +func TestRedisCacheFirewallRuleNoRedisCache(t *testing.T) { + t.Parallel() + defer PanicRecover(t) + ctx := context.Background() + + var rgName string + var redisCache string + var redisCacheFirewallRule string + + rgName = tc.resourceGroupName + redisCache = GenerateTestResourceNameWithRandom("rediscache", 10) + redisCacheFirewallRule = GenerateTestResourceNameWithRandom("rediscachefirewallrule", 10) + + redisCacheFirewallRuleInstance := &azurev1alpha1.RedisCacheFirewallRule{ + ObjectMeta: metav1.ObjectMeta{ + Name: redisCacheFirewallRule, + Namespace: "default", + }, + Spec: azurev1alpha1.RedisCacheFirewallRuleSpec{ + ResourceGroup: rgName, + CacheName: redisCache, + Properties: azurev1alpha1.RedisCacheFirewallRuleProperties{ + StartIP: "0.0.0.0", + EndIP: "0.0.0.0", + }, + }, + } + EnsureInstanceWithResult(ctx, t, tc, redisCacheFirewallRuleInstance, errhelp.ResourceNotFound, false) + EnsureDelete(ctx, t, tc, redisCacheFirewallRuleInstance) +} diff --git a/controllers/suite_test.go b/controllers/suite_test.go index 0f72d8d256b..e709a4f3202 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -17,9 +17,10 @@ import ( kscheme "k8s.io/client-go/kubernetes/scheme" - k8sSecrets "github.com/Azure/azure-service-operator/pkg/secrets/kube" "k8s.io/client-go/rest" + k8sSecrets "github.com/Azure/azure-service-operator/pkg/secrets/kube" + resourcemanagerapimgmt "github.com/Azure/azure-service-operator/pkg/resourcemanager/apim/apimgmt" resourcemanagerappinsights "github.com/Azure/azure-service-operator/pkg/resourcemanager/appinsights" resourcemanagersqlaction "github.com/Azure/azure-service-operator/pkg/resourcemanager/azuresql/azuresqlaction" @@ -44,7 +45,8 @@ import ( resourcemanagerpsqldatabase "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/database" resourcemanagerpsqlfirewallrule "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/firewallrule" resourcemanagerpsqlserver "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/server" - resourcemanagerrediscaches "github.com/Azure/azure-service-operator/pkg/resourcemanager/rediscaches" + rcfwr "github.com/Azure/azure-service-operator/pkg/resourcemanager/rediscaches/firewallrule" + resourcemanagerrediscaches "github.com/Azure/azure-service-operator/pkg/resourcemanager/rediscaches/redis" resourcegroupsresourcemanager "github.com/Azure/azure-service-operator/pkg/resourcemanager/resourcegroups" resourcemanagerblobcontainer "github.com/Azure/azure-service-operator/pkg/resourcemanager/storages/blobcontainer" resourcemanagerstorageaccount "github.com/Azure/azure-service-operator/pkg/resourcemanager/storages/storageaccount" @@ -54,13 +56,14 @@ import ( resourcemanagervnet "github.com/Azure/azure-service-operator/pkg/resourcemanager/vnet" telemetry "github.com/Azure/azure-service-operator/pkg/telemetry" - azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" - "github.com/Azure/azure-service-operator/api/v1alpha2" - "github.com/Azure/azure-service-operator/api/v1beta1" "k8s.io/client-go/kubernetes/scheme" ctrl "sigs.k8s.io/controller-runtime" "sigs.k8s.io/controller-runtime/pkg/client" "sigs.k8s.io/controller-runtime/pkg/envtest" + + azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/api/v1alpha2" + "github.com/Azure/azure-service-operator/api/v1beta1" // +kubebuilder:scaffold:imports ) @@ -284,6 +287,22 @@ func setup() error { return err } + err = (&RedisCacheFirewallRuleReconciler{ + Reconciler: &AsyncReconciler{ + Client: k8sManager.GetClient(), + AzureClient: rcfwr.NewAzureRedisCacheFirewallRuleManager(), + Telemetry: telemetry.InitializeTelemetryDefault( + "RedisCacheFirewallRule", + ctrl.Log.WithName("controllers").WithName("RedisCacheFirewallRule"), + ), + Recorder: k8sManager.GetEventRecorderFor("RedisCacheFirewallRule-controller"), + Scheme: k8sManager.GetScheme(), + }, + }).SetupWithManager(k8sManager) + if err != nil { + return err + } + err = (&EventhubNamespaceReconciler{ Reconciler: &AsyncReconciler{ Client: k8sManager.GetClient(), diff --git a/docs/services/rediscache/rediscache.md b/docs/services/rediscache/rediscache.md index bb7e1d3b4cf..34673a400cd 100644 --- a/docs/services/rediscache/rediscache.md +++ b/docs/services/rediscache/rediscache.md @@ -1,7 +1,13 @@ # Redis Cache Operator -This operator deploys an Azure Cache for Redis into a specified resource group at the specified location. +## Resources Supported +The RedisCache operator suite consists of the following operators. + +1. Redis Cache - Deploys an Azure Cache for redis into a specified resource group at the specified location +2. Redis Cache Firewall Rule - Deploys a firewall rule to allow access to the RedisCache from the specified IP range + +### RedisCache Learn more about Azure Cache for Redis [here](https://docs.microsoft.com/en-us/azure/azure-cache-for-redis/cache-overview). Here is a [sample YAML](/config/samples/azure_v1alpha1_rediscache.yaml) to provision an Azure Cache for Redis. @@ -21,7 +27,7 @@ The spec is comprised of the following fields: - SecretName - KeyVaultToStoreSecrets -### Required Fields +#### Required Fields A Redis Cache needs the following fields to deploy, along with a location and resource group. @@ -30,7 +36,7 @@ A Redis Cache needs the following fields to deploy, along with a location and re * `Properties.SKU.Capacity` Set the desired capacity * `EnableNonSslPort` defaults to True -### Optional Fields +#### Optional Fields * `SecretName` specify the name of the secret. If none is given, it will fall back to the name of the redis cache. * `KeyVaultToStoreSecrets` specify a Key Vault to store primary and secondary credentials in. If none is given, it will default to storing credentials as a Kube Secret. @@ -38,13 +44,24 @@ A Redis Cache needs the following fields to deploy, along with a location and re * `Properties.StaticIP` specify a statis IP for the Redis Cache * `Properties.Configuration` specify configuration values as key value pairs for the Redis Cache -### Secrets +#### Secrets After creating an Azure Cache for Redis instance, the operator stores a JSON formatted secret with the following fields. For more details on where the secrets are stored, look [here](/docs/secrets.md). * `primaryKey` * `secondaryKey` +## RedisCache firewall rule + +The RedisCache firewall rule allows you to add a firewall rule to RedisCache. + +Here is a [sample YAML](https://github.com/Azure/azure-service-operator/blob/master/config/samples/azure_v1alpha1_rediscachefirewallrule.yaml) for RedisCache firewall rule + +The `redisCache` indicates the RedisCache on which you want to configure the new RedisCache firewall rule on and `resourceGroup` is the resource group of the RedisCache. The `startIP` and `endIP` under Properties indicates the IP range of sources to allow access to the RedisCache. + +_Note:_ When the `startIP` and `endIP` are 0.0.0.0, it denotes a special case that adds a firewall rule to allow all Azure services to access the RedisCache. + + ## 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 7f532aee160..e8e667805c1 100644 --- a/main.go +++ b/main.go @@ -9,9 +9,10 @@ import ( "k8s.io/apimachinery/pkg/runtime" - "github.com/Azure/azure-service-operator/controllers" "github.com/Azure/go-autorest/autorest/azure/auth" + "github.com/Azure/azure-service-operator/controllers" + kscheme "k8s.io/client-go/kubernetes/scheme" _ "k8s.io/client-go/plugin/pkg/client/auth/gcp" ctrl "sigs.k8s.io/controller-runtime" @@ -48,8 +49,9 @@ import ( psqlfirewallrule "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/firewallrule" psqlserver "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/server" psqlvnetrule "github.com/Azure/azure-service-operator/pkg/resourcemanager/psql/vnetrule" - rediscache "github.com/Azure/azure-service-operator/pkg/resourcemanager/rediscaches" rediscacheactions "github.com/Azure/azure-service-operator/pkg/resourcemanager/rediscaches/actions" + rcfwr "github.com/Azure/azure-service-operator/pkg/resourcemanager/rediscaches/firewallrule" + rediscache "github.com/Azure/azure-service-operator/pkg/resourcemanager/rediscaches/redis" resourcemanagerresourcegroup "github.com/Azure/azure-service-operator/pkg/resourcemanager/resourcegroups" blobContainerManager "github.com/Azure/azure-service-operator/pkg/resourcemanager/storages/blobcontainer" storageaccountManager "github.com/Azure/azure-service-operator/pkg/resourcemanager/storages/storageaccount" @@ -147,6 +149,8 @@ func main() { secretClient, scheme, ) + + redisCacheFirewallRuleManager := rcfwr.NewAzureRedisCacheFirewallRuleManager() appInsightsManager := resourcemanagerappinsights.NewManager( secretClient, scheme, @@ -263,6 +267,22 @@ func main() { os.Exit(1) } + if err = (&controllers.RedisCacheFirewallRuleReconciler{ + Reconciler: &controllers.AsyncReconciler{ + Client: mgr.GetClient(), + AzureClient: redisCacheFirewallRuleManager, + Telemetry: telemetry.InitializeTelemetryDefault( + "RedisCacheFirewallRule", + ctrl.Log.WithName("controllers").WithName("RedisCacheFirewallRule"), + ), + Recorder: mgr.GetEventRecorderFor("RedisCacheFirewallRule-controller"), + Scheme: scheme, + }, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "RedisCacheFirewallRule") + os.Exit(1) + } + err = (&controllers.EventhubReconciler{ Reconciler: &controllers.AsyncReconciler{ Client: mgr.GetClient(), diff --git a/pkg/resourcemanager/mysql/server/client.go b/pkg/resourcemanager/mysql/server/client.go index 933ac600609..f32e147bc73 100644 --- a/pkg/resourcemanager/mysql/server/client.go +++ b/pkg/resourcemanager/mysql/server/client.go @@ -5,7 +5,6 @@ package server import ( "context" - "strings" mysql "github.com/Azure/azure-sdk-for-go/services/mysql/mgmt/2017-12-01/mysql" "github.com/Azure/azure-service-operator/api/v1alpha2" @@ -62,7 +61,7 @@ func (m *MySQLServerClient) CheckServerNameAvailability(ctx context.Context, ser } -func (m *MySQLServerClient) CreateServerIfValid(ctx context.Context, instance v1alpha2.MySQLServer, tags map[string]*string, skuInfo mysql.Sku, adminlogin string, adminpassword string, createmode string) (pollingURL string, server mysql.Server, err error) { +func (m *MySQLServerClient) CreateServerIfValid(ctx context.Context, instance v1alpha2.MySQLServer, tags map[string]*string, skuInfo mysql.Sku, adminlogin string, adminpassword string, createmode mysql.CreateMode, hash string) (pollingURL string, server mysql.Server, err error) { client := getMySQLServersClient() @@ -72,7 +71,6 @@ func (m *MySQLServerClient) CreateServerIfValid(ctx context.Context, instance v1 return "", server, err } - var result mysql.ServersCreateFuture var serverProperties mysql.BasicServerPropertiesForCreate var skuData *mysql.Sku var storageProfile *mysql.StorageProfile @@ -81,7 +79,7 @@ func (m *MySQLServerClient) CreateServerIfValid(ctx context.Context, instance v1 storageProfile = &obj } - if strings.EqualFold(createmode, "replica") { + if createmode == mysql.CreateModeReplica { serverProperties = &mysql.ServerPropertiesForReplica{ SourceServerID: to.StringPtr(instance.Spec.ReplicaProperties.SourceServerId), CreateMode: mysql.CreateModeReplica, @@ -100,20 +98,49 @@ func (m *MySQLServerClient) CreateServerIfValid(ctx context.Context, instance v1 skuData = &skuInfo } - result, _ = client.Create( - ctx, - instance.Spec.ResourceGroup, - instance.Name, - mysql.ServerForCreate{ - Location: &instance.Spec.Location, - Tags: tags, - Properties: serverProperties, - Sku: skuData, - }, - ) - - res, err := result.Result(client) - return result.PollingURL(), res, err + if hash != instance.Status.SpecHash && instance.Status.SpecHash != "" { + res, err := client.Update( + ctx, + instance.Spec.ResourceGroup, + instance.Name, + mysql.ServerUpdateParameters{ + Tags: tags, + Sku: skuData, + ServerUpdateParametersProperties: &mysql.ServerUpdateParametersProperties{ + StorageProfile: storageProfile, + Version: mysql.ServerVersion(instance.Spec.ServerVersion), + SslEnforcement: mysql.SslEnforcementEnum(instance.Spec.SSLEnforcement), + }, + }, + ) + if err != nil { + return "", mysql.Server{}, err + } + + pollingURL = res.PollingURL() + server, err = res.Result(client) + + } else { + res, err := client.Create( + ctx, + instance.Spec.ResourceGroup, + instance.Name, + mysql.ServerForCreate{ + Location: &instance.Spec.Location, + Tags: tags, + Properties: serverProperties, + Sku: skuData, + }, + ) + if err != nil { + return "", mysql.Server{}, err + } + + pollingURL = res.PollingURL() + server, err = res.Result(client) + } + + return pollingURL, server, err } diff --git a/pkg/resourcemanager/mysql/server/reconcile.go b/pkg/resourcemanager/mysql/server/reconcile.go index a3a7adb6705..3ec84275e01 100644 --- a/pkg/resourcemanager/mysql/server/reconcile.go +++ b/pkg/resourcemanager/mysql/server/reconcile.go @@ -39,13 +39,13 @@ func (m *MySQLServerClient) Ensure(ctx context.Context, obj runtime.Object, opts return true, err } - createmode := "Default" + createmode := mysql.CreateModeDefault if len(instance.Spec.CreateMode) != 0 { - createmode = instance.Spec.CreateMode + createmode = mysql.CreateMode(instance.Spec.CreateMode) } // If a replica is requested, ensure that source server is specified - if strings.EqualFold(createmode, "replica") { + if createmode == mysql.CreateModeReplica { if len(instance.Spec.ReplicaProperties.SourceServerId) == 0 { instance.Status.Message = "Replica requested but source server unspecified" return true, nil @@ -66,122 +66,132 @@ func (m *MySQLServerClient) Ensure(ctx context.Context, obj runtime.Object, opts // convert kube labels to expected tag format labels := helpers.LabelsToTags(instance.GetLabels()) - // Check if this server already exists and its state if it does. This is required - // to overcome the issue with the lack of idempotence of the Create call - hash := "" - server, err := m.GetServer(ctx, instance.Spec.ResourceGroup, instance.Name) - if err != nil { - // 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 - } + hash := helpers.Hash256(instance.Spec) + if instance.Status.SpecHash == hash && (instance.Status.Provisioned || instance.Status.FailedProvisioning) { + instance.Status.RequestedAt = nil + return true, nil + } else if instance.Status.SpecHash != hash && !instance.Status.Provisioning { + instance.Status.Provisioned = false + } - if res.Status == "Failed" { - instance.Status.Provisioning = false - instance.Status.RequestedAt = nil - ignore := []string{ - errhelp.SubscriptionDoesNotHaveServer, - errhelp.ServiceBusy, + if instance.Status.Provisioning { + + // Check if this server already exists and its state if it does. This is required + // to overcome the issue with the lack of idempotence of the Create call + server, err := m.GetServer(ctx, instance.Spec.ResourceGroup, instance.Name) + if err != nil { + // 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 !helpers.ContainsString(ignore, res.Error.Code) { - instance.Status.Message = res.Error.Error() - return true, nil + + 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 + } } } - } - } else { - instance.Status.State = string(server.UserVisibleState) + } else { + instance.Status.State = string(server.UserVisibleState) - hash = helpers.Hash256(instance.Spec) - if instance.Status.SpecHash == hash && (instance.Status.Provisioned || instance.Status.FailedProvisioning) { - instance.Status.RequestedAt = nil - return true, nil - } - if server.UserVisibleState == mysql.ServerStateReady { - // Update secret with FQ name of the server. We ignore the error. - m.UpdateServerNameInSecret(ctx, instance.Name, secret, *server.FullyQualifiedDomainName, instance) + hash = helpers.Hash256(instance.Spec) + if instance.Status.SpecHash == hash && (instance.Status.Provisioned || instance.Status.FailedProvisioning) { + instance.Status.RequestedAt = nil + return true, nil + } + if server.UserVisibleState == mysql.ServerStateReady { + // Update secret with FQ name of the server. We ignore the error. + m.UpdateServerNameInSecret(ctx, instance.Name, secret, *server.FullyQualifiedDomainName, instance) - instance.Status.Provisioned = true - instance.Status.Provisioning = false - instance.Status.Message = resourcemanager.SuccessMsg - instance.Status.ResourceId = *server.ID - instance.Status.State = string(server.UserVisibleState) - instance.Status.SpecHash = hash - return true, nil + instance.Status.Provisioned = true + instance.Status.Provisioning = false + instance.Status.Message = resourcemanager.SuccessMsg + instance.Status.ResourceId = *server.ID + instance.Status.State = string(server.UserVisibleState) + instance.Status.SpecHash = hash + return true, nil + } } - } - // if the create has been sent with no error we need to wait before calling it again - // @todo set an appropriate time since create has been called to retry - if instance.Status.Provisioning { - return false, nil } - adminlogin := string(secret["username"]) - adminpassword := string(secret["password"]) - skuInfo := mysql.Sku{ - Name: to.StringPtr(instance.Spec.Sku.Name), - Tier: mysql.SkuTier(instance.Spec.Sku.Tier), - Capacity: to.Int32Ptr(instance.Spec.Sku.Capacity), - Size: to.StringPtr(instance.Spec.Sku.Size), - Family: to.StringPtr(instance.Spec.Sku.Family), - } + if !instance.Status.Provisioning && !instance.Status.Provisioned { + instance.Status.Provisioning = true + instance.Status.FailedProvisioning = false + + adminlogin := string(secret["username"]) + adminpassword := string(secret["password"]) + skuInfo := mysql.Sku{ + Name: to.StringPtr(instance.Spec.Sku.Name), + Tier: mysql.SkuTier(instance.Spec.Sku.Tier), + Capacity: to.Int32Ptr(instance.Spec.Sku.Capacity), + Size: to.StringPtr(instance.Spec.Sku.Size), + Family: to.StringPtr(instance.Spec.Sku.Family), + } - pollURL, server, err := m.CreateServerIfValid( - ctx, - *instance, - labels, - skuInfo, - adminlogin, - adminpassword, - createmode, - ) - if err != nil { - // let the user know what happened - instance.Status.Message = errhelp.StripErrorIDs(err) - instance.Status.Provisioning = false - azerr := errhelp.NewAzureErrorAzureError(err) + pollURL, _, err := m.CreateServerIfValid( + ctx, + *instance, + labels, + skuInfo, + adminlogin, + adminpassword, + createmode, + hash, + ) + if err != nil { + instance.Status.Message = errhelp.StripErrorIDs(err) + instance.Status.Provisioning = false - catchRequeue := []string{ - errhelp.ResourceGroupNotFoundErrorCode, - errhelp.ParentNotFoundErrorCode, - errhelp.AsyncOpIncompleteError, - errhelp.SubscriptionDoesNotHaveServer, - errhelp.ServiceBusy, - } - catchUnrecoverable := []string{ - errhelp.ProvisioningDisabled, - errhelp.LocationNotAvailableForResourceType, - errhelp.InvalidRequestContent, - errhelp.InternalServerError, - } + azerr := errhelp.NewAzureErrorAzureError(err) + + catchInProgress := []string{ + errhelp.AsyncOpIncompleteError, + errhelp.AlreadyExists, + } + catchKnownError := []string{ + errhelp.ResourceGroupNotFoundErrorCode, + errhelp.ParentNotFoundErrorCode, + errhelp.NotFoundErrorCode, + errhelp.ServiceBusy, + errhelp.InternalServerError, + } - // handle the errors - if helpers.ContainsString(catchRequeue, azerr.Type) { - if azerr.Type == errhelp.AsyncOpIncompleteError { + // 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 - instance.Status.PollingURL = pollURL + return false, nil } - return false, nil - } - if helpers.ContainsString(catchUnrecoverable, azerr.Type) { - // Unrecoverable error, so stop reconcilation - instance.Status.Message = "Reconcilation hit unrecoverable error: " + errhelp.StripErrorIDs(err) + if helpers.ContainsString(catchKnownError, azerr.Type) { + return false, nil + } + + // serious error occured, end reconcilliation and mark it as failed + instance.Status.Message = errhelp.StripErrorIDs(err) + instance.Status.Provisioned = false + instance.Status.FailedProvisioning = true return true, nil + } - // reconciliation not done and we don't know what happened - return false, err + instance.Status.Message = "request submitted to Azure" } - - instance.Status.Provisioning = true - instance.Status.Message = "Server request submitted to Azure" - return false, nil } diff --git a/pkg/resourcemanager/psql/server/server.go b/pkg/resourcemanager/psql/server/server.go index 13c457f5ca2..2ca4778560c 100644 --- a/pkg/resourcemanager/psql/server/server.go +++ b/pkg/resourcemanager/psql/server/server.go @@ -6,7 +6,6 @@ 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/v1alpha2" @@ -79,7 +78,8 @@ func (p *PSQLServerClient) CreateServerIfValid(ctx context.Context, tags map[string]*string, skuInfo psql.Sku, adminlogin string, adminpassword string, - createmode string) (pollingURL string, server psql.Server, err error) { + createmode psql.CreateMode, + hash string) (pollingURL string, server psql.Server, err error) { client, err := getPSQLServersClient() if err != nil { @@ -92,7 +92,6 @@ func (p *PSQLServerClient) CreateServerIfValid(ctx context.Context, return "", psql.Server{}, err } - var result psql.ServersCreateFuture var serverProperties psql.BasicServerPropertiesForCreate var skuData *psql.Sku var storageProfile *psql.StorageProfile @@ -101,7 +100,7 @@ func (p *PSQLServerClient) CreateServerIfValid(ctx context.Context, storageProfile = &obj } - if strings.EqualFold(createmode, string(psql.CreateModeReplica)) { + if createmode == psql.CreateModeReplica { serverProperties = &psql.ServerPropertiesForReplica{ SourceServerID: to.StringPtr(instance.Spec.ReplicaProperties.SourceServerId), CreateMode: psql.CreateModeReplica, @@ -120,23 +119,49 @@ func (p *PSQLServerClient) CreateServerIfValid(ctx context.Context, skuData = &skuInfo } - result, err = client.Create( - ctx, - instance.Spec.ResourceGroup, - instance.Name, - psql.ServerForCreate{ - Location: &instance.Spec.Location, - Tags: tags, - Properties: serverProperties, - Sku: skuData, - }, - ) - if err != nil { - return "", psql.Server{}, err + if hash != instance.Status.SpecHash && instance.Status.SpecHash != "" { + res, err := client.Update( + ctx, + instance.Spec.ResourceGroup, + instance.Name, + psql.ServerUpdateParameters{ + Tags: tags, + Sku: skuData, + ServerUpdateParametersProperties: &psql.ServerUpdateParametersProperties{ + StorageProfile: storageProfile, + Version: psql.ServerVersion(instance.Spec.ServerVersion), + SslEnforcement: psql.SslEnforcementEnum(instance.Spec.SSLEnforcement), + }, + }, + ) + if err != nil { + return "", psql.Server{}, err + } + + pollingURL = res.PollingURL() + server, err = res.Result(client) + + } else { + res, err := client.Create( + ctx, + instance.Spec.ResourceGroup, + instance.Name, + psql.ServerForCreate{ + Location: &instance.Spec.Location, + Tags: tags, + Properties: serverProperties, + Sku: skuData, + }, + ) + if err != nil { + return "", psql.Server{}, err + } + + pollingURL = res.PollingURL() + server, err = res.Result(client) } - res, err := result.Result(client) - return result.PollingURL(), res, err + return pollingURL, server, err } func (p *PSQLServerClient) DeleteServer(ctx context.Context, resourcegroup string, servername string) (status string, err error) { diff --git a/pkg/resourcemanager/psql/server/server_reconcile.go b/pkg/resourcemanager/psql/server/server_reconcile.go index 33aeb3d2707..dd9656d807c 100644 --- a/pkg/resourcemanager/psql/server/server_reconcile.go +++ b/pkg/resourcemanager/psql/server/server_reconcile.go @@ -6,7 +6,6 @@ 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" @@ -37,13 +36,13 @@ func (p *PSQLServerClient) Ensure(ctx context.Context, obj runtime.Object, opts return true, err } - createmode := string(psql.CreateModeDefault) + createmode := psql.CreateModeDefault if len(instance.Spec.CreateMode) != 0 { - createmode = instance.Spec.CreateMode + createmode = psql.CreateMode(instance.Spec.CreateMode) } // If a replica is requested, ensure that source server is specified - if strings.EqualFold(createmode, string(psql.CreateModeReplica)) { + if createmode == psql.CreateModeReplica { if len(instance.Spec.ReplicaProperties.SourceServerId) == 0 { instance.Status.Message = "Replica requested but source server unspecified" return true, nil @@ -62,122 +61,132 @@ 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 - } + hash := helpers.Hash256(instance.Spec) + if instance.Status.SpecHash == hash && (instance.Status.Provisioned || instance.Status.FailedProvisioning) { + instance.Status.RequestedAt = nil + return true, nil + } else if instance.Status.SpecHash != hash && !instance.Status.Provisioning { + instance.Status.Provisioned = false + } - // succeeded! so end reconcilliation successfully - if getServer.UserVisibleState == psql.ServerStateReady { + if instance.Status.Provisioning { + // 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) - // 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) + // succeeded! so end reconcilliation successfully + if getServer.UserVisibleState == psql.ServerStateReady { - instance.Status.Message = resourcemanager.SuccessMsg - instance.Status.ResourceId = *getServer.ID - instance.Status.Provisioned = true - instance.Status.Provisioning = false - instance.Status.FailedProvisioning = false - instance.Status.SpecHash = hash - return true, nil - } + // 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) - // 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.Message = resourcemanager.SuccessMsg + instance.Status.ResourceId = *getServer.ID + instance.Status.Provisioned = true instance.Status.Provisioning = false - return false, err + instance.Status.FailedProvisioning = false + instance.Status.SpecHash = hash + return true, nil } - if res.Status == "Failed" { - instance.Status.Provisioning = false - instance.Status.RequestedAt = nil - ignore := []string{ - errhelp.SubscriptionDoesNotHaveServer, - errhelp.ServiceBusy, + // 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 !helpers.ContainsString(ignore, res.Error.Code) { - instance.Status.Message = res.Error.Error() - return true, nil + + 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 - labels := helpers.LabelsToTags(instance.GetLabels()) - adminlogin := string(secret["username"]) - adminpassword := string(secret["password"]) - skuInfo := psql.Sku{ - Name: to.StringPtr(instance.Spec.Sku.Name), - Tier: psql.SkuTier(instance.Spec.Sku.Tier), - Capacity: to.Int32Ptr(instance.Spec.Sku.Capacity), - Size: to.StringPtr(instance.Spec.Sku.Size), - Family: to.StringPtr(instance.Spec.Sku.Family), - } + if !instance.Status.Provisioning && !instance.Status.Provisioned { + instance.Status.Provisioning = true + instance.Status.FailedProvisioning = false + + // setup variables for create call + labels := helpers.LabelsToTags(instance.GetLabels()) + adminlogin := string(secret["username"]) + adminpassword := string(secret["password"]) + skuInfo := psql.Sku{ + Name: to.StringPtr(instance.Spec.Sku.Name), + Tier: psql.SkuTier(instance.Spec.Sku.Tier), + Capacity: to.Int32Ptr(instance.Spec.Sku.Capacity), + Size: to.StringPtr(instance.Spec.Sku.Size), + Family: to.StringPtr(instance.Spec.Sku.Family), + } - // create the server - instance.Status.Provisioning = true - instance.Status.FailedProvisioning = false - pollURL, _, err := p.CreateServerIfValid( - ctx, - *instance, - labels, - skuInfo, - adminlogin, - adminpassword, - createmode, - ) - if err != nil { - instance.Status.Message = errhelp.StripErrorIDs(err) - instance.Status.Provisioning = false + // create the server + pollURL, _, err := p.CreateServerIfValid( + ctx, + *instance, + labels, + skuInfo, + adminlogin, + adminpassword, + createmode, + hash, + ) + if err != nil { + instance.Status.Message = errhelp.StripErrorIDs(err) + instance.Status.Provisioning = false - azerr := errhelp.NewAzureErrorAzureError(err) + azerr := errhelp.NewAzureErrorAzureError(err) - catchInProgress := []string{ - errhelp.AsyncOpIncompleteError, - errhelp.AlreadyExists, - } - catchKnownError := []string{ - errhelp.ResourceGroupNotFoundErrorCode, - errhelp.ParentNotFoundErrorCode, - errhelp.NotFoundErrorCode, - errhelp.ServiceBusy, - errhelp.InternalServerError, - } + catchInProgress := []string{ + errhelp.AsyncOpIncompleteError, + errhelp.AlreadyExists, + } + catchKnownError := []string{ + errhelp.ResourceGroupNotFoundErrorCode, + 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 + // 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 } - instance.Status.Message = "Postgres server exists but may not be ready" - instance.Status.Provisioning = true - return false, nil - } - if helpers.ContainsString(catchKnownError, azerr.Type) { - return false, nil + if helpers.ContainsString(catchKnownError, azerr.Type) { + return false, nil + } + + // serious error occured, end reconcilliation and mark it as failed + instance.Status.Message = errhelp.StripErrorIDs(err) + instance.Status.Provisioned = false + instance.Status.FailedProvisioning = true + return true, nil + } - // serious error occured, end reconcilliation and mark it as failed - instance.Status.Message = errhelp.StripErrorIDs(err) - instance.Status.Provisioned = false - instance.Status.FailedProvisioning = true - return true, nil + instance.Status.Message = "request submitted to Azure" } diff --git a/pkg/resourcemanager/rediscaches/firewallrule/rediscachefirewallrule.go b/pkg/resourcemanager/rediscaches/firewallrule/rediscachefirewallrule.go new file mode 100644 index 00000000000..cf4f2f64697 --- /dev/null +++ b/pkg/resourcemanager/rediscaches/firewallrule/rediscachefirewallrule.go @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package rediscachefirewallrules + +import ( + "context" + "net/http" + + "github.com/Azure/azure-sdk-for-go/services/redis/mgmt/2018-03-01/redis" + azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" + "github.com/Azure/azure-service-operator/pkg/resourcemanager/iam" + "github.com/Azure/go-autorest/autorest" + "github.com/Azure/go-autorest/autorest/to" +) + +// AzureRedisCacheFirewallRuleManager creates a new AzureRedisCacheFirewallRuleManager +type AzureRedisCacheFirewallRuleManager struct{} + +// NewAzureRedisCacheFirewallRuleManager creates a new AzureRedisCacheFirewallRuleManager +func NewAzureRedisCacheFirewallRuleManager() *AzureRedisCacheFirewallRuleManager { + return &AzureRedisCacheFirewallRuleManager{} +} + +// getRedisCacheFirewallRuleClient retrieves a firewallrules client +func getRedisCacheFirewallRuleClient() (redis.FirewallRulesClient, error) { + firewallRulesClient := redis.NewFirewallRulesClientWithBaseURI(config.BaseURI(), config.SubscriptionID()) + a, err := iam.GetResourceManagementAuthorizer() + if err != nil { + return redis.FirewallRulesClient{}, err + } + firewallRulesClient.Authorizer = a + firewallRulesClient.AddToUserAgent(config.UserAgent()) + return firewallRulesClient, nil +} + +// CreateRedisCacheFirewallRule creates a new RedisCacheFirewallRule +func (r *AzureRedisCacheFirewallRuleManager) CreateRedisCacheFirewallRule(ctx context.Context, instance azurev1alpha1.RedisCacheFirewallRule) (result redis.FirewallRule, err error) { + + firewallRuleClient, err := getRedisCacheFirewallRuleClient() + if err != nil { + return redis.FirewallRule{}, err + } + + resourceGroup := instance.Spec.ResourceGroup + redisCacheName := instance.Spec.CacheName + firewallRuleName := instance.ObjectMeta.Name + + firewallRuleParameters := redis.FirewallRuleCreateParameters{ + FirewallRuleProperties: &redis.FirewallRuleProperties{ + StartIP: to.StringPtr(instance.Spec.Properties.StartIP), + EndIP: to.StringPtr(instance.Spec.Properties.EndIP), + }, + } + future, err := firewallRuleClient.CreateOrUpdate( + ctx, + resourceGroup, + redisCacheName, + firewallRuleName, + firewallRuleParameters, + ) + if err != nil { + return redis.FirewallRule{}, err + } + + return future, err +} + +// Get gets a single firewall rule in a specified redis cache +func (r *AzureRedisCacheFirewallRuleManager) Get(ctx context.Context, resourceGroup string, redisCacheName string, firewallRuleName string) (result redis.FirewallRule, err error) { + + firewallRuleClient, err := getRedisCacheFirewallRuleClient() + if err != nil { + return redis.FirewallRule{}, err + } + + return firewallRuleClient.Get(ctx, resourceGroup, redisCacheName, firewallRuleName) +} + +// DeleteRedisCacheFirewallRule deletes a redis firewall rule +func (r *AzureRedisCacheFirewallRuleManager) DeleteRedisCacheFirewallRule(ctx context.Context, resourceGroup string, redisCacheName string, firewallRuleName string) (result autorest.Response, err error) { + result = autorest.Response{ + Response: &http.Response{ + StatusCode: 200, + }, + } + firewallRuleClient, err := getRedisCacheFirewallRuleClient() + if err != nil { + return result, err + } + future, err := firewallRuleClient.Delete(ctx, resourceGroup, redisCacheName, firewallRuleName) + if err != nil { + return result, nil + } + return future, err +} diff --git a/pkg/resourcemanager/rediscaches/firewallrule/rediscachefirewallrule_manager.go b/pkg/resourcemanager/rediscaches/firewallrule/rediscachefirewallrule_manager.go new file mode 100644 index 00000000000..f5324f893cf --- /dev/null +++ b/pkg/resourcemanager/rediscaches/firewallrule/rediscachefirewallrule_manager.go @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package rediscachefirewallrules + +import ( + "context" + + "github.com/Azure/azure-sdk-for-go/services/redis/mgmt/2018-03-01/redis" + azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" + "github.com/Azure/azure-service-operator/pkg/resourcemanager" + "github.com/Azure/go-autorest/autorest" +) + +// RedisCacheFirewallRuleManager for RedisCacheFirewallRule +type RedisCacheFirewallRuleManager interface { + // CreateRedisCacheFirewallRule creates a new RedisCacheFirewallRule + CreateRedisCacheFirewallRule(ctx context.Context, instance azurev1alpha1.RedisCacheFirewallRule) (result redis.FirewallRule, err error) + + // Get gets a single firewall rule in a specified redis cache + Get(ctx context.Context, resourceGroup string, redisCacheName string, firewallRuleName string) (err error) + + // DeleteRedisCacheFirewallRule deletes a server firewall rule + DeleteRedisCacheFirewallRule(ctx context.Context, resourceGroup string, redisCacheName string, firewallRuleName string) (result autorest.Response, err error) + + // also embed async client methods + resourcemanager.ARMClient +} diff --git a/pkg/resourcemanager/rediscaches/firewallrule/rediscachefirewallrule_reconcile.go b/pkg/resourcemanager/rediscaches/firewallrule/rediscachefirewallrule_reconcile.go new file mode 100644 index 00000000000..2afb076c36b --- /dev/null +++ b/pkg/resourcemanager/rediscaches/firewallrule/rediscachefirewallrule_reconcile.go @@ -0,0 +1,154 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package rediscachefirewallrules + +import ( + "context" + "fmt" + + "github.com/Azure/azure-service-operator/api/v1alpha1" + 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" +) + +// Ensure creates a rediscachefirewallrule +func (fw *AzureRedisCacheFirewallRuleManager) Ensure(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { + instance, err := fw.convert(obj) + if err != nil { + return true, err + } + + fwr, err := fw.Get(ctx, instance.Spec.ResourceGroup, instance.Spec.CacheName, instance.ObjectMeta.Name) + if err == nil { + instance.Status.Provisioned = true + instance.Status.Provisioning = false + instance.Status.Message = resourcemanager.SuccessMsg + instance.Status.ResourceId = *fwr.ID + instance.Status.State = fwr.Status + return true, nil + } + + instance.Status.Provisioning = true + instance.Status.FailedProvisioning = false + + _, err = fw.CreateRedisCacheFirewallRule(ctx, *instance) + if err != nil { + instance.Status.Message = err.Error() + instance.Status.Provisioning = false + azerr := errhelp.NewAzureErrorAzureError(err) + + inProgress := []string{ + errhelp.RequestConflictError, + errhelp.AsyncOpIncompleteError, + } + ignorableErr := []string{ + errhelp.ParentNotFoundErrorCode, + errhelp.ResourceGroupNotFoundErrorCode, + errhelp.ResourceNotFound, + } + fatalErr := []string{ + errhelp.BadRequest, + } + + if helpers.ContainsString(inProgress, azerr.Type) { + instance.Status.Provisioning = true + instance.Status.Message = "RedisCacheFirewallRule exists but may not be ready" + return false, nil + } + if helpers.ContainsString(ignorableErr, azerr.Type) { + instance.Status.Provisioning = false + return false, nil + } + if helpers.ContainsString(fatalErr, azerr.Type) { + // serious error occured, end reconcilliation and mark it as failed + instance.Status.Message = fmt.Sprintf("Error occurred creating the RedisCacheFirewallRule: %s", errhelp.StripErrorIDs(err)) + instance.Status.Provisioned = false + instance.Status.Provisioning = false + instance.Status.FailedProvisioning = true + return true, nil + } + return false, nil + } + return false, nil +} + +// Delete removes a rediscachefirewallrule +func (fw *AzureRedisCacheFirewallRuleManager) Delete(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { + instance, err := fw.convert(obj) + if err != nil { + return true, err + } + + _, err = fw.DeleteRedisCacheFirewallRule(ctx, instance.Spec.ResourceGroup, instance.Spec.CacheName, instance.ObjectMeta.Name) + if err != nil { + instance.Status.Message = err.Error() + azerr := errhelp.NewAzureErrorAzureError(err) + + ignorableErr := []string{ + errhelp.AsyncOpIncompleteError, + } + + finished := []string{ + errhelp.ResourceNotFound, + } + if helpers.ContainsString(ignorableErr, azerr.Type) { + return true, nil + } + if helpers.ContainsString(finished, azerr.Type) { + return false, nil + } + + instance.Status.Message = fmt.Sprintf("AzureSqlFailoverGroup Delete failed with: %s", err.Error()) + return false, err + } + // successful return + instance.Status.Message = fmt.Sprintf("Delete AzureSqlFailoverGroup succeeded") + return false, err +} + +// GetParents returns the parents of rediscachefirewallrule +func (fw *AzureRedisCacheFirewallRuleManager) GetParents(obj runtime.Object) ([]resourcemanager.KubeParent, error) { + instance, err := fw.convert(obj) + if err != nil { + return nil, err + } + + return []resourcemanager.KubeParent{ + { + Key: types.NamespacedName{ + Namespace: instance.Namespace, + Name: instance.Spec.CacheName, + }, + Target: &v1alpha1.RedisCache{}, + }, + { + Key: types.NamespacedName{ + Namespace: instance.Namespace, + Name: instance.Spec.ResourceGroup, + }, + Target: &v1alpha1.ResourceGroup{}, + }, + }, nil +} + +// GetStatus gets the ASOStatus +func (fw *AzureRedisCacheFirewallRuleManager) GetStatus(obj runtime.Object) (*azurev1alpha1.ASOStatus, error) { + instance, err := fw.convert(obj) + if err != nil { + return nil, err + } + return &instance.Status, nil +} + +func (fw *AzureRedisCacheFirewallRuleManager) convert(obj runtime.Object) (*v1alpha1.RedisCacheFirewallRule, error) { + local, ok := obj.(*v1alpha1.RedisCacheFirewallRule) + if !ok { + return nil, fmt.Errorf("failed type assertion on kind: %s", obj.GetObjectKind().GroupVersionKind().String()) + } + return local, nil +} diff --git a/pkg/resourcemanager/rediscaches/rediscache_manager.go b/pkg/resourcemanager/rediscaches/redis/rediscache_manager.go similarity index 100% rename from pkg/resourcemanager/rediscaches/rediscache_manager.go rename to pkg/resourcemanager/rediscaches/redis/rediscache_manager.go diff --git a/pkg/resourcemanager/rediscaches/rediscache_reconcile.go b/pkg/resourcemanager/rediscaches/redis/rediscache_reconcile.go similarity index 100% rename from pkg/resourcemanager/rediscaches/rediscache_reconcile.go rename to pkg/resourcemanager/rediscaches/redis/rediscache_reconcile.go diff --git a/pkg/resourcemanager/rediscaches/rediscaches.go b/pkg/resourcemanager/rediscaches/redis/rediscaches.go similarity index 96% rename from pkg/resourcemanager/rediscaches/rediscaches.go rename to pkg/resourcemanager/rediscaches/redis/rediscaches.go index 510afd36c8f..a005e2e0057 100644 --- a/pkg/resourcemanager/rediscaches/rediscaches.go +++ b/pkg/resourcemanager/rediscaches/redis/rediscaches.go @@ -12,6 +12,7 @@ import ( "github.com/Azure/azure-sdk-for-go/services/redis/mgmt/2018-03-01/redis" 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/rediscaches" "github.com/Azure/azure-service-operator/pkg/secrets" "github.com/Azure/go-autorest/autorest/to" "k8s.io/apimachinery/pkg/runtime" @@ -19,13 +20,13 @@ import ( // AzureRedisCacheManager creates a new RedisCacheManager type AzureRedisCacheManager struct { - AzureRedisManager + rediscaches.AzureRedisManager } // NewAzureRedisCacheManager creates a new RedisCacheManager func NewAzureRedisCacheManager(secretClient secrets.SecretClient, scheme *runtime.Scheme) *AzureRedisCacheManager { return &AzureRedisCacheManager{ - AzureRedisManager{ + rediscaches.AzureRedisManager{ SecretClient: secretClient, Scheme: scheme, }, diff --git a/pkg/resourcemanager/rediscaches/suite_test.go b/pkg/resourcemanager/rediscaches/suite_test.go deleted file mode 100644 index 5f8c782ad1e..00000000000 --- a/pkg/resourcemanager/rediscaches/suite_test.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -package rediscaches - -import ( - "testing" - - resourcemanagerconfig "github.com/Azure/azure-service-operator/pkg/resourcemanager/config" - resourcegroupsresourcemanager "github.com/Azure/azure-service-operator/pkg/resourcemanager/resourcegroups" - - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - "context" - - "github.com/Azure/azure-service-operator/pkg/helpers" - logf "sigs.k8s.io/controller-runtime/pkg/log" - "sigs.k8s.io/controller-runtime/pkg/log/zap" -) - -// These tests use Ginkgo (BDD-style Go testing framework). Refer to -// http://onsi.github.io/ginkgo/ to learn more about Ginkgo. - -type TestContext struct { - ResourceGroupName string - ResourceGroupLocation string - ResourceGroupManager resourcegroupsresourcemanager.ResourceGroupManager - RedisCacheManager *AzureRedisCacheManager -} - -var tc TestContext - -func TestAPIs(t *testing.T) { - t.Parallel() - if testing.Short() { - t.Skip("skipping Resource Manager Eventhubs Suite") - } - RegisterFailHandler(Fail) - RunSpecs(t, "RedisCache Suite") -} - -var _ = BeforeSuite(func() { - zaplogger := zap.LoggerTo(GinkgoWriter, true) - logf.SetLogger(zaplogger) - - By("bootstrapping test environment") - - err := resourcemanagerconfig.ParseEnvironment() - Expect(err).ToNot(HaveOccurred()) - Expect(err).ToNot(HaveOccurred()) - - resourceGroupName := "t-rg-dev-rm-eh-" + helpers.RandomString(10) - resourceGroupLocation := resourcemanagerconfig.DefaultLocation() - resourceGroupManager := resourcegroupsresourcemanager.NewAzureResourceGroupManager() - - //create resourcegroup for this suite - _, err = resourceGroupManager.CreateGroup(context.Background(), resourceGroupName, resourceGroupLocation) - Expect(err).ToNot(HaveOccurred()) - - tc = TestContext{ - ResourceGroupName: resourceGroupName, - ResourceGroupLocation: resourceGroupLocation, - RedisCacheManager: &AzureRedisCacheManager{}, - ResourceGroupManager: resourceGroupManager, - } -}) - -var _ = AfterSuite(func() { - By("tearing down the test environment") - // delete the resource group and contained resources - _, _ = tc.ResourceGroupManager.DeleteGroup(context.Background(), tc.ResourceGroupName) -})