internal/controller/utils.go (291 lines of code) (raw):
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
package controller
import (
acpv1 "azappconfig/provider/api/v1"
"azappconfig/provider/internal/loader"
"encoding/json"
"fmt"
"net/url"
"os"
"reflect"
"strings"
"time"
"github.com/google/uuid"
"gopkg.in/yaml.v2"
corev1 "k8s.io/api/core/v1"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const (
MinimalSentinelBasedRefreshInterval time.Duration = time.Second
MinimalSecretRefreshInterval time.Duration = time.Minute
MinimalFeatureFlagRefreshInterval time.Duration = time.Second
WorkloadIdentityEnabled string = "WORKLOAD_IDENTITY_ENABLED"
WorkloadIdentityGlobalServiceAccountEnabled string = "WORKLOAD_IDENTITY_GLOBAL_SERVICE_ACCOUNT_ENABLED"
)
func verifyObject(spec acpv1.AzureAppConfigurationProviderSpec) error {
var err error
if spec.Endpoint == nil && spec.ConnectionStringReference == nil {
return loader.NewArgumentError("spec", fmt.Errorf("one of endpoint and connectionStringReference field must be set"))
}
if spec.ConnectionStringReference != nil {
if spec.Endpoint != nil {
return loader.NewArgumentError("spec", fmt.Errorf("both endpoint and connectionStringReference field are set"))
}
if spec.Auth != nil {
return loader.NewArgumentError("spec.auth", fmt.Errorf("auth field is not allowed when connectionStringReference field is set"))
}
}
if spec.Target.ConfigMapData != nil {
if spec.Target.ConfigMapData.Type == acpv1.Default {
if spec.Target.ConfigMapData.Key != "" {
return loader.NewArgumentError("spec.target.configMapData.key", fmt.Errorf("key field is not allowed when type is default"))
}
} else {
if spec.Target.ConfigMapData.Key == "" {
return loader.NewArgumentError("spec.target.configMapData.key", fmt.Errorf("key field is required when type is json, yaml or properties"))
}
}
if spec.Target.ConfigMapData.Separator != nil &&
(spec.Target.ConfigMapData.Type == acpv1.Default ||
spec.Target.ConfigMapData.Type == acpv1.Properties) {
return loader.NewArgumentError("spec.target.configMapData.separator", fmt.Errorf("separator field is not allowed when type is %s", spec.Target.ConfigMapData.Type))
}
}
for i := range spec.Configuration.Selectors {
err = verifySelectorObject(spec.Configuration.Selectors[i])
if err != nil {
return loader.NewArgumentError("spec.configuration.selectors", err)
}
}
if spec.FeatureFlag != nil {
if spec.Target.ConfigMapData == nil || spec.Target.ConfigMapData.Type == acpv1.Default || spec.Target.ConfigMapData.Type == acpv1.Properties {
return loader.NewArgumentError("spec.target.configMapData", fmt.Errorf("configMap data type must be json or yaml when FeatureFlag is set"))
}
if len(spec.FeatureFlag.Selectors) == 0 {
return loader.NewArgumentError("spec.featureFlag.selectors", fmt.Errorf("featureFlag.selectors must be specified when FeatureFlag is set"))
}
// Check if feature flag label filters are valid
for i := range spec.FeatureFlag.Selectors {
err = verifySelectorObject(spec.FeatureFlag.Selectors[i])
if err != nil {
return loader.NewArgumentError("spec.featureFlag.selectors", err)
}
}
// Check if feature flag refresh interval is valid
if spec.FeatureFlag.Refresh != nil {
err = verifyRefreshInterval(spec.FeatureFlag.Refresh.Interval, MinimalFeatureFlagRefreshInterval, "featureFlag.refresh.interval")
if err != nil {
return err
}
}
}
if spec.Endpoint != nil {
err = verifyEndpoint(*spec.Endpoint)
if err != nil {
return err
}
}
err = verifyAuthObject(spec.Auth)
if err != nil {
return err
}
if spec.Secret != nil && spec.Secret.Auth != nil {
err = verifyAuthObject(spec.Secret.Auth.AzureAppConfigurationProviderAuth)
if err != nil {
return err
}
for _, v := range spec.Secret.Auth.KeyVaults {
err = verifyEndpoint(v.Uri)
if err != nil {
return err
}
if v.AzureAppConfigurationProviderAuth == nil {
return loader.NewArgumentError("secret.auth.keyVaults", fmt.Errorf("authentication method must be specified for Key Vault '%s'", v.Uri))
}
err = verifyAuthObject(v.AzureAppConfigurationProviderAuth)
if err != nil {
return err
}
}
}
if spec.Configuration.Refresh != nil {
if spec.Configuration.Refresh.Monitoring != nil {
sentinelMap := make(map[acpv1.Sentinel]bool)
for _, sentinel := range spec.Configuration.Refresh.Monitoring.Sentinels {
if _, ok := sentinelMap[sentinel]; ok {
return loader.NewArgumentError("spec.configuration.refresh.monitoring.keyValues", fmt.Errorf("monitoring duplicated key '%s'", sentinel.Key))
}
sentinelMap[sentinel] = true
}
}
if spec.Configuration.Refresh.Interval != "" {
err = verifyRefreshInterval(spec.Configuration.Refresh.Interval, MinimalSentinelBasedRefreshInterval, "configuration.refresh.interval")
if err != nil {
return err
}
}
}
if spec.Secret != nil && spec.Secret.Refresh != nil {
err = verifyRefreshInterval(spec.Secret.Refresh.Interval, MinimalSecretRefreshInterval, "secret.refresh.interval")
if err != nil {
return err
}
}
return nil
}
// verifyEndpoint verifies if the endpoint is a valid key vault endpoint
func verifyEndpoint(endpoint string) error {
url, err := url.Parse(strings.ToLower(endpoint))
if err != nil {
return loader.NewArgumentError("endpoint", err)
}
if url.Host == "" {
return loader.NewArgumentError("endpoint", fmt.Errorf("%q is not a valid endpoint. Host must be specified", endpoint))
}
if url.Scheme != "https" {
return loader.NewArgumentError("endpoint", fmt.Errorf("%q is not a valid endpoint. Only https scheme is allowed", endpoint))
}
if strings.Trim(url.Path, "/") != "" {
return loader.NewArgumentError("endpoint", fmt.Errorf("%q is not a valid endpoint. Only host name is allowed", endpoint))
}
return nil
}
func verifyAuthObject(auth *acpv1.AzureAppConfigurationProviderAuth) error {
if auth != nil {
var authCount int = 0
if auth.ServicePrincipalReference != nil {
authCount++
}
if auth.ManagedIdentityClientId != nil {
authCount++
_, err := uuid.Parse(*auth.ManagedIdentityClientId)
if err != nil {
return loader.NewArgumentError("auth", fmt.Errorf("ManagedIdentityClientId %q in auth field is not a valid uuid", *auth.ManagedIdentityClientId))
}
}
if auth.WorkloadIdentity != nil {
authCount++
err := verifyWorkloadIdentityParameters(auth.WorkloadIdentity)
if err != nil {
return err
}
}
if authCount > 1 {
return loader.NewArgumentError("auth", fmt.Errorf("more than one authentication methods are specified in 'auth' field"))
}
}
return nil
}
func verifyExistingTargetObject[T client.Object](targetObj T, targetName string, providerName string) error {
objectKind := targetObj.GetObjectKind().GroupVersionKind().Kind
if targetObj.GetName() != targetName {
return nil
}
// If existing object is created by current provider, just skip it.
for _, ownerRef := range targetObj.GetOwnerReferences() {
if ownerRef.Name == providerName {
return nil
}
}
return fmt.Errorf("a %s with name '%s' already exists in namespace '%s'", objectKind, targetName, targetObj.GetNamespace())
}
func hasNonEscapedValueInLabel(label string) bool {
length := len(label)
i := 0
for i < length {
if label[i] == '\\' {
i += 2
} else if label[i] == '*' || label[i] == ',' {
return true
} else {
i++
}
}
return false
}
func verifyRefreshInterval(interval string, allowedMinimalRefreshInterval time.Duration, refreshArgument string) error {
refreshInterval, err := time.ParseDuration(interval)
if err == nil {
if refreshInterval < allowedMinimalRefreshInterval {
return loader.NewArgumentError(refreshArgument, fmt.Errorf("%s can not be shorter than %s", refreshArgument, allowedMinimalRefreshInterval.String()))
}
} else {
return loader.NewArgumentError(refreshArgument, err)
}
return nil
}
func verifyWorkloadIdentityParameters(workloadIdentity *acpv1.WorkloadIdentityParameters) error {
if !strings.EqualFold(os.Getenv(WorkloadIdentityEnabled), "true") {
return loader.NewArgumentError("auth.workloadIdentity", fmt.Errorf("workloadIdentity is not enabled"))
}
var authCount int = 0
if workloadIdentity.ManagedIdentityClientId != nil {
if strings.EqualFold(os.Getenv(WorkloadIdentityGlobalServiceAccountEnabled), "false") {
return loader.NewArgumentError("auth.workloadIdentity.managedIdentityClientId", fmt.Errorf("using a global service account is no longer permitted with workload identity. See https://aka.ms/appconfig/k8sglobalserviceaccount for more information"))
}
authCount++
}
if workloadIdentity.ManagedIdentityClientIdReference != nil {
if strings.EqualFold(os.Getenv(WorkloadIdentityGlobalServiceAccountEnabled), "false") {
return loader.NewArgumentError("auth.workloadIdentity.managedIdentityClientIdReference", fmt.Errorf("using a global service account is no longer permitted with workload identity. See https://aka.ms/appconfig/k8sglobalserviceaccount for more information"))
}
authCount++
}
if workloadIdentity.ServiceAccountName != nil {
authCount++
}
if authCount == 0 {
return loader.NewArgumentError("auth.workloadIdentity", fmt.Errorf("setting one of 'managedIdentityClientId', 'managedIdentityClientIdReference' or 'serviceAccountName' field is required"))
}
if authCount > 1 {
return loader.NewArgumentError("auth.workloadIdentity", fmt.Errorf("setting only one of 'managedIdentityClientId', 'managedIdentityClientIdReference' or 'serviceAccountName' field is allowed"))
}
if workloadIdentity.ManagedIdentityClientId != nil {
_, err := uuid.Parse(*workloadIdentity.ManagedIdentityClientId)
if err != nil {
return loader.NewArgumentError("auth.workloadIdentity.managedIdentityClientId", fmt.Errorf("managedIdentityClientId %q in auth.workloadIdentity is not a valid uuid", *workloadIdentity.ManagedIdentityClientId))
}
}
return nil
}
func verifySelectorObject(selector acpv1.Selector) error {
if selector.KeyFilter == nil && selector.SnapshotName == nil {
return fmt.Errorf("a selector uses 'labelFilter' but misses the 'keyFilter', 'keyFilter' is required for key-label pair filtering")
}
if selector.SnapshotName != nil {
if selector.KeyFilter != nil {
return fmt.Errorf("set both 'keyFilter' and 'snapshotName' in one selector causes ambiguity, only one of them should be set")
}
if selector.LabelFilter != nil {
return fmt.Errorf("'labelFilter' is not allowed when 'snapshotName' is set")
}
}
if selector.LabelFilter != nil && hasNonEscapedValueInLabel(*selector.LabelFilter) {
return fmt.Errorf("non-escaped reserved wildcard character '*' and multiple labels separator ',' are not supported in label filters")
}
return nil
}
func shouldCreateOrUpdateSecret(processor *AppConfigurationProviderProcessor, secretName string, existingK8sSecrets map[string]corev1.Secret) bool {
// If the secret does not exist, create it
if _, ok := existingK8sSecrets[secretName]; !ok {
return true
}
return !reflect.DeepEqual(processor.Settings.SecretSettings[secretName].Data, existingK8sSecrets[secretName].Data)
}
func shouldCreateOrUpdateConfigMap(existingConfigMap *corev1.ConfigMap, latestConfigMapSettings map[string]string, dataOptions *acpv1.ConfigMapDataOptions) bool {
if existingConfigMap == nil || existingConfigMap.Data == nil {
return true
}
if dataOptions == nil || dataOptions.Type == acpv1.Default || dataOptions.Type == acpv1.Properties {
return !reflect.DeepEqual(existingConfigMap.Data, latestConfigMapSettings)
}
// spec.target.configMapData.key is changed
if _, ok := existingConfigMap.Data[dataOptions.Key]; !ok {
return true
}
var existingSettings, latestSettings map[string]interface{}
if dataOptions.Type == acpv1.Yaml {
_ = yaml.Unmarshal([]byte(existingConfigMap.Data[dataOptions.Key]), &existingSettings)
_ = yaml.Unmarshal([]byte(latestConfigMapSettings[dataOptions.Key]), &latestSettings)
return !reflect.DeepEqual(existingSettings, latestSettings)
}
if dataOptions.Type == acpv1.Json {
_ = json.Unmarshal([]byte(existingConfigMap.Data[dataOptions.Key]), &existingSettings)
_ = json.Unmarshal([]byte(latestConfigMapSettings[dataOptions.Key]), &latestSettings)
return !reflect.DeepEqual(existingSettings, latestSettings)
}
return false
}