diff --git a/PROJECT b/PROJECT index b287247d3de..8f03bf3c9f7 100644 --- a/PROJECT +++ b/PROJECT @@ -1,96 +1,95 @@ -version: "2" domain: microsoft.com repo: github.com/Azure/azure-service-operator resources: - group: azure - version: v1alpha1 kind: StorageAccount -- group: azure version: v1alpha1 - kind: CosmosDB - group: azure + kind: CosmosDB version: v1alpha1 - kind: RedisCache - group: azure + kind: RedisCache version: v1alpha1 - kind: Eventhub - group: azure + kind: Eventhub version: v1alpha1 - kind: ResourceGroup - group: azure + kind: ResourceGroup version: v1alpha1 - kind: EventhubNamespace - group: azure + kind: EventhubNamespace version: v1alpha1 - kind: AzureSqlServer - group: azure + kind: AzureSqlServer version: v1alpha1 - kind: AzureSqlDatabase - group: azure + kind: AzureSqlDatabase version: v1alpha1 - kind: AzureSqlFirewallRule - group: azure + kind: AzureSqlFirewallRule version: v1alpha1 - kind: KeyVault - group: azure + kind: KeyVault version: v1alpha1 - kind: ConsumerGroup - group: azure + kind: ConsumerGroup version: v1alpha1 - kind: AzureSqlAction - group: azure + kind: AzureSqlAction version: v1alpha1 - kind: BlobContainer - group: azure + kind: BlobContainer version: v1alpha1 - kind: PostgreSQLServer - group: azure + kind: PostgreSQLServer version: v1alpha1 - kind: PostgreSQLDatabase - group: azure + kind: PostgreSQLDatabase version: v1alpha1 - kind: PostgreSQLVNetRule - group: azure + kind: PostgreSQLVNetRule version: v1alpha1 +- group: azure kind: PostgreSQLFirewallRule + version: v1alpha1 - group: azure kind: PostgreSQLUser version: v1alpha1 - group: azure version: v1alpha1 - group: azure - version: v1alpha1 kind: ApimService -- group: azure version: v1alpha1 - kind: VirtualNetwork - group: azure + kind: VirtualNetwork version: v1alpha1 - kind: AzurePublicIPAddress - group: azure + kind: AzurePublicIPAddress version: v1alpha1 - kind: AzureNetworkInterface - group: azure + kind: AzureNetworkInterface version: v1alpha1 - kind: AppInsights - group: azure + kind: AppInsights version: v1alpha1 - kind: KeyVaultKey - group: azure + kind: KeyVaultKey version: v1alpha1 - kind: AzureSQLVNetRule - group: azure + kind: AzureSQLVNetRule version: v1alpha1 - kind: MySQLServer - group: azure + kind: MySQLServer version: v1alpha1 - kind: MySQLDatabase - group: azure + kind: MySQLDatabase version: v1alpha1 - kind: MySQLFirewallRule - group: azure + kind: MySQLFirewallRule version: v1alpha1 +- group: azure kind: MySQLVNetRule + version: v1alpha1 - group: azure kind: MySQLUser version: v1alpha1 @@ -98,32 +97,32 @@ resources: kind: AzureVirtualMachine version: v1alpha1 - group: azure - version: v1alpha1 kind: AzureSQLManagedUser -- group: azure version: v1alpha1 - kind: AzureLoadBalancer - group: azure + kind: AzureLoadBalancer version: v1alpha1 +- group: azure kind: AzureVMScaleSet + version: v1alpha1 - group: azure - version: v1beta1 kind: AzureSqlServer -- group: azure version: v1beta1 - kind: AzureSqlDatabase - group: azure + kind: AzureSqlDatabase version: v1beta1 - kind: AzureSqlFirewallRule - group: azure + kind: AzureSqlFirewallRule version: v1beta1 +- group: azure kind: AzureSqlFailoverGroup + version: v1beta1 - group: azure - version: v1alpha2 kind: BlobContainer -- group: azure version: v1alpha2 +- group: azure kind: MySQLServer + version: v1alpha2 - group: azure kind: RedisCacheFirewallRule version: v1alpha1 @@ -133,4 +132,7 @@ resources: - group: azure kind: AzureVirtualMachineExtension version: v1alpha1 - +- group: azure + kind: AppInsightsApiKey + version: v1alpha1 +version: "2" diff --git a/api/v1alpha1/appinsightsapikey_types.go b/api/v1alpha1/appinsightsapikey_types.go new file mode 100644 index 00000000000..077504f9b5f --- /dev/null +++ b/api/v1alpha1/appinsightsapikey_types.go @@ -0,0 +1,50 @@ +// 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. + +// AppInsightsApiKeySpec defines the desired state of AppInsightsApiKey +type AppInsightsApiKeySpec struct { + // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster + // Important: Run "make" to regenerate code after modifying this file + + ResourceGroup string `json:"resourceGroup"` + AppInsights string `json:"appInsights"` + ReadTelemetry bool `json:"readTelemetry,omitempty"` + WriteAnnotations bool `json:"writeAnnotations,omitempty"` + AuthSDKControlChannel bool `json:"authSDKControlChannel,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// +kubebuilder:printcolumn:name="Provisioned",type="string",JSONPath=".status.provisioned" +// +kubebuilder:printcolumn:name="Message",type="string",JSONPath=".status.message" +// AppInsightsApiKey is the Schema for the appinsightsapikeys API +type AppInsightsApiKey struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec AppInsightsApiKeySpec `json:"spec,omitempty"` + Status ASOStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// AppInsightsApiKeyList contains a list of AppInsightsApiKey +type AppInsightsApiKeyList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []AppInsightsApiKey `json:"items"` +} + +func init() { + SchemeBuilder.Register(&AppInsightsApiKey{}, &AppInsightsApiKeyList{}) +} diff --git a/api/v1alpha1/keyvault_types.go b/api/v1alpha1/keyvault_types.go index fafa9bb571d..5d289f241ba 100644 --- a/api/v1alpha1/keyvault_types.go +++ b/api/v1alpha1/keyvault_types.go @@ -9,7 +9,7 @@ import ( // KeyVaultSpec defines the desired state of KeyVault type KeyVaultSpec struct { - Location string `json:"location"` + Location string `json:"location"` // +kubebuilder:validation:Pattern=^[-\w\._\(\)]+$ // +kubebuilder:validation:MinLength:1 // +kubebuilder:validation:Required @@ -36,6 +36,9 @@ type AccessPolicyEntry struct { TenantID string `json:"tenantID,omitempty"` // ClientID - The client ID of a user, service principal or security group in the Azure Active Directory tenant for the vault. The client ID must be unique for the list of access policies. ClientID string `json:"clientID,omitempty"` + // ObjectID is the value to use if the access policy is for a user other than the user creating the Key Vault when the creating user does not have access to the Application API which is used to translate ClientID to Object ID + // To get around this, use az-cli or the Azure portal to source the ObjectID from your Service Principal + ObjectID string `json:"objectID,omitempty"` // ApplicationID - Application ID of the client making request on behalf of a principal ApplicationID string `json:"applicationID,omitempty"` // Permissions - Permissions the identity has for keys, secrets, and certificates. diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index f6b4d040db4..df74f74a710 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -267,6 +267,80 @@ func (in *AppInsights) DeepCopyObject() runtime.Object { return nil } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *AppInsightsApiKey) DeepCopyInto(out *AppInsightsApiKey) { + *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 AppInsightsApiKey. +func (in *AppInsightsApiKey) DeepCopy() *AppInsightsApiKey { + if in == nil { + return nil + } + out := new(AppInsightsApiKey) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AppInsightsApiKey) 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 *AppInsightsApiKeyList) DeepCopyInto(out *AppInsightsApiKeyList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]AppInsightsApiKey, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppInsightsApiKeyList. +func (in *AppInsightsApiKeyList) DeepCopy() *AppInsightsApiKeyList { + if in == nil { + return nil + } + out := new(AppInsightsApiKeyList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *AppInsightsApiKeyList) 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 *AppInsightsApiKeySpec) DeepCopyInto(out *AppInsightsApiKeySpec) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new AppInsightsApiKeySpec. +func (in *AppInsightsApiKeySpec) DeepCopy() *AppInsightsApiKeySpec { + if in == nil { + return nil + } + out := new(AppInsightsApiKeySpec) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *AppInsightsList) DeepCopyInto(out *AppInsightsList) { *out = *in diff --git a/charts/azure-service-operator-0.1.0.tgz b/charts/azure-service-operator-0.1.0.tgz index a3aa3b08341..a9f4fee3c36 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/azure-service-operator/crds/apiextensions.k8s.io_v1beta1_customresourcedefinition_appinsightsapikeys.azure.microsoft.com.yaml b/charts/azure-service-operator/crds/apiextensions.k8s.io_v1beta1_customresourcedefinition_appinsightsapikeys.azure.microsoft.com.yaml new file mode 100644 index 00000000000..8141ad40f49 --- /dev/null +++ b/charts/azure-service-operator/crds/apiextensions.k8s.io_v1beta1_customresourcedefinition_appinsightsapikeys.azure.microsoft.com.yaml @@ -0,0 +1,102 @@ +apiVersion: apiextensions.k8s.io/v1beta1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.2.5 + creationTimestamp: null + name: appinsightsapikeys.azure.microsoft.com +spec: + additionalPrinterColumns: + - JSONPath: .status.provisioned + name: Provisioned + type: string + - JSONPath: .status.message + name: Message + type: string + group: azure.microsoft.com + names: + kind: AppInsightsApiKey + listKind: AppInsightsApiKeyList + plural: appinsightsapikeys + singular: appinsightsapikey + scope: Namespaced + subresources: + status: {} + validation: + openAPIV3Schema: + description: AppInsightsApiKey is the Schema for the appinsightsapikeys API + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: AppInsightsApiKeySpec defines the desired state of AppInsightsApiKey + properties: + appInsights: + type: string + authSDKControlChannel: + type: boolean + readTelemetry: + type: boolean + resourceGroup: + type: string + writeAnnotations: + type: boolean + required: + - appInsights + - resourceGroup + type: object + status: + description: ASOStatus (AzureServiceOperatorsStatus) defines the observed + state of resource actions + properties: + completed: + format: date-time + type: string + containsUpdate: + type: boolean + failedProvisioning: + type: boolean + flattenedSecrets: + type: boolean + message: + type: string + output: + type: string + pollingUrl: + type: string + provisioned: + type: boolean + provisioning: + type: boolean + requested: + format: date-time + type: string + resourceId: + type: string + specHash: + type: string + state: + type: string + type: object + type: object + version: v1alpha1 + versions: + - name: v1alpha1 + served: true + storage: true +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] diff --git a/charts/azure-service-operator/templates/generated/rbac.authorization.k8s.io_v1_clusterrole_azureoperator-manager-role.yaml b/charts/azure-service-operator/templates/generated/rbac.authorization.k8s.io_v1_clusterrole_azureoperator-manager-role.yaml index 25263dbc4ae..7411ec71dc9 100644 --- a/charts/azure-service-operator/templates/generated/rbac.authorization.k8s.io_v1_clusterrole_azureoperator-manager-role.yaml +++ b/charts/azure-service-operator/templates/generated/rbac.authorization.k8s.io_v1_clusterrole_azureoperator-manager-role.yaml @@ -104,6 +104,26 @@ rules: - get - patch - update +- apiGroups: + - azure.microsoft.com + resources: + - appinsightsapikeys + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - azure.microsoft.com + resources: + - appinsightsapikeys/status + verbs: + - get + - patch + - update - apiGroups: - azure.microsoft.com resources: diff --git a/charts/index.yaml b/charts/index.yaml index 1835476b32c..ea7d4271589 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-07-02T13:28:16.308913-06:00" + created: "2020-07-08T10:09:00.884245-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: be07303de61e0675551bfeae5cf5dd59d944a5dd822f583ebdd0739d6b16567d + digest: 7772dcf19b531eced14cec948a8d2705c19303d5c210ff2f0b52784931baeb19 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-07-02T13:28:16.303844-06:00" +generated: "2020-07-08T10:09:00.877634-06:00" diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index 092cba104fa..ffa2eee9227 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -43,6 +43,7 @@ resources: - bases/azure.microsoft.com_rediscacheactions.yaml - bases/azure.microsoft.com_rediscachefirewallrules.yaml - bases/azure.microsoft.com_azurevirtualmachineextensions.yaml +- bases/azure.microsoft.com_appinsightsapikeys.yaml # +kubebuilder:scaffold:crdkustomizeresource patches: @@ -85,6 +86,7 @@ patches: #- patches/webhook_in_rediscachefirewallrules.yaml #- patches/webhook_in_azurevirtualmachineextensions.yaml #- patches/webhook_in_mysqlusers.yaml +#- patches/webhook_in_appinsightsapikeys.yaml # +kubebuilder:scaffold:crdkustomizewebhookpatch # [CAINJECTION] patches here are for enabling the CA injection for each CRD @@ -126,6 +128,7 @@ patches: #- patches/cainjection_in_rediscachefirewallrules.yaml #- patches/cainjection_in_azurevirtualmachineextensions.yaml #- patches/cainjection_in_mysqlusers.yaml +#- patches/cainjection_in_appinsightsapikeys.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_appinsightsapikeys.yaml b/config/crd/patches/cainjection_in_appinsightsapikeys.yaml new file mode 100644 index 00000000000..cfa4fc8b0c6 --- /dev/null +++ b/config/crd/patches/cainjection_in_appinsightsapikeys.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: appinsightsapikeys.azure.microsoft.com diff --git a/config/crd/patches/webhook_in_appinsightsapikeys.yaml b/config/crd/patches/webhook_in_appinsightsapikeys.yaml new file mode 100644 index 00000000000..5dc6c727f9e --- /dev/null +++ b/config/crd/patches/webhook_in_appinsightsapikeys.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: appinsightsapikeys.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/appinsightsapikey_editor_role.yaml b/config/rbac/appinsightsapikey_editor_role.yaml new file mode 100644 index 00000000000..20ec0046ab5 --- /dev/null +++ b/config/rbac/appinsightsapikey_editor_role.yaml @@ -0,0 +1,24 @@ +# permissions for end users to edit appinsightsapikeys. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: appinsightsapikey-editor-role +rules: +- apiGroups: + - azure.microsoft.com + resources: + - appinsightsapikeys + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - azure.microsoft.com + resources: + - appinsightsapikeys/status + verbs: + - get diff --git a/config/rbac/appinsightsapikey_viewer_role.yaml b/config/rbac/appinsightsapikey_viewer_role.yaml new file mode 100644 index 00000000000..01d9377ad5a --- /dev/null +++ b/config/rbac/appinsightsapikey_viewer_role.yaml @@ -0,0 +1,20 @@ +# permissions for end users to view appinsightsapikeys. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + name: appinsightsapikey-viewer-role +rules: +- apiGroups: + - azure.microsoft.com + resources: + - appinsightsapikeys + verbs: + - get + - list + - watch +- apiGroups: + - azure.microsoft.com + resources: + - appinsightsapikeys/status + verbs: + - get diff --git a/config/samples/azure_v1alpha1_appinsightsapikey.yaml b/config/samples/azure_v1alpha1_appinsightsapikey.yaml new file mode 100644 index 00000000000..ea43e2325eb --- /dev/null +++ b/config/samples/azure_v1alpha1_appinsightsapikey.yaml @@ -0,0 +1,10 @@ +apiVersion: azure.microsoft.com/v1alpha1 +kind: AppInsightsApiKey +metadata: + name: key-sample +spec: + appInsights: appinsights-sample + resourceGroup: resourcegroup-azure-operators + readTelemetry: true + writeAnnotations: true + authSDKControlChannel: true diff --git a/config/samples/azure_v1alpha1_keyvault.yaml b/config/samples/azure_v1alpha1_keyvault.yaml index cc482ebacf9..d79117245bf 100644 --- a/config/samples/azure_v1alpha1_keyvault.yaml +++ b/config/samples/azure_v1alpha1_keyvault.yaml @@ -24,7 +24,12 @@ spec: accessPolicies: - tenantID: clientID: - applicationID: + # applicationID and objectID are optional replacements for clientID + # Use clientID when the access you are providing is for the same Service Principal who created the Key Vault. An access policy actually needs the ObjectID but with proper permissions we can translate clientID to ObjectID. + # Use applicationID when providing access to a managed identity + # Use objectID when the service principal who needs access is not the same as the one the operator used to create the Key Vault. The permissions a service principal needs to translate clientID to objectID for other SPs is astronomical. + # applicationID: + # objectID: permissions: keys: # backup create decrypt delete encrypt get import list purge recover restore sign unwrapKey update verify wrapKey - list @@ -37,4 +42,4 @@ spec: - get storage: # backup delete deleteas get getas list listsas purge recover regeneratekey restore set setas update - list - - get \ No newline at end of file + - get diff --git a/controllers/appinsightsapikey_controller.go b/controllers/appinsightsapikey_controller.go new file mode 100644 index 00000000000..9f226c05ec0 --- /dev/null +++ b/controllers/appinsightsapikey_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" +) + +// AppInsightsApiKeyReconciler reconciles a AppInsightsApiKey object +type AppInsightsApiKeyReconciler struct { + Reconciler *AsyncReconciler +} + +// +kubebuilder:rbac:groups=azure.microsoft.com,resources=appinsightsapikeys,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=azure.microsoft.com,resources=appinsightsapikeys/status,verbs=get;update;patch + +func (r *AppInsightsApiKeyReconciler) Reconcile(req ctrl.Request) (ctrl.Result, error) { + return r.Reconciler.Reconcile(req, &azurev1alpha1.AppInsightsApiKey{}) +} + +func (r *AppInsightsApiKeyReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&azurev1alpha1.AppInsightsApiKey{}). + Complete(r) +} diff --git a/controllers/appinsightsapikey_controller_test.go b/controllers/appinsightsapikey_controller_test.go new file mode 100644 index 00000000000..f1d20735df3 --- /dev/null +++ b/controllers/appinsightsapikey_controller_test.go @@ -0,0 +1,58 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +// +build all appinsights + +package controllers + +import ( + "context" + "testing" + + azurev1alpha1 "github.com/Azure/azure-service-operator/api/v1alpha1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func TestAppInsightsApiKeyController(t *testing.T) { + t.Parallel() + defer PanicRecover(t) + ctx := context.Background() + + rgName := tc.resourceGroupName + rgLocation := tc.resourceGroupLocation + appInsightsName := GenerateTestResourceName("appinsights2") + appInsightsKeyName := GenerateTestResourceName("insightskey") + + // Create an instance of Azure AppInsights + appInsightsInstance := &azurev1alpha1.AppInsights{ + ObjectMeta: metav1.ObjectMeta{ + Name: appInsightsName, + Namespace: "default", + }, + Spec: azurev1alpha1.AppInsightsSpec{ + Kind: "web", + Location: rgLocation, + ResourceGroup: rgName, + ApplicationType: "other", + }, + } + + EnsureInstance(ctx, t, tc, appInsightsInstance) + + apiKey := &azurev1alpha1.AppInsightsApiKey{ + ObjectMeta: metav1.ObjectMeta{ + Name: appInsightsKeyName, + Namespace: "default", + }, + Spec: azurev1alpha1.AppInsightsApiKeySpec{ + AppInsights: appInsightsName, + ResourceGroup: rgName, + ReadTelemetry: true, + WriteAnnotations: true, + }, + } + + EnsureInstance(ctx, t, tc, apiKey) + EnsureDelete(ctx, t, tc, apiKey) + EnsureDelete(ctx, t, tc, appInsightsInstance) +} diff --git a/controllers/suite_test.go b/controllers/suite_test.go index b9e6db85a2b..acafff6f731 100644 --- a/controllers/suite_test.go +++ b/controllers/suite_test.go @@ -207,6 +207,25 @@ func setup() error { return err } + err = (&AppInsightsApiKeyReconciler{ + Reconciler: &AsyncReconciler{ + Client: k8sManager.GetClient(), + AzureClient: resourcemanagerappinsights.NewAPIKeyClient( + secretClient, + scheme.Scheme, + ), + Telemetry: telemetry.InitializeTelemetryDefault( + "AppInsightsApiKey", + ctrl.Log.WithName("controllers").WithName("AppInsightsApiKey"), + ), + Recorder: k8sManager.GetEventRecorderFor("AppInsightsApiKey-controller"), + Scheme: scheme.Scheme, + }, + }).SetupWithManager(k8sManager) + if err != nil { + return err + } + err = (&APIMAPIReconciler{ Reconciler: &AsyncReconciler{ Client: k8sManager.GetClient(), diff --git a/main.go b/main.go index 20da1712aea..ab577f58258 100644 --- a/main.go +++ b/main.go @@ -860,6 +860,22 @@ func main() { os.Exit(1) } + if err = (&controllers.AppInsightsApiKeyReconciler{ + Reconciler: &controllers.AsyncReconciler{ + Client: mgr.GetClient(), + AzureClient: resourcemanagerappinsights.NewAPIKeyClient(secretClient, scheme), + Telemetry: telemetry.InitializeTelemetryDefault( + "AppInsightsApiKey", + ctrl.Log.WithName("controllers").WithName("AppInsightsApiKey"), + ), + Recorder: mgr.GetEventRecorderFor("AppInsightsApiKey-controller"), + Scheme: scheme, + }, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "AppInsightsApiKey") + os.Exit(1) + } + if err = (&v1alpha1.AzureSqlServer{}).SetupWebhookWithManager(mgr); err != nil { setupLog.Error(err, "unable to create webhook", "webhook", "AzureSqlServer") os.Exit(1) @@ -889,6 +905,7 @@ func main() { setupLog.Error(err, "unable to create webhook", "webhook", "PostgreSQLServer") os.Exit(1) } + // +kubebuilder:scaffold:builder setupLog.Info("starting manager") diff --git a/pkg/errhelp/errhelp.go b/pkg/errhelp/errhelp.go index ab377ae596f..99a9482f675 100644 --- a/pkg/errhelp/errhelp.go +++ b/pkg/errhelp/errhelp.go @@ -9,14 +9,34 @@ import ( "strings" ) +// ErrIdsRegex is used to find and remove uuids from errors +var ErrIdsRegex *regexp.Regexp + +// ErrTimesRegex allows timestamp seconds to be removed from error strings +var ErrTimesRegex *regexp.Regexp + // StripErrorIDs takes an error and returns its string representation after filtering some common ID patterns func StripErrorIDs(err error) string { patterns := []string{ "RequestID=", "CorrelationId:\\s", "Tracking ID: ", + "requestId", + } + + if ErrIdsRegex == nil { + ErrIdsRegex = regexp.MustCompile(fmt.Sprintf(`(%s)\S+`, strings.Join(patterns, "|"))) + } + + return ErrIdsRegex.ReplaceAllString(err.Error(), "") + +} + +// StripErrorTimes removes the hours:minutes:seconds from a date to prevent updates to Status.Message from changing unnecessarily +func StripErrorTimes(err string) string { + if ErrTimesRegex == nil { + ErrTimesRegex = regexp.MustCompile(`(T\d\d:\d\d:\d\d)\"`) } - reg := regexp.MustCompile(fmt.Sprintf(`(%s)\S+`, strings.Join(patterns, "|"))) - return reg.ReplaceAllString(err.Error(), "") + return ErrTimesRegex.ReplaceAllString(err, "") } diff --git a/pkg/resourcemanager/appinsights/api_keys_client.go b/pkg/resourcemanager/appinsights/api_keys_client.go new file mode 100644 index 00000000000..3266b79c986 --- /dev/null +++ b/pkg/resourcemanager/appinsights/api_keys_client.go @@ -0,0 +1,140 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package appinsights + +import ( + "context" + "fmt" + + "github.com/Azure/azure-sdk-for-go/services/appinsights/mgmt/2015-05-01/insights" + "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 InsightsAPIKeysClient struct { + SecretClient secrets.SecretClient + Scheme *runtime.Scheme +} + +func NewAPIKeyClient(secretClient secrets.SecretClient, scheme *runtime.Scheme) *InsightsAPIKeysClient { + return &InsightsAPIKeysClient{ + SecretClient: secretClient, + Scheme: scheme, + } +} + +func getApiKeysClient() (insights.APIKeysClient, error) { + insightsClient := insights.NewAPIKeysClientWithBaseURI(config.BaseURI(), config.SubscriptionID()) + a, err := iam.GetResourceManagementAuthorizer() + if err != nil { + insightsClient = insights.APIKeysClient{} + return insights.APIKeysClient{}, err + } + + insightsClient.Authorizer = a + insightsClient.AddToUserAgent(config.UserAgent()) + + return insightsClient, err +} + +func (c *InsightsAPIKeysClient) CreateKey(ctx context.Context, resourceGroup, insightsaccount, name string, read, write, authSDK bool) (insights.ApplicationInsightsComponentAPIKey, error) { + apiKey := insights.ApplicationInsightsComponentAPIKey{} + + client, err := getApiKeysClient() + if err != nil { + return apiKey, err + } + + readIds := []string{ + fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/microsoft.insights/components/%s/api", config.SubscriptionID(), resourceGroup, insightsaccount), + fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/microsoft.insights/components/%s/draft", config.SubscriptionID(), resourceGroup, insightsaccount), + fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/microsoft.insights/components/%s/extendqueries", config.SubscriptionID(), resourceGroup, insightsaccount), + fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/microsoft.insights/components/%s/search", config.SubscriptionID(), resourceGroup, insightsaccount), + fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/microsoft.insights/components/%s/aggregate", config.SubscriptionID(), resourceGroup, insightsaccount), + } + + writeIds := []string{ + fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/microsoft.insights/components/%s/annotations", config.SubscriptionID(), resourceGroup, insightsaccount), + } + + authSDKControl := []string{fmt.Sprintf("/subscriptions/%s/resourceGroups/%s/providers/microsoft.insights/components/%s/agentconfig", config.SubscriptionID(), resourceGroup, insightsaccount)} + + keyprops := insights.APIKeyRequest{ + Name: &name, + } + + if read { + keyprops.LinkedReadProperties = &readIds + } + + if write { + keyprops.LinkedWriteProperties = &writeIds + } + + if authSDK { + if keyprops.LinkedReadProperties == nil { + keyprops.LinkedReadProperties = &authSDKControl + } else { + combined := append(*keyprops.LinkedReadProperties, authSDKControl...) + keyprops.LinkedReadProperties = &combined + } + } + + apiKey, err = client.Create( + ctx, + resourceGroup, + insightsaccount, + keyprops, + ) + if err != nil { + return apiKey, err + } + + return apiKey, nil +} + +func (c *InsightsAPIKeysClient) DeleteKey(ctx context.Context, resourceGroup, insightsaccount, name string) error { + client, err := getApiKeysClient() + if err != nil { + return err + } + + _, err = client.Delete(ctx, resourceGroup, insightsaccount, name) + if err != nil { + return err + } + return nil +} + +func (c *InsightsAPIKeysClient) GetKey(ctx context.Context, resourceGroup, insightsaccount, name string) (insights.ApplicationInsightsComponentAPIKey, error) { + result := insights.ApplicationInsightsComponentAPIKey{} + client, err := getApiKeysClient() + if err != nil { + return result, err + } + + result, err = client.Get(ctx, resourceGroup, insightsaccount, name) + if err != nil { + return result, err + } + + return result, nil +} + +func (c *InsightsAPIKeysClient) ListKeys(ctx context.Context, resourceGroup, insightsaccount string) (insights.ApplicationInsightsComponentAPIKeyListResult, error) { + result := insights.ApplicationInsightsComponentAPIKeyListResult{} + client, err := getApiKeysClient() + if err != nil { + return result, err + } + + result, err = client.List(ctx, resourceGroup, insightsaccount) + if err != nil { + return result, err + } + + return result, nil +} diff --git a/pkg/resourcemanager/appinsights/api_keys_reconcile.go b/pkg/resourcemanager/appinsights/api_keys_reconcile.go new file mode 100644 index 00000000000..a283462cb51 --- /dev/null +++ b/pkg/resourcemanager/appinsights/api_keys_reconcile.go @@ -0,0 +1,198 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +package appinsights + +import ( + "context" + "fmt" + "net/http" + "strings" + + "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" + "github.com/Azure/azure-service-operator/pkg/secrets" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" +) + +func (c *InsightsAPIKeysClient) Ensure(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { + + instance, err := c.convert(obj) + if err != nil { + return false, err + } + + options := &resourcemanager.Options{} + for _, opt := range opts { + opt(options) + } + + if options.SecretClient != nil { + c.SecretClient = options.SecretClient + } + + instance.Status.Provisioning = true + + // we may have reconciled this previously, check if it already exists + if instance.Status.ResourceId != "" { + idParts := strings.Split(instance.Status.ResourceId, "/") + + _, err := c.GetKey( + ctx, + instance.Spec.ResourceGroup, + instance.Spec.AppInsights, + idParts[len(idParts)-1], + ) + if err == nil { + return true, nil + } + + return false, nil + } + + apiKey, err := c.CreateKey( + ctx, + instance.Spec.ResourceGroup, + instance.Spec.AppInsights, + instance.Name, + instance.Spec.ReadTelemetry, + instance.Spec.WriteAnnotations, + instance.Spec.AuthSDKControlChannel, + ) + if err != nil { + instance.Status.Message = err.Error() + azerr := errhelp.NewAzureErrorAzureError(err) + + // handle errors + switch azerr.Code { + case http.StatusBadRequest: + // if the key already exists it is fine only if the secret exists + if strings.Contains(azerr.Type, "already exists") { + sKey := types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace} + if _, err := c.SecretClient.Get(ctx, sKey); err != nil { + instance.Status.Message = "api key exists but no key could be recovered" + instance.Status.FailedProvisioning = true + } + return true, nil + } + instance.Status.FailedProvisioning = true + return true, nil + case http.StatusNotFound: + return false, nil + } + + return false, fmt.Errorf("api key create error %v", err) + } + + // when create is successful we have to store the apikey somewhere + err = c.SecretClient.Upsert(ctx, + types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace}, + map[string][]byte{"apiKey": []byte(*apiKey.APIKey)}, + secrets.WithOwner(instance), + secrets.WithScheme(c.Scheme), + ) + if err != nil { + instance.Status.Message = "api key created but key was lost before storage" + instance.Status.FailedProvisioning = true + return false, err + } + + // instance.Status.Output = *apiKey.APIKey + instance.Status.Provisioned = true + instance.Status.Provisioning = false + instance.Status.FailedProvisioning = false + instance.Status.Message = resourcemanager.SuccessMsg + instance.Status.ResourceId = *apiKey.ID + + return true, nil +} + +func (c *InsightsAPIKeysClient) Delete(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { + instance, err := c.convert(obj) + if err != nil { + return false, err + } + + // can't delete without an id and it probably wasn't provisioned by us if it's missing + if instance.Status.ResourceId == "" { + return false, nil + } + + idParts := strings.Split(instance.Status.ResourceId, "/") + + err = c.DeleteKey( + ctx, + instance.Spec.ResourceGroup, + instance.Spec.AppInsights, + idParts[len(idParts)-1], + ) + if err != nil { + catch := []string{ + errhelp.ResourceGroupNotFoundErrorCode, + errhelp.AsyncOpIncompleteError, + } + azerr := errhelp.NewAzureErrorAzureError(err) + if helpers.ContainsString(catch, azerr.Type) { + return false, nil + } + + if azerr.Code == http.StatusNotFound { + return false, nil + } + + return true, fmt.Errorf("ResourceGroup delete error %v", err) + + } + + sKey := types.NamespacedName{Name: instance.Name, Namespace: instance.Namespace} + err = c.SecretClient.Delete(ctx, sKey) + if err != nil { + return true, err + } + + return false, nil +} + +func (c *InsightsAPIKeysClient) GetParents(obj runtime.Object) ([]resourcemanager.KubeParent, error) { + i, err := c.convert(obj) + if err != nil { + return nil, err + } + + return []resourcemanager.KubeParent{ + { + Key: types.NamespacedName{ + Namespace: i.Namespace, + Name: i.Spec.AppInsights, + }, + Target: &v1alpha1.AppInsights{}, + }, + { + Key: types.NamespacedName{ + Namespace: i.Namespace, + Name: i.Spec.ResourceGroup, + }, + Target: &v1alpha1.ResourceGroup{}, + }, + }, nil +} + +func (c *InsightsAPIKeysClient) GetStatus(obj runtime.Object) (*v1alpha1.ASOStatus, error) { + instance, err := c.convert(obj) + if err != nil { + return nil, err + } + return &instance.Status, nil +} + +func (c *InsightsAPIKeysClient) convert(obj runtime.Object) (*azurev1alpha1.AppInsightsApiKey, error) { + local, ok := obj.(*azurev1alpha1.AppInsightsApiKey) + if !ok { + return nil, fmt.Errorf("failed type assertion on kind: %s", obj.GetObjectKind().GroupVersionKind().String()) + } + return local, nil +} diff --git a/pkg/resourcemanager/appinsights/appinsights.go b/pkg/resourcemanager/appinsights/appinsights.go index 86b18a6f9d8..a5c9f627be2 100644 --- a/pkg/resourcemanager/appinsights/appinsights.go +++ b/pkg/resourcemanager/appinsights/appinsights.go @@ -63,8 +63,8 @@ func (m *Manager) GetParents(obj runtime.Object) ([]resourcemanager.KubeParent, }, nil } -func (g *Manager) GetStatus(obj runtime.Object) (*v1alpha1.ASOStatus, error) { - instance, err := g.convert(obj) +func (m *Manager) GetStatus(obj runtime.Object) (*v1alpha1.ASOStatus, error) { + instance, err := m.convert(obj) if err != nil { return nil, err } diff --git a/pkg/resourcemanager/keyvaults/keyvault.go b/pkg/resourcemanager/keyvaults/keyvault.go index a98e9bb586d..0df404e904a 100644 --- a/pkg/resourcemanager/keyvaults/keyvault.go +++ b/pkg/resourcemanager/keyvaults/keyvault.go @@ -6,6 +6,7 @@ package keyvaults import ( "context" "fmt" + "net/http" "strings" "time" @@ -48,20 +49,20 @@ func getVaultsClient() (keyvault.VaultsClient, error) { return vaultsClient, nil } -func getObjectID(ctx context.Context, tenantID string, clientID string) *string { +func getObjectID(ctx context.Context, tenantID string, clientID string) (*string, error) { appclient := auth.NewApplicationsClient(tenantID) a, err := iam.GetGraphAuthorizer() if err != nil { - return nil + return nil, err } appclient.Authorizer = a appclient.AddToUserAgent(config.UserAgent()) result, err := appclient.GetServicePrincipalsIDByAppID(ctx, clientID) if err != nil { - return nil + return nil, err } - return result.Value + return result.Value, nil } // ParseNetworkPolicy - helper function to parse network policies from Kubernetes spec @@ -210,9 +211,14 @@ func ParseAccessPolicy(policy *v1alpha1.AccessPolicyEntry, ctx context.Context) } if policy.ClientID != "" { - if objID := getObjectID(ctx, policy.TenantID, policy.ClientID); objID != nil { - newEntry.ObjectID = objID + objID, err := getObjectID(ctx, policy.TenantID, policy.ClientID) + if err != nil { + return keyvault.AccessPolicyEntry{}, err } + newEntry.ObjectID = objID + + } else if policy.ObjectID != "" { + newEntry.ObjectID = &policy.ObjectID } return newEntry, nil @@ -250,7 +256,7 @@ func InstantiateVault(ctx context.Context, vaultName string, containsUpdate bool } // CreateVault creates a new key vault -func (k *azureKeyVaultManager) CreateVault(ctx context.Context, instance *v1alpha1.KeyVault, sku azurev1alpha1.KeyVaultSku, tags map[string]*string) (keyvault.Vault, error) { +func (k *azureKeyVaultManager) CreateVault(ctx context.Context, instance *v1alpha1.KeyVault, sku azurev1alpha1.KeyVaultSku, tags map[string]*string, vaultExists bool) (keyvault.Vault, error) { vaultName := instance.Name location := instance.Spec.Location groupName := instance.Spec.ResourceGroup @@ -290,10 +296,11 @@ func (k *azureKeyVaultManager) CreateVault(ctx context.Context, instance *v1alph keyVaultSku.Name = keyvault.Premium } + pols := []keyvault.AccessPolicyEntry{} params := keyvault.VaultCreateOrUpdateParameters{ Properties: &keyvault.VaultProperties{ TenantID: &id, - AccessPolicies: &accessPolicies, + AccessPolicies: &pols, Sku: &keyVaultSku, NetworkAcls: &networkAcls, EnableSoftDelete: &enableSoftDelete, @@ -302,7 +309,14 @@ func (k *azureKeyVaultManager) CreateVault(ctx context.Context, instance *v1alph Tags: tags, } + if vaultExists { + params.Properties.AccessPolicies = &accessPolicies + } + future, err := vaultsClient.CreateOrUpdate(ctx, groupName, vaultName, params) + if err != nil { + return keyvault.Vault{}, err + } return future.Result(vaultsClient) } @@ -330,7 +344,11 @@ func (k *azureKeyVaultManager) CreateVaultWithAccessPolicies(ctx context.Context }, } if clientID != "" { - if objID := getObjectID(ctx, config.TenantID(), clientID); objID != nil { + objID, err := getObjectID(ctx, config.TenantID(), clientID) + if err != nil { + return keyvault.Vault{}, err + } + if objID != nil { ap.ObjectID = objID apList = append(apList, ap) } @@ -382,22 +400,19 @@ func (k *azureKeyVaultManager) Ensure(ctx context.Context, obj runtime.Object, o return true, err } - // hash the spec and set if new + // hash the spec hash := helpers.Hash256(instance.Spec) - if instance.Status.SpecHash == "" { - instance.Status.SpecHash = hash - } // convert kube labels to expected tag format labels := helpers.LabelsToTags(instance.GetLabels()) instance.Status.Provisioning = true instance.Status.FailedProvisioning = false - + exists := false // Check if this KeyVault already exists and its state if it does. - keyvault, err := k.GetVault(ctx, instance.Spec.ResourceGroup, instance.Name) if err == nil { + exists = true if instance.Status.SpecHash == hash { instance.Status.Message = resourcemanager.SuccessMsg instance.Status.Provisioned = true @@ -408,6 +423,7 @@ func (k *azureKeyVaultManager) Ensure(ctx context.Context, obj runtime.Object, o instance.Status.SpecHash = hash instance.Status.ContainsUpdate = true + } keyvault, err = k.CreateVault( @@ -415,67 +431,85 @@ func (k *azureKeyVaultManager) Ensure(ctx context.Context, obj runtime.Object, o instance, instance.Spec.Sku, labels, + exists, ) - 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, - } - - catchUnrecoverableErrors := []string{ - errhelp.AccountNameInvalid, - errhelp.AlreadyExists, - errhelp.InvalidAccessPolicy, - errhelp.BadRequest, - errhelp.LocationNotAvailableForResourceType, - } - - 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 + done, err := HandleCreationError(instance, err) + if done && exists { + instance.Status.Message = "key vault created but access policies failed: " + instance.Status.Message return false, nil } - if helpers.ContainsString(catchUnrecoverableErrors, azerr.Type) { - // Unrecoverable error, so stop reconcilation - switch azerr.Type { - case errhelp.AlreadyExists: - timeNow := metav1.NewTime(time.Now()) - if timeNow.Sub(instance.Status.RequestedAt.Time) < (30 * time.Second) { - instance.Status.Provisioning = true - return false, nil - } - } - instance.Status.Message = "Reconcilation hit unrecoverable error " + err.Error() - return true, nil - } - // reconciliation not done and we don't know what happened - return false, err + return done, err + } + instance.Status.State = keyvault.Status + if keyvault.ID != nil { + instance.Status.ResourceId = *keyvault.ID } instance.Status.ContainsUpdate = false - instance.Status.State = keyvault.Status - instance.Status.Provisioned = true instance.Status.Provisioning = false instance.Status.Message = resourcemanager.SuccessMsg - instance.Status.ResourceId = *keyvault.ID return true, nil } +func HandleCreationError(instance *v1alpha1.KeyVault, err error) (bool, error) { + // let the user know what happened + instance.Status.Message = errhelp.StripErrorTimes(errhelp.StripErrorIDs(err)) + 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, + } + + catchUnrecoverableErrors := []string{ + errhelp.AccountNameInvalid, + errhelp.AlreadyExists, + errhelp.InvalidAccessPolicy, + errhelp.BadRequest, + errhelp.LocationNotAvailableForResourceType, + } + + 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 + } + + if helpers.ContainsString(catchUnrecoverableErrors, azerr.Type) { + // Unrecoverable error, so stop reconcilation + switch azerr.Type { + case errhelp.AlreadyExists: + timeNow := metav1.NewTime(time.Now()) + if timeNow.Sub(instance.Status.RequestedAt.Time) < (30 * time.Second) { + instance.Status.Provisioning = true + return false, nil + } + + } + instance.Status.Message = "Reconcilation hit unrecoverable error " + err.Error() + return true, nil + } + + if azerr.Code == http.StatusForbidden { + // permission errors when applying access policies are generally worth waiting on + return false, nil + } + + // reconciliation not done and we don't know what happened + return false, err +} + func (k *azureKeyVaultManager) Delete(ctx context.Context, obj runtime.Object, opts ...resourcemanager.ConfigOption) (bool, error) { instance, err := k.convert(obj) if err != nil { diff --git a/pkg/resourcemanager/keyvaults/keyvault_manager.go b/pkg/resourcemanager/keyvaults/keyvault_manager.go index b5681d395cb..2b41acbaf2c 100644 --- a/pkg/resourcemanager/keyvaults/keyvault_manager.go +++ b/pkg/resourcemanager/keyvaults/keyvault_manager.go @@ -16,7 +16,7 @@ import ( var AzureKeyVaultManager KeyVaultManager = &azureKeyVaultManager{} type KeyVaultManager interface { - CreateVault(ctx context.Context, instance *azurev1alpha1.KeyVault, sku azurev1alpha1.KeyVaultSku, tags map[string]*string) (keyvault.Vault, error) + CreateVault(ctx context.Context, instance *azurev1alpha1.KeyVault, sku azurev1alpha1.KeyVaultSku, tags map[string]*string, exists bool) (keyvault.Vault, error) // CreateVault and grant access to the specific user ID CreateVaultWithAccessPolicies(ctx context.Context, groupName string, vaultName string, location string, userID string) (keyvault.Vault, error) diff --git a/pkg/resourcemanager/keyvaults/keyvault_test.go b/pkg/resourcemanager/keyvaults/keyvault_test.go index 1140979d38a..81cc74b1668 100644 --- a/pkg/resourcemanager/keyvaults/keyvault_test.go +++ b/pkg/resourcemanager/keyvaults/keyvault_test.go @@ -71,6 +71,7 @@ var _ = Describe("KeyVault Resource Manager test", func() { &kv, sku, tags, + false, ) if err != nil { fmt.Println(err.Error())