...

Source file src/edge-infra.dev/pkg/edge/iam/ctl/providerctl/provider_controller.go

Documentation: edge-infra.dev/pkg/edge/iam/ctl/providerctl

     1  /*
     2  Copyright 2022.
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package providerctl
    18  
    19  import (
    20  	"bytes"
    21  	"context"
    22  	"fmt"
    23  	"os"
    24  	"path/filepath"
    25  	"reflect"
    26  	"sort"
    27  	"strconv"
    28  	"strings"
    29  	"time"
    30  
    31  	"github.com/fluxcd/pkg/ssa"
    32  	"github.com/go-logr/logr"
    33  	"k8s.io/apimachinery/pkg/api/meta"
    34  	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
    35  	"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
    36  	"k8s.io/apimachinery/pkg/runtime"
    37  
    38  	"sigs.k8s.io/cli-utils/pkg/kstatus/polling"
    39  	ctrl "sigs.k8s.io/controller-runtime"
    40  	"sigs.k8s.io/controller-runtime/pkg/client"
    41  	logger "sigs.k8s.io/controller-runtime/pkg/log"
    42  
    43  	"sigs.k8s.io/kustomize/api/hasher"
    44  	kp "sigs.k8s.io/kustomize/api/provider"
    45  	"sigs.k8s.io/kustomize/api/resmap"
    46  	"sigs.k8s.io/kustomize/api/resource"
    47  	"sigs.k8s.io/kustomize/kyaml/resid"
    48  
    49  	apiv1 "k8s.io/api/core/v1"
    50  
    51  	"edge-infra.dev/pkg/edge/info"
    52  	edgeConditions "edge-infra.dev/pkg/k8s/runtime/conditions"
    53  	"edge-infra.dev/pkg/k8s/runtime/controller/metrics"
    54  
    55  	api "edge-infra.dev/pkg/edge/iam/api/v1alpha1"
    56  	"edge-infra.dev/pkg/edge/iam/config"
    57  	ff "edge-infra.dev/pkg/lib/featureflag"
    58  )
    59  
    60  const (
    61  	SecretExistCheckFailure     = "SecretExistCheckFailure"
    62  	SecretCreationFailure       = "SecretCreationFailure"
    63  	MissingSecretData           = "MissingSecretData"
    64  	ExternalProvider            = "external-provider"
    65  	ProviderContainer           = "edge-iam-provider"
    66  	DaemonSet                   = "DaemonSet"
    67  	Deployment                  = "Deployment"
    68  	ClientCRDName               = "Client"
    69  	ClientCRDVersion            = "v1alpha1"
    70  	ClientCRDGroup              = "iam.edge-infra.dev"
    71  	TouchpointProvider          = "touchpoint-provider"
    72  	StoreProvider               = "store-provider"
    73  	TestClient                  = "test-client"
    74  	TestClientCRName            = "verify"
    75  	TestClientSecretName        = "verify-creds"
    76  	EncryptionRotationSucceeded = "EncryptionRotationSucceeded"
    77  	EdgeIAM                     = "edge-iam"
    78  	EncryptionKeySecretPrefix   = "id-encryption-key-"
    79  )
    80  
    81  var envsToSync = [...]string{
    82  	"IAM_PROFILE_LIFESPAN",
    83  	"IAM_BARCODE_LIFESPAN",
    84  	"FF_BARCODE_ENFORCE_ROLES",
    85  	"FF_ENFORCE_PERMISSIONS",
    86  	"IAM_PIN_RETRY_THRESHOLD",
    87  	"IAM_PIN_HISTORY_LENGTH",
    88  	"IAM_PIN_LIFESPAN",
    89  }
    90  
    91  // ProviderReconciler reconciles a Provider object
    92  type ProviderReconciler struct {
    93  	client.Client
    94  	Scheme *runtime.Scheme
    95  
    96  	// Name is the controller's name, used to consistently represent the controller
    97  	// in various cluster interactions, e.g., as field manager
    98  	Name string
    99  
   100  	// Manifests is ?
   101  	Manifests []*unstructured.Unstructured
   102  
   103  	// ResourceManager is a server-side apply client that can poll for the resources
   104  	// we are applying to the server
   105  	ResourceManager *ssa.ResourceManager
   106  
   107  	// kubebuilder default metrics
   108  	Metrics metrics.Metrics
   109  
   110  	// Resmaps is a map of resmap.Resmap keyed by the target env
   111  	Resmaps map[string]resmap.ResMap
   112  }
   113  
   114  //+kubebuilder:rbac:groups=iam.edge-infra.dev,resources=providers,verbs=get;list;watch;create;update;patch;delete
   115  //+kubebuilder:rbac:groups=iam.edge-infra.dev,resources=providers/status,verbs=get;update;patch
   116  //+kubebuilder:rbac:groups=iam.edge-infra.dev,resources=providers/finalizers,verbs=update
   117  
   118  // +kubebuilder:rbac:groups="",resources=pods;serviceaccounts;secrets;configmaps;services;namespaces,verbs=create;get;list;update;patch;watch;delete
   119  // +kubebuilder:rbac:groups="",resources=services/status;namespaces/status,verbs=get
   120  
   121  // +kubebuilder:rbac:groups="apps",resources=deployments;daemonsets;statefulsets;replicasets,verbs=create;get;list;update;patch;watch;delete
   122  // +kubebuilder:rbac:groups="getambassador.io",resources=mappings,verbs=create;get;list;update;patch;watch;delete
   123  // +kubebuilder:rbac:groups="policy.linkerd.io",resources=servers;serverauthorizations,verbs=create;get;list;update;patch;watch;delete
   124  // +kubebuilder:rbac:groups="external-secrets.io",resources=externalsecrets,verbs=get;watch;create;patch;update;list;delete
   125  // +kubebuilder:rbac:groups="batch",resources=cronjobs,verbs=create;list;patch
   126  
   127  // +kubebuilder:rbac:groups="monitoring.coreos.com",resources=servicemonitors,verbs=create;get;list;update;patch;watch;delete
   128  
   129  // +kubebuilder:rbac:groups="edge.ncr.com",resources=persistence,verbs=get;watch;create;patch;update;list
   130  
   131  // Reconcile is part of the main kubernetes reconciliation loop which aims to
   132  // move the current state of the cluster closer to the desired state.
   133  // TODO(user): Modify the Reconcile function to compare the state specified by
   134  // the Provider object against the actual cluster state, and then
   135  // perform operations to make the cluster state reflect the state specified by
   136  // the user.
   137  //
   138  // For more details, check Reconcile and its Result here:
   139  // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.11.2/pkg/reconcile
   140  func (r *ProviderReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
   141  	var (
   142  		reconcileStart = time.Now()
   143  		log            = logger.FromContext(ctx)
   144  		provider       = api.Provider{}
   145  	)
   146  	//log := logger.FromContext(ctx)
   147  	log.V(2).Info("reconcile provider")
   148  
   149  	defer func() {
   150  		r.Metrics.RecordReadiness(ctx, &provider)
   151  		r.Metrics.RecordReconciling(ctx, &provider)
   152  		r.Metrics.RecordStalled(ctx, &provider)
   153  		r.Metrics.RecordDuration(ctx, &provider, reconcileStart)
   154  	}()
   155  
   156  	// used to retain ownership on resources (so it doesnt get fluxed away)
   157  	r.setResourceManager()
   158  
   159  	// try to get the provider to reconcile
   160  
   161  	err := r.Get(ctx, req.NamespacedName, &provider)
   162  	if err != nil {
   163  		return ctrl.Result{}, err
   164  	}
   165  
   166  	// reconcile the provider object, creating a copy so that we avoid mutating our
   167  	// controller's cache
   168  	reconciled, reconcileErr := r.reconcile(ctx, req, *provider.DeepCopy())
   169  
   170  	// reflect the reconciled status on the API server
   171  	if err := r.updateStatus(ctx, req, reconciled.Status); err != nil {
   172  		log.Error(err, "unable to update status")
   173  		return ctrl.Result{Requeue: true}, nil
   174  	}
   175  
   176  	// if reconciliation errors occur, return them and requeue so we re-try
   177  	// if there are reconciliation errors that are nonrecoverable, we can handle
   178  	// them here by setting Requeue: false conditionally
   179  	if reconcileErr != nil {
   180  		log.Error(reconcileErr, "error reconciling provider resource")
   181  		return ctrl.Result{Requeue: true}, nil
   182  	}
   183  
   184  	// by default, we want to reconcile every 5 minutes to be sure that our
   185  	// linkerd installation manifests are not drifting or deleted by another
   186  	// actor on the cluster
   187  	return ctrl.Result{RequeueAfter: 5 * time.Minute}, nil
   188  }
   189  
   190  // SetupWithManager sets up the controller with the Manager.
   191  func (r *ProviderReconciler) SetupWithManager(mgr ctrl.Manager) error {
   192  	return ctrl.NewControllerManagedBy(mgr).
   193  		// TODO should we builder.WithPredicates(predicate.GenerationChangedPredicate{}?
   194  		For(&api.Provider{}).
   195  		Complete(r)
   196  }
   197  
   198  func (r *ProviderReconciler) setResourceManager() {
   199  	if r.ResourceManager == nil {
   200  		mgr := ssa.NewResourceManager(
   201  			r.Client,
   202  			// be sure to consistently communicate this controllers ownership of objects
   203  			// this should match the result of createOpts()
   204  			polling.NewStatusPoller(r.Client, r.Client.RESTMapper(), polling.Options{}), ssa.Owner{Field: r.Name, Group: config.Domain()},
   205  		)
   206  		r.ResourceManager = mgr
   207  	}
   208  }
   209  
   210  func (r *ProviderReconciler) reconcile(ctx context.Context, req ctrl.Request, provider api.Provider) (api.Provider, error) {
   211  	var err error
   212  	// provider, err = r.reconcileNamespace(ctx, provider)
   213  	// if err != nil {
   214  	// 	return provider, err
   215  	// }
   216  
   217  	provider, err = r.reconcileSecrets(ctx, req, provider)
   218  	if err != nil {
   219  		return provider, err
   220  	}
   221  
   222  	provider, err = r.reconcileEncryptionKeyRotation(ctx, provider)
   223  	if err != nil {
   224  		return provider, err
   225  	}
   226  
   227  	provider, err = r.reconcileTarget(ctx, provider)
   228  	if err != nil {
   229  		return provider, err
   230  	}
   231  
   232  	// reconciled without errors..
   233  	if !meta.IsStatusConditionFalse(provider.Status.Conditions, api.ReadyCondition) {
   234  		return api.Ready(provider, "InstallationSucceeded", "successfully installed the provider"), nil
   235  	}
   236  
   237  	return api.Provider{}, nil
   238  }
   239  
   240  func (r *ProviderReconciler) reconcileSecrets(ctx context.Context, req ctrl.Request, provider api.Provider) (api.Provider, error) {
   241  	var pkSecret, challengeSecret *apiv1.Secret
   242  	var err error
   243  
   244  	log := logger.FromContext(ctx)
   245  	provider, pkSecret, err = r.reconcilePrivateKeysSecret(ctx, req, provider)
   246  	if err != nil {
   247  		return provider, err
   248  	}
   249  
   250  	// secret exists, let's validate it
   251  	provider, err = r.validatePrivateKeysSecret(provider, pkSecret)
   252  	if err != nil {
   253  		log.Error(err, fmt.Sprintf("secret %s/%s is invalid", pkSecret.Name, pkSecret.Namespace))
   254  		return provider, err
   255  	}
   256  
   257  	provider, challengeSecret, err = r.reconcileChallengeSecret(ctx, req, provider)
   258  	if err != nil {
   259  		return provider, err
   260  	}
   261  
   262  	// secret exists, let's validate it
   263  	provider, err = r.validateChallengeSecret(provider, challengeSecret)
   264  	if err != nil {
   265  		log.Error(err, fmt.Sprintf("secret %s/%s is invalid", challengeSecret.Name, challengeSecret.Namespace))
   266  		return provider, err
   267  	}
   268  
   269  	return provider, nil
   270  }
   271  
   272  // func (r *ProviderReconciler) reconcileNamespace(ctx context.Context, provider api.Provider) (api.Provider, error) {
   273  // 	namespace := &corev1.Namespace{
   274  // 		ObjectMeta: metav1.ObjectMeta{
   275  // 			Name:            config.Namespace(),
   276  // 			OwnerReferences: r.ownerRef(&provider),
   277  // 			Labels: map[string]string{
   278  // 				// this gets us our pull-secret
   279  // 				"workload.edge.ncr.com": "platform",
   280  // 			},
   281  // 		},
   282  // 	}
   283  
   284  // 	options := &client.CreateOptions{
   285  // 		FieldManager: config.FieldManager(r.Name),
   286  // 	}
   287  
   288  // 	err := r.Create(ctx, namespace, options)
   289  // 	if err != nil && !errors.IsAlreadyExists(err) {
   290  // 		return api.NotReady(provider, "NamespaceCreationFailed", err.Error()), err
   291  // 	}
   292  
   293  // 	return provider, nil
   294  // }
   295  
   296  func (r *ProviderReconciler) deleteResource(ctx context.Context, resourceName string, gvk resid.Gvk, resMap resmap.ResMap, provider api.Provider) error {
   297  	resourceID := resid.ResId{
   298  		Gvk:       gvk,
   299  		Name:      resourceName,
   300  		Namespace: provider.Namespace,
   301  	}
   302  
   303  	objectToBeDeleted, err := resMap.GetById(resourceID)
   304  	if err != nil {
   305  		return err
   306  	}
   307  
   308  	resource, err := objectToBeDeleted.AsYAML()
   309  	if err != nil {
   310  		return err
   311  	}
   312  
   313  	manifests, err := ssa.ReadObjects(bytes.NewReader(resource))
   314  	if err != nil {
   315  		return err
   316  	}
   317  
   318  	sort.Sort(ssa.SortableUnstructureds(manifests))
   319  
   320  	for i := range manifests {
   321  		manifests[i].SetOwnerReferences(r.ownerRef(&provider))
   322  	}
   323  
   324  	_, err = r.ResourceManager.DeleteAll(ctx, manifests, ssa.DefaultDeleteOptions())
   325  	if err != nil {
   326  		return err
   327  	}
   328  
   329  	err = resMap.Remove(resourceID)
   330  	return err
   331  }
   332  
   333  func (r *ProviderReconciler) reconcileTarget(ctx context.Context, provider api.Provider) (api.Provider, error) {
   334  	log := logger.FromContext(ctx)
   335  
   336  	target := provider.Spec.Target
   337  	if target == "d-edge-lab" {
   338  		target = ""
   339  	}
   340  	// if spec.target is empty, detect the cluster type by retrieving edge-info
   341  	// apply target env based on cluster types
   342  	if target == "" {
   343  		clusterType := ""
   344  		if einfo, _ := info.FromClient(ctx, r.Client); einfo != nil {
   345  			clusterType = einfo.ClusterType
   346  		}
   347  		switch clusterType {
   348  		case "dsds":
   349  			target = "d-edge"
   350  		case "sds":
   351  			target = "edge"
   352  		default:
   353  			target = "generic"
   354  		}
   355  	}
   356  
   357  	log.V(2).Info("reconcile target", "target", target)
   358  
   359  	if !config.TestClientEnabled() { //nolint
   360  
   361  		// Check if copy already exists if it does use that instead of removing test-client resources again.
   362  		_, ok := r.Resmaps[target+"-without-test-client"]
   363  
   364  		// Create resmap copy without test client resources.
   365  		if !ok {
   366  			orgResMap, ok := r.Resmaps[target]
   367  			if !ok {
   368  				return api.NotReady(provider, "ResMapFailed", fmt.Sprintf("no resources found for target %s", target)), fmt.Errorf("no resources found for target %s", target)
   369  			}
   370  
   371  			// Create a copy that can be used by reconiliations (for create test client false) after the first one for optimization.
   372  			noTestClientResMap := orgResMap.DeepCopy()
   373  
   374  			err := r.deleteResource(ctx, TestClient, resid.Gvk{Version: "v1", Kind: "ServiceAccount"}, noTestClientResMap, provider)
   375  			if err != nil {
   376  				log.Error(err, "delete test client resource: no resources found for ServiceAccount test-client")
   377  			}
   378  
   379  			err = r.deleteResource(ctx, TestClient, resid.Gvk{Version: "v1", Kind: "Service"}, noTestClientResMap, provider)
   380  			if err != nil {
   381  				log.Error(err, "delete test client resource: no resources found for Service test-client")
   382  			}
   383  
   384  			err = r.deleteResource(ctx, TestClient, resid.Gvk{Version: "v1", Kind: Deployment, Group: "apps"}, noTestClientResMap, provider)
   385  			if err != nil {
   386  				log.Error(err, "delete test client resource: no resources found for Deployment test-client")
   387  			}
   388  
   389  			err = r.deleteResource(ctx, TestClientCRName, resid.Gvk{Version: ClientCRDVersion, Kind: ClientCRDName, Group: ClientCRDGroup}, noTestClientResMap, provider)
   390  			if err != nil {
   391  				log.Error(err, "delete test client resource: no resources found for Client CR of test-client")
   392  			}
   393  
   394  			err = r.deleteResource(ctx, TestClientSecretName, resid.Gvk{Version: "v1", Kind: "Secret"}, noTestClientResMap, provider)
   395  			if err != nil {
   396  				log.Error(err, "delete test client resource: no resources found for secret of test-client")
   397  			}
   398  			r.Resmaps[target+"-without-test-client"] = noTestClientResMap
   399  		}
   400  
   401  		target = target + "-without-test-client"
   402  	} else {
   403  		delete(r.Resmaps, target+"-without-test-client")
   404  	}
   405  
   406  	resMap, ok := r.Resmaps[target]
   407  	if !ok {
   408  		return api.NotReady(provider, "ResMapFailed", fmt.Sprintf("no resources found for target %s", target)), fmt.Errorf("no resources found for target %s", target)
   409  	}
   410  
   411  	syncDeploymentErr := updateEnvVarsForResource("deployment", provider, resMap, log)
   412  	if syncDeploymentErr != nil {
   413  		log.Info("failed to sync deployment environment variables with provider", "error", syncDeploymentErr)
   414  	}
   415  
   416  	syncDaemonsetErr := updateEnvVarsForResource("daemonset", provider, resMap, log)
   417  	if syncDaemonsetErr != nil {
   418  		log.Info("failed to sync deamonset environment variables with provider", "error", syncDaemonsetErr)
   419  	}
   420  
   421  	resources, err := resMap.AsYaml()
   422  	if err != nil {
   423  		return api.NotReady(provider, "KustomizeToYamlFailed", err.Error()), err
   424  	}
   425  
   426  	r.Manifests, err = ssa.ReadObjects(bytes.NewReader(resources))
   427  	if err != nil {
   428  		return api.NotReady(provider, "SSAReadObjectsFailed", err.Error()), err
   429  	}
   430  	sort.Sort(ssa.SortableUnstructureds(r.Manifests))
   431  
   432  	for i := range r.Manifests {
   433  		r.Manifests[i].SetOwnerReferences(r.ownerRef(&provider))
   434  	}
   435  
   436  	if provider.Spec.Okta.CredsSecretName != "" {
   437  		//oktaAddSecret checks if the manifest needs secretRef for okta and adds if so
   438  		oktaAddSecret(r.Manifests, log)
   439  		//this should override the secret name in the external secret with credsSecretName from provider which contains uuid
   440  		oktaReplaceSecretName(r.Manifests, provider)
   441  	} else {
   442  		//removes okta external secret if credsSecretName is empty
   443  		r.Manifests = oktaRemoveExternalSecret(r.Manifests)
   444  	}
   445  
   446  	// get the encryption version the databases are using (if any)
   447  	version := encryptionGetDatabaseVersion(provider.Status)
   448  	if version != "" {
   449  		//encryptionAddSecretVersion adds the encryption key version to the IAM_ENCRYPTION_KEY env
   450  		r.Manifests = encryptionAddSecretVersion(version, r.Manifests, log)
   451  	} else {
   452  		//removes encryption key secret if encryption is not enabled/no version found
   453  		r.Manifests = encryptionRemoveSecretEnv(r.Manifests, log)
   454  	}
   455  
   456  	// TODO: aggregate errors to reduce `if` spam
   457  	// apply the objects in order based on their dependencies, e.g., Namespaces first
   458  	changed, err := r.ResourceManager.ApplyAllStaged(ctx, r.Manifests, ssa.ApplyOptions{
   459  		Force: true, WaitTimeout: 60 * time.Second,
   460  	})
   461  	if err != nil {
   462  		return api.NotReady(provider, "ResourceManagerApplyFailed", err.Error()), err
   463  	}
   464  
   465  	log.V(2).Info("applied target manifests", "resources", changed.Entries)
   466  
   467  	// wait for them to reconcile
   468  	// placed around if statement so that integration tests dont timeout waiting for api server
   469  	if !config.IsTest() {
   470  		err = r.ResourceManager.WaitForSet(changed.ToObjMetadataSet(), ssa.WaitOptions{
   471  			Interval: 2 * time.Second,
   472  			Timeout:  60 * time.Second,
   473  		})
   474  		if err != nil {
   475  			return api.NotReady(provider, "ResourceManagerTimedOutWaitingForSet", err.Error()), err
   476  		}
   477  	}
   478  
   479  	log.Info("target manifests successfully reconciled")
   480  
   481  	return provider, nil
   482  }
   483  
   484  func oktaAddSecret(manifest []*unstructured.Unstructured, log logr.Logger) {
   485  	for i := range manifest {
   486  		if ((manifest[i].GetName() == StoreProvider) || (manifest[i].GetName() == ExternalProvider) || (manifest[i].GetName() == TouchpointProvider)) &&
   487  			(manifest[i].GetKind() == Deployment || manifest[i].GetKind() == DaemonSet) && manifest[i].GetNamespace() == EdgeIAM {
   488  			containers := manifest[i].Object["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})["containers"].([]interface{})
   489  			for _, container := range containers {
   490  				if container.(map[string]interface{})["name"] != ProviderContainer {
   491  					continue
   492  				}
   493  
   494  				// get the 'envFrom' map
   495  				envFrom := container.(map[string]interface{})["envFrom"].([]interface{})
   496  
   497  				oktaSecretRef := map[string]interface{}{"secretRef": map[string]interface{}{"name": "okta-secret"}}
   498  				envFrom = append(envFrom, oktaSecretRef)
   499  				container.(map[string]interface{})["envFrom"] = envFrom
   500  				log.Info("Added okta secretRef")
   501  			}
   502  		}
   503  	}
   504  }
   505  
   506  func oktaReplaceSecretName(manifest []*unstructured.Unstructured, provider api.Provider) {
   507  	for i := range manifest {
   508  		if (manifest[i].GetName() == "okta-secret") && (manifest[i].GetKind() == "ExternalSecret") {
   509  			datafrom := manifest[i].Object["spec"].(map[string]interface{})["dataFrom"].([]interface{})
   510  			for _, key := range datafrom {
   511  				if key.(map[string]interface{})["extract"].(map[string]interface{})["key"].(string) == "okta-creds-0" {
   512  					key.(map[string]interface{})["extract"].(map[string]interface{})["key"] = provider.Spec.Okta.CredsSecretName
   513  					break
   514  				}
   515  			}
   516  		}
   517  	}
   518  }
   519  
   520  func oktaRemoveExternalSecret(manifests []*unstructured.Unstructured) []*unstructured.Unstructured {
   521  	for i := range manifests {
   522  		if (manifests[i].GetName() == "okta-secret") && (manifests[i].GetKind() == "ExternalSecret") {
   523  			manifests = append(manifests[:i], manifests[i+1:]...)
   524  			break
   525  		}
   526  	}
   527  	return manifests
   528  }
   529  
   530  func getProviderValueFor(provider api.Provider, envName string) string {
   531  	switch envName {
   532  	case "IAM_PIN_RETRY_THRESHOLD":
   533  		if provider.Spec.PIN.Attempts != 0 {
   534  			return strconv.Itoa(int(provider.Spec.PIN.Attempts))
   535  		}
   536  		return ""
   537  	case "IAM_PIN_HISTORY_LENGTH":
   538  		if provider.Spec.PIN.History != 0 {
   539  			return strconv.Itoa(int(provider.Spec.PIN.History))
   540  		}
   541  		return ""
   542  	case "IAM_PIN_LIFESPAN":
   543  		return provider.Spec.PIN.Expire
   544  	case "IAM_PROFILE_LIFESPAN":
   545  		return provider.Spec.Profile.Expire
   546  	case "IAM_BARCODE_LIFESPAN":
   547  		return provider.Spec.Barcode.Expire
   548  	case "FF_BARCODE_ENFORCE_ROLES":
   549  		if provider.Spec.Barcode.Role {
   550  			return "true"
   551  		}
   552  		return ""
   553  	case "FF_ENFORCE_PERMISSIONS":
   554  		if len(provider.Spec.FF) > 0 {
   555  			if v, ok := provider.Spec.FF[ff.EnforceEdgeIDPermissions]; ok {
   556  				return strconv.FormatBool(v)
   557  			}
   558  			return ""
   559  		}
   560  		return ""
   561  	default:
   562  	}
   563  
   564  	return ""
   565  }
   566  
   567  func updateEnvVarsForResource(resType string, provider api.Provider, resMap resmap.ResMap, log logr.Logger) error {
   568  	// define the resource ID
   569  	var gvk resid.Gvk
   570  	var resourceName string
   571  	if resType == "deployment" {
   572  		gvk = resid.Gvk{Version: "v1", Kind: Deployment, Group: "apps"}
   573  		resourceName = StoreProvider
   574  	} else if resType == "daemonset" {
   575  		gvk = resid.Gvk{Version: "v1", Kind: "DaemonSet", Group: "apps"}
   576  		resourceName = TouchpointProvider
   577  	} else {
   578  		return fmt.Errorf("unsupported resource type %s", resType)
   579  	}
   580  
   581  	resourceID := resid.ResId{
   582  		Gvk:       gvk,
   583  		Name:      resourceName,
   584  		Namespace: provider.Namespace,
   585  	}
   586  
   587  	// get the resource
   588  	resource, err := resMap.GetById(resourceID)
   589  	if err != nil {
   590  		log.Info("failed to find the resource in the resources list. will try to sync it again soon.", "error", err)
   591  		return fmt.Errorf("failed to find the resource in the resources list. will try to sync it again soon")
   592  	}
   593  
   594  	// get the unstructured resource so we can edit it
   595  	unstructured, _ := resource.Map()
   596  
   597  	// get the 'containers' part, where the `env` section is located...
   598  	containers, ok := unstructured["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})["containers"].([]interface{})
   599  	if !ok {
   600  		return fmt.Errorf("failed to get containers for %s", resType)
   601  	}
   602  
   603  	for _, container := range containers {
   604  		// make sure we work only on the right container
   605  		if container.(map[string]interface{})["name"] != ProviderContainer {
   606  			continue
   607  		}
   608  
   609  		// get the 'env' map
   610  		env, ok := container.(map[string]interface{})["env"].([]interface{})
   611  		if !ok {
   612  			continue
   613  		}
   614  
   615  		var exists bool
   616  		// iterate the environment variables that we are interested in and look for changes
   617  		for _, envCandidate := range envsToSync {
   618  			exists = false
   619  			for j, e := range env {
   620  				name, ok := e.(map[string]interface{})["name"].(string)
   621  				if ok && name == envCandidate {
   622  					// Update the existing environment variable with the matching value in the provider
   623  					exists = true
   624  					val := getProviderValueFor(provider, name)
   625  					if val != "" {
   626  						e.(map[string]interface{})["value"] = val
   627  					} else {
   628  						env = append(env[:j], env[j+1:]...)
   629  					}
   630  					break
   631  				}
   632  			}
   633  
   634  			if !exists {
   635  				// if the environment variable does not exist, create a new one
   636  				envValue := getProviderValueFor(provider, envCandidate)
   637  				if envValue != "" {
   638  					env = append(env, map[string]interface{}{
   639  						"name":  envCandidate,
   640  						"value": envValue,
   641  					})
   642  				}
   643  				container.(map[string]interface{})["env"] = env
   644  			}
   645  		}
   646  	}
   647  
   648  	// create resource back from the unstructured deployment
   649  	depProvider := kp.NewDefaultDepProvider()
   650  	rf := depProvider.GetResourceFactory()
   651  	updatedResource := rf.FromMap(unstructured)
   652  
   653  	// make sure that the latest resource is set
   654  	_, err = resMap.Replace(updatedResource)
   655  	if err != nil {
   656  		return fmt.Errorf("failed to replace "+resType+"resource. ", err)
   657  	}
   658  
   659  	log.V(2).Info(resType + " is synced with provider")
   660  	return nil
   661  }
   662  
   663  func (r *ProviderReconciler) ownerRef(provider *api.Provider) []metav1.OwnerReference {
   664  	kind := reflect.TypeOf(api.Provider{}).Name()
   665  	ownerRef := []metav1.OwnerReference{
   666  		*metav1.NewControllerRef(
   667  			provider,
   668  			api.GroupVersion.WithKind(kind),
   669  		),
   670  	}
   671  	return ownerRef
   672  }
   673  
   674  // updateStatus fetches an up-to-date copy of the object we want to update the
   675  // status for and patches its status.
   676  // this is done to minimize cache mutation errors and API server mismatch errors
   677  // that can occur if the patch does not align with the API server's current state
   678  func (r *ProviderReconciler) updateStatus(ctx context.Context, req ctrl.Request, status api.ProviderStatus) error {
   679  	var provider api.Provider
   680  	if err := r.Get(ctx, req.NamespacedName, &provider); err != nil {
   681  		return err
   682  	}
   683  	providerStatus := provider.Status.Conditions
   684  
   685  	patch := client.MergeFrom(provider.DeepCopy())
   686  
   687  	provider.Status = status
   688  
   689  	// getting most recent EncryptionRotationSucceeded status from the most recent provider grabbed
   690  	// to make sure it's not overridden by the older one that mightve been passed in
   691  	for i, condition := range providerStatus {
   692  		if condition.Reason == EncryptionRotationSucceeded {
   693  			if provider.Spec.Encryption.Version != "" {
   694  				edgeConditions.Set(&provider, &condition) //nolint:gosec
   695  			} else {
   696  				// remove the condition from the list
   697  				provider.Status.Conditions = append(providerStatus[:i], providerStatus[i+1:]...)
   698  			}
   699  		}
   700  	}
   701  
   702  	return r.Status().Patch(ctx, &provider, patch)
   703  }
   704  
   705  // createResmap reads the kustomized manifests file at the given path and
   706  // returns a resmap created from its bytes.
   707  // This is used to populate the Resmaps field
   708  func CreateResmaps(paths []string) (map[string]resmap.ResMap, error) {
   709  	factory := resmap.NewFactory(resource.NewFactory(&hasher.Hasher{}))
   710  	resMaps := make(map[string]resmap.ResMap)
   711  
   712  	for _, path := range paths {
   713  		readManifests, err := os.Open(path)
   714  		if err != nil {
   715  			return nil, err
   716  		}
   717  		b, err := os.ReadFile(readManifests.Name())
   718  		if err != nil {
   719  			return nil, err
   720  		}
   721  		resMap, err := factory.NewResMapFromBytes(b)
   722  		if err != nil {
   723  			return nil, err
   724  		}
   725  		dir := filepath.Dir(path)
   726  		key := strings.Split(dir, "/")
   727  		resMaps[key[len(key)-1]] = resMap
   728  	}
   729  	return resMaps, nil
   730  }
   731  
   732  // grab the status version the databases are using, if none found, return empty string
   733  func encryptionGetDatabaseVersion(status api.ProviderStatus) string {
   734  	for _, condition := range status.Conditions {
   735  		if condition.Reason == EncryptionRotationSucceeded {
   736  			message := condition.Message
   737  			version := strings.TrimPrefix(message, "successfully updated databases to version: ")
   738  			return version
   739  		}
   740  	}
   741  	return ""
   742  }
   743  
   744  // for store-provider, touchpoint-provider, & operator, edit the IAM_ENCRYPTION_KEY env to add the secret name
   745  func encryptionAddSecretVersion(version string, manifest []*unstructured.Unstructured, log logr.Logger) []*unstructured.Unstructured {
   746  	secretName := EncryptionKeySecretPrefix + version
   747  	for i := range manifest {
   748  		name := manifest[i].GetName()
   749  		// if its a store/touchpoint/external provider
   750  		if ((name == StoreProvider) || (name == ExternalProvider) || (name == TouchpointProvider)) &&
   751  			(manifest[i].GetKind() == Deployment || manifest[i].GetKind() == DaemonSet) && manifest[i].GetNamespace() == EdgeIAM {
   752  			containers := manifest[i].Object["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})["containers"].([]interface{})
   753  			// get edge-iam-provider container
   754  			for _, container := range containers {
   755  				containerName := container.(map[string]interface{})["name"]
   756  				if containerName == ProviderContainer {
   757  					// get the 'env' map
   758  					envs := container.(map[string]interface{})["env"].([]interface{})
   759  					for _, env := range envs {
   760  						envName := env.(map[string]interface{})["name"]
   761  						// add the iam encryption key env secretname
   762  						if envName == "IAM_ENCRYPTION_KEY" {
   763  							env.(map[string]interface{})["valueFrom"].(map[string]interface{})["secretKeyRef"].(map[string]interface{})["name"] = secretName
   764  							// todo: do i need the following line
   765  							// container.(map[string]interface{})["env"] = envs
   766  							log.Info(fmt.Sprintf("Updated encryption key env on %s to use the encryption key secret", name), "secret", secretName)
   767  							break
   768  						}
   769  					}
   770  				}
   771  			}
   772  		}
   773  	}
   774  	return manifest
   775  }
   776  
   777  // for store-provider, touchpoint-provider, & operator, edit obj to remove the IAM_ENCRYPTION_KEY env
   778  func encryptionRemoveSecretEnv(manifest []*unstructured.Unstructured, log logr.Logger) []*unstructured.Unstructured {
   779  	for i := range manifest {
   780  		name := manifest[i].GetName()
   781  		if ((name == StoreProvider) || (name == ExternalProvider) || (name == TouchpointProvider)) &&
   782  			(manifest[i].GetKind() == Deployment || manifest[i].GetKind() == DaemonSet) && manifest[i].GetNamespace() == EdgeIAM {
   783  			containers := manifest[i].Object["spec"].(map[string]interface{})["template"].(map[string]interface{})["spec"].(map[string]interface{})["containers"].([]interface{})
   784  			for _, container := range containers {
   785  				containerName := container.(map[string]interface{})["name"]
   786  				if containerName == ProviderContainer {
   787  					// get the 'env' map
   788  					envs := container.(map[string]interface{})["env"].([]interface{})
   789  					for j, env := range envs {
   790  						envName := env.(map[string]interface{})["name"]
   791  						// remove the iam encryption key env from the list
   792  						if envName == "IAM_ENCRYPTION_KEY" {
   793  							envs = append(envs[:j], envs[j+1:]...)
   794  							container.(map[string]interface{})["env"] = envs
   795  							log.Info(fmt.Sprintf("removed encryption key env on %s to use the encryption key secret", name), "object", name)
   796  							break
   797  						}
   798  					}
   799  				}
   800  			}
   801  		}
   802  	}
   803  	return manifest
   804  }
   805  

View as plain text