internal/controller/appconfigurationprovider_controller.go (432 lines of code) (raw):
// Portions Copyright (c) Microsoft Corporation.
/*
Copyright 2023.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package controller
import (
acpv1 "azappconfig/provider/api/v1"
"azappconfig/provider/internal/loader"
"context"
"errors"
"maps"
"strconv"
"time"
"github.com/Azure/azure-sdk-for-go/sdk/azcore"
corev1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/klog/v2"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/builder"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
"sigs.k8s.io/controller-runtime/pkg/reconcile"
)
// AzureAppConfigurationProviderReconciler reconciles a AzureAppConfigurationProvider object
type AzureAppConfigurationProviderReconciler struct {
client.Client
Scheme *runtime.Scheme
Retriever loader.ConfigurationSettingsRetriever
ProvidersReconcileState map[types.NamespacedName]*ReconciliationState
}
type ReconciliationState struct {
Generation int64
ConfigMapResourceVersion *string
Annotations map[string]string
SentinelETags map[acpv1.Sentinel]*azcore.ETag
KeyValueETags map[acpv1.Selector][]*azcore.ETag
FeatureFlagETags map[acpv1.Selector][]*azcore.ETag
ExistingK8sSecrets map[string]*loader.TargetK8sSecretMetadata
NextKeyValueRefreshReconcileTime metav1.Time
NextSecretReferenceRefreshReconcileTime metav1.Time
NextFeatureFlagRefreshReconcileTime metav1.Time
ClientManager loader.ClientManager
}
const (
ProviderName string = "AzureAppConfigurationProvider"
LastReconcileTimeAnnotation string = "azconfig.io/LastReconcileTime"
SecretReferenceContentType string = "application/vnd.microsoft.appconfig.keyvaultref+json;charset=utf-8"
FeatureFlagContentType string = "application/vnd.microsoft.appconfig.ff+json;charset=utf-8"
HeaderRetryAfter string = "Retry-After"
RequeueReconcileAfter time.Duration = time.Second * 30
RetryAttempt int = 3
DefaultRefreshInterval time.Duration = time.Second * 30
)
//Markers for teaching kubebuiler how generate the rabc manifests, see https://book.kubebuilder.io/reference/markers/rbac.html for detail
//+kubebuilder:rbac:groups=azconfig.io,resources=azureappconfigurationproviders,verbs=get;list;watch;create;update;patch;delete
//+kubebuilder:rbac:groups=azconfig.io,resources=azureappconfigurationproviders/status,verbs=get;update;patch
//+kubebuilder:rbac:groups=azconfig.io,resources=azureappconfigurationproviders/finalizers,verbs=update
//+kubebuilder:rbac:groups=core,resources=configmaps,verbs=get;list;create;update;patch;watch
//+kubebuilder:rbac:groups=core,resources=secrets,verbs=get;list;create;update;delete;patch;watch
// Reconcile is part of the main kubernetes reconciliation loop which aims to
// move the current state of the cluster closer to the desired state.
func (reconciler *AzureAppConfigurationProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
provider := &acpv1.AzureAppConfigurationProvider{}
err := reconciler.Get(ctx, req.NamespacedName, provider)
//Get object, if not exists, exit reconcile
if err != nil && apierrors.IsNotFound(err) {
delete(reconciler.ProvidersReconcileState, req.NamespacedName)
return reconcile.Result{}, nil
} else if err != nil {
klog.ErrorS(err, "Fail to get AzureAppConfigurationProvider object.")
return reconcile.Result{}, err
}
/* Patch the object status when finish processing. */
var patch client.Patch = client.MergeFrom(provider.DeepCopy())
defer func() {
retry := RetryAttempt
patchSuccess := false
for retry > 0 {
err = reconciler.Status().Patch(ctx, provider, patch)
if err != nil {
retry--
} else {
patchSuccess = true
break
}
}
if !patchSuccess {
klog.ErrorS(err, "Fail to patch the status of AzureAppConfigurationProvider.")
}
}()
/* Status initialization and resource object verification. */
if provider.Status.Phase == "" {
provider.Status.Phase = acpv1.PhasePending
}
if provider.Status.Phase == acpv1.PhaseRunning {
klog.V(3).Infof("The reconcile for AzureAppConfigurationProvider '%s' is running, just exit.", provider.Name)
return reconcile.Result{}, nil
}
provider.Status = newProviderStatus(acpv1.PhaseRunning, acpv1.SyncRunningMessage, provider.Status.LastSyncTime, provider.Status.RefreshStatus)
klog.V(3).Infof("Start reconcile AzureAppConfigurationProvider %q in %q namespace ", provider.Name, provider.Namespace)
err = verifyObject(provider.Spec)
if err != nil {
reconciler.logAndSetFailStatus(provider, err)
return reconcile.Result{Requeue: false}, nil
}
existingConfigMap := corev1.ConfigMap{}
isExisting := false
_, err = reconciler.verifyTargetObjectExistence(ctx, provider, &existingConfigMap)
if err != nil {
reconciler.logAndSetFailStatus(provider, err)
return reconcile.Result{Requeue: true, RequeueAfter: RequeueReconcileAfter}, nil
}
existingSecrets := make(map[string]corev1.Secret)
var existingSecret corev1.Secret
if provider.Spec.Secret != nil {
existingSecret = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: provider.Spec.Secret.Target.SecretName,
},
}
isExisting, err = reconciler.verifyTargetObjectExistence(ctx, provider, &existingSecret)
if err != nil {
reconciler.logAndSetFailStatus(provider, err)
return reconcile.Result{Requeue: true, RequeueAfter: RequeueReconcileAfter}, nil
}
if isExisting {
existingSecrets[provider.Spec.Secret.Target.SecretName] = existingSecret
}
}
if reconciler.ProvidersReconcileState[req.NamespacedName] != nil {
for name := range reconciler.ProvidersReconcileState[req.NamespacedName].ExistingK8sSecrets {
if _, ok := existingSecrets[name]; !ok {
existingSecret = corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
}
isExisting, err = reconciler.verifyTargetObjectExistence(ctx, provider, &existingSecret)
if err != nil {
reconciler.logAndSetFailStatus(provider, err)
return reconcile.Result{Requeue: true, RequeueAfter: RequeueReconcileAfter}, nil
}
if isExisting {
existingSecrets[name] = existingSecret
}
}
}
} else {
// Initialize the ReconcileState for the provider
reconciler.ProvidersReconcileState[req.NamespacedName] = &ReconciliationState{
Generation: -1,
ConfigMapResourceVersion: nil,
Annotations: make(map[string]string),
SentinelETags: make(map[acpv1.Sentinel]*azcore.ETag),
KeyValueETags: make(map[acpv1.Selector][]*azcore.ETag),
FeatureFlagETags: make(map[acpv1.Selector][]*azcore.ETag),
ExistingK8sSecrets: make(map[string]*loader.TargetK8sSecretMetadata),
ClientManager: nil,
}
}
// Reset the resource version if the configmap or secret was unexpected deleted
if existingConfigMap.Name == "" {
reconciler.ProvidersReconcileState[req.NamespacedName].ConfigMapResourceVersion = nil
}
if provider.Spec.Secret == nil {
reconciler.ProvidersReconcileState[req.NamespacedName].ExistingK8sSecrets = make(map[string]*loader.TargetK8sSecretMetadata)
} else {
for name := range reconciler.ProvidersReconcileState[req.NamespacedName].ExistingK8sSecrets {
if _, ok := existingSecrets[name]; !ok {
reconciler.ProvidersReconcileState[req.NamespacedName].ExistingK8sSecrets[name].SecretResourceVersion = ""
}
}
}
if reconciler.ProvidersReconcileState[req.NamespacedName].ClientManager == nil ||
reconciler.ProvidersReconcileState[req.NamespacedName].Generation != provider.Generation {
clientManager, err := loader.NewConfigurationClientManager(ctx, *provider)
if err != nil {
reconciler.logAndSetFailStatus(provider, err)
return reconcile.Result{Requeue: true, RequeueAfter: RequeueReconcileAfter}, nil
}
reconciler.ProvidersReconcileState[req.NamespacedName].ClientManager = clientManager
}
/* Create ConfigurationSettingLoader to get the key-value settings from Azure AppConfiguration. */
clientManager := reconciler.ProvidersReconcileState[req.NamespacedName].ClientManager
configLoader, err := loader.NewConfigurationSettingLoader(*provider, clientManager, nil)
if err != nil {
reconciler.logAndSetFailStatus(provider, err)
return reconcile.Result{Requeue: true, RequeueAfter: RequeueReconcileAfter}, nil
}
var retriever loader.ConfigurationSettingsRetriever
if reconciler.Retriever == nil {
retriever = configLoader
} else {
retriever = reconciler.Retriever
}
ctx = context.WithValue(ctx, loader.RequestTracingKey, loader.RequestTracing{
IsStartUp: reconciler.ProvidersReconcileState[req.NamespacedName].ConfigMapResourceVersion == nil,
})
// Initialize the processor setting in this reconcile
processor := &AppConfigurationProviderProcessor{
Context: ctx,
Provider: provider,
Retriever: retriever,
CurrentTime: metav1.Now(),
ReconciliationState: reconciler.ProvidersReconcileState[req.NamespacedName],
Settings: &loader.TargetKeyValueSettings{},
RefreshOptions: NewRefreshOptions(),
SecretReferenceResolver: nil,
}
if err := processor.PopulateSettings(&existingConfigMap, existingSecrets); err != nil {
return reconciler.requeueWhenGetSettingsFailed(provider, err)
}
/* Create ConfigMap from key-value settings */
if processor.RefreshOptions.ConfigMapSettingPopulated {
result, err := reconciler.createOrUpdateConfigMap(ctx, &existingConfigMap, provider, processor.Settings)
if err != nil {
return result, nil
}
}
/* Create secret when there are secret settings */
if processor.RefreshOptions.SecretSettingPopulated {
// Verify the existence of the secret which is not owned by the current provider
for name := range processor.Settings.SecretSettings {
if _, ok := existingSecrets[name]; !ok {
_, err := reconciler.verifyTargetObjectExistence(ctx, provider, &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
},
})
if err != nil {
reconciler.logAndSetFailStatus(provider, err)
return reconcile.Result{Requeue: true, RequeueAfter: RequeueReconcileAfter}, nil
}
}
}
result, err := reconciler.createOrUpdateSecrets(ctx, provider, processor, existingSecrets)
if err != nil {
return result, nil
}
}
// Expel the secrets which are no longer selected by the provider.
if provider.Spec.Secret == nil || processor.RefreshOptions.SecretSettingPopulated {
result, err := reconciler.expelRemovedSecrets(ctx, provider, existingSecrets, processor.Settings.K8sSecrets)
if err != nil {
return result, nil
}
}
/* Finish the reconcile */
provider.Status = newProviderStatus(acpv1.PhaseComplete, acpv1.SyncCompleteMessage, metav1.Now(), provider.Status.RefreshStatus)
return processor.Finish()
}
func (reconciler *AzureAppConfigurationProviderReconciler) verifyTargetObjectExistence(
ctx context.Context,
provider *acpv1.AzureAppConfigurationProvider,
obj client.Object) (bool, error) {
// Get and verify the existing configMap or secret, if there's existing configMap/secret which is not owned by current provider, throw error
var targetName string
if _, ok := obj.(*corev1.ConfigMap); ok {
targetName = provider.Spec.Target.ConfigMapName
} else if _, ok := obj.(*corev1.Secret); ok {
targetName = obj.GetName()
} else {
// Only verify ConfigMap and Secret object
return false, nil
}
err := reconciler.Client.Get(ctx, types.NamespacedName{Namespace: provider.Namespace, Name: targetName}, obj)
if err != nil {
if apierrors.IsNotFound(err) {
return false, nil
}
return false, err
}
return true, verifyExistingTargetObject(obj, targetName, provider.Name)
}
func (reconciler *AzureAppConfigurationProviderReconciler) logAndSetFailStatus(
provider *acpv1.AzureAppConfigurationProvider,
err error) {
var showErrorAsWarning bool = false
namespacedName := types.NamespacedName{
Name: provider.Name,
Namespace: provider.Namespace,
}
reconcileState := reconciler.ProvidersReconcileState[namespacedName]
if _, ok := err.(*loader.ArgumentError); ok {
// If the error is caused by invalid argument, just show it as error.
showErrorAsWarning = false
} else if reconcileState != nil &&
reconcileState.ConfigMapResourceVersion != nil &&
(provider.Spec.Secret == nil ||
len(reconcileState.ExistingK8sSecrets) == 0) {
// If the target ConfigMap or Secret does exists, just show error as warning.
showErrorAsWarning = true
}
if showErrorAsWarning {
klog.Warningf("Fail to update the target ConfigMap or Secret of AzureAppConfigurationProvider '%s' in '%s' namespace: %s", provider.Name, provider.Namespace, err.Error())
provider.Status = newProviderStatus(acpv1.PhaseUpdateFailed, acpv1.UpdateFailMessage, provider.Status.LastSyncTime, provider.Status.RefreshStatus)
} else {
klog.Errorf("Fail to create the target ConfigMap or Secret of AzureAppConfigurationProvider '%s' in '%s' namespace: %s", provider.Name, provider.Namespace, err.Error())
provider.Status = newProviderStatus(acpv1.PhaseFailed, acpv1.CreateFailMessage, provider.Status.LastSyncTime, provider.Status.RefreshStatus)
}
}
func (reconciler *AzureAppConfigurationProviderReconciler) requeueWhenGetSettingsFailed(
provider *acpv1.AzureAppConfigurationProvider,
err error) (ctrl.Result, error) {
requeueAfter := RequeueReconcileAfter
reconciler.logAndSetFailStatus(provider, err)
if errors.Is(err, &loader.ArgumentError{}) {
return reconcile.Result{Requeue: false}, nil
}
var respErr *azcore.ResponseError
if errors.As(err, &respErr) && respErr.StatusCode == 429 {
retryAfter, err := strconv.Atoi(respErr.RawResponse.Header.Get(HeaderRetryAfter))
if err == nil {
requeueAfter = time.Duration(retryAfter) * time.Second
klog.Errorf("Too many requests to the Azure App Configuration endpoint %s, retry the reconciliation after %d seconds", *provider.Spec.Endpoint, retryAfter)
} else {
klog.ErrorS(err, "Fail to parse the response header 'Retry-After'")
}
}
return reconcile.Result{Requeue: true, RequeueAfter: requeueAfter}, nil
}
func (reconciler *AzureAppConfigurationProviderReconciler) createOrUpdateConfigMap(
ctx context.Context,
existingConfigMap *corev1.ConfigMap,
provider *acpv1.AzureAppConfigurationProvider,
settings *loader.TargetKeyValueSettings) (reconcile.Result, error) {
if !shouldCreateOrUpdateConfigMap(existingConfigMap, settings.ConfigMapSettings, provider.Spec.Target.ConfigMapData) {
klog.V(5).Infof("Skip updating the configMap %q in %q namespace since data is not changed", provider.Spec.Target.ConfigMapName, provider.Namespace)
return reconcile.Result{}, nil
}
configMapObj := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: provider.Spec.Target.ConfigMapName,
Namespace: provider.Namespace,
},
}
// Important: set the ownership of configMap
if err := controllerutil.SetControllerReference(provider, configMapObj, reconciler.Scheme); err != nil {
reconciler.logAndSetFailStatus(provider, err)
return reconcile.Result{Requeue: true, RequeueAfter: RequeueReconcileAfter}, err
}
annotations := make(map[string]string)
maps.Copy(annotations, provider.Annotations)
annotations[LastReconcileTimeAnnotation] = metav1.Now().UTC().String()
if len(settings.ConfigMapSettings) == 0 {
klog.V(3).Info("No configMap settings are fetched from Azure AppConfiguration")
}
operationResult, err := ctrl.CreateOrUpdate(ctx, reconciler.Client, configMapObj, func() error {
configMapObj.Data = settings.ConfigMapSettings
configMapObj.Labels = provider.Labels
configMapObj.Annotations = annotations
return nil
})
if err != nil {
reconciler.logAndSetFailStatus(provider, err)
return reconcile.Result{Requeue: true, RequeueAfter: RequeueReconcileAfter}, err
}
namespacedName := types.NamespacedName{
Name: provider.Name,
Namespace: provider.Namespace,
}
reconciler.ProvidersReconcileState[namespacedName].ConfigMapResourceVersion = &configMapObj.ResourceVersion
klog.V(5).Infof("configMap %q in %q namespace is %s", configMapObj.Name, configMapObj.Namespace, string(operationResult))
return reconcile.Result{}, nil
}
func (reconciler *AzureAppConfigurationProviderReconciler) createOrUpdateSecrets(
ctx context.Context,
provider *acpv1.AzureAppConfigurationProvider,
processor *AppConfigurationProviderProcessor,
existingSecrets map[string]corev1.Secret) (reconcile.Result, error) {
if len(processor.Settings.SecretSettings) == 0 {
klog.V(3).Info("No secret settings are fetched from Azure AppConfiguration")
}
namespacedName := types.NamespacedName{
Name: provider.Name,
Namespace: provider.Namespace,
}
for secretName, secret := range processor.Settings.SecretSettings {
if !shouldCreateOrUpdateSecret(processor, secretName, existingSecrets) {
if _, ok := reconciler.ProvidersReconcileState[namespacedName].ExistingK8sSecrets[secretName]; ok {
processor.Settings.K8sSecrets[secretName].SecretResourceVersion = reconciler.ProvidersReconcileState[namespacedName].ExistingK8sSecrets[secretName].SecretResourceVersion
}
klog.V(5).Infof("Skip updating the secret %q in %q namespace since data is not changed", secretName, provider.Namespace)
continue
}
secretObj := &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: secretName,
Namespace: provider.Namespace,
},
Type: secret.Type,
}
// Important: set the ownership of secret
if err := controllerutil.SetControllerReference(provider, secretObj, reconciler.Scheme); err != nil {
reconciler.logAndSetFailStatus(provider, err)
return reconcile.Result{Requeue: true, RequeueAfter: RequeueReconcileAfter}, err
}
annotations := make(map[string]string)
maps.Copy(annotations, provider.Annotations)
annotations[LastReconcileTimeAnnotation] = metav1.Now().UTC().String()
operationResult, err := ctrl.CreateOrUpdate(ctx, reconciler.Client, secretObj, func() error {
secretObj.Data = secret.Data
secretObj.Labels = provider.Labels
secretObj.Annotations = annotations
return nil
})
if err != nil {
reconciler.logAndSetFailStatus(provider, err)
return reconcile.Result{Requeue: true, RequeueAfter: RequeueReconcileAfter}, err
}
processor.Settings.K8sSecrets[secretName].SecretResourceVersion = secretObj.ResourceVersion
klog.V(5).Infof("Secret %q in %q namespace is %s", secretObj.Name, secretObj.Namespace, string(operationResult))
}
return reconcile.Result{}, nil
}
func (reconciler *AzureAppConfigurationProviderReconciler) expelRemovedSecrets(
ctx context.Context,
provider *acpv1.AzureAppConfigurationProvider,
existingSecrets map[string]corev1.Secret,
secretReferences map[string]*loader.TargetK8sSecretMetadata) (reconcile.Result, error) {
for name := range existingSecrets {
if _, ok := secretReferences[name]; !ok {
err := reconciler.Client.Delete(ctx, &corev1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: provider.Namespace,
},
})
if err != nil {
reconciler.logAndSetFailStatus(provider, err)
return reconcile.Result{Requeue: true, RequeueAfter: RequeueReconcileAfter}, err
}
}
}
return reconcile.Result{}, nil
}
func newProviderStatus(
phase acpv1.AppConfigurationSyncPhase,
message string,
syncTime metav1.Time,
refreshStatus acpv1.RefreshStatus) acpv1.AzureAppConfigurationProviderStatus {
return acpv1.AzureAppConfigurationProviderStatus{
Message: message,
Phase: phase,
LastReconcileTime: metav1.Now(),
LastSyncTime: syncTime,
RefreshStatus: refreshStatus,
}
}
// SetupWithManager sets up the controller with the Manager.
func (r *AzureAppConfigurationProviderReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&acpv1.AzureAppConfigurationProvider{}, builder.WithPredicates(newEventFilter())).
Watches(&corev1.ConfigMap{},
&EnqueueRequestsFromWatchedObject{},
builder.WithPredicates(WatchedObjectPredicate{})).
Watches(&corev1.Secret{},
&EnqueueRequestsFromWatchedObject{},
builder.WithPredicates(WatchedObjectPredicate{})).
Complete(r)
}