All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.github.microcks.operator.secret.SecretSourceReconciler Maven / Gradle / Ivy

The newest version!
/*
 * Copyright The Microcks Authors.
 *
 * 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 io.github.microcks.operator.secret;

import io.github.microcks.client.ApiClient;
import io.github.microcks.client.ApiException;
import io.github.microcks.client.api.ConfigApi;
import io.github.microcks.client.model.Secret;
import io.github.microcks.operator.AbstractMicrocksDependantReconciler;
import io.github.microcks.operator.KeycloakHelper;
import io.github.microcks.operator.api.base.v1alpha1.Microcks;
import io.github.microcks.operator.api.model.Condition;
import io.github.microcks.operator.api.model.Status;
import io.github.microcks.operator.api.secret.v1alpha1.SecretSource;
import io.github.microcks.operator.api.secret.v1alpha1.SecretSourceSpec;
import io.github.microcks.operator.api.secret.v1alpha1.SecretSourceStatus;
import io.github.microcks.operator.api.secret.v1alpha1.SecretSpec;
import io.github.microcks.operator.api.secret.v1alpha1.SecretValuesFromSpec;
import io.github.microcks.operator.model.ConditionUtil;

import io.fabric8.kubernetes.client.KubernetesClient;
import io.javaoperatorsdk.operator.api.config.informer.InformerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.Cleaner;
import io.javaoperatorsdk.operator.api.reconciler.Context;
import io.javaoperatorsdk.operator.api.reconciler.ControllerConfiguration;
import io.javaoperatorsdk.operator.api.reconciler.DeleteControl;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceContext;
import io.javaoperatorsdk.operator.api.reconciler.EventSourceInitializer;
import io.javaoperatorsdk.operator.api.reconciler.Reconciler;
import io.javaoperatorsdk.operator.api.reconciler.UpdateControl;
import io.javaoperatorsdk.operator.processing.event.ResourceID;
import io.javaoperatorsdk.operator.processing.event.source.EventSource;
import io.javaoperatorsdk.operator.processing.event.source.SecondaryToPrimaryMapper;
import io.javaoperatorsdk.operator.processing.event.source.informer.InformerEventSource;
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.Logger;

import java.time.Duration;
import java.util.Base64;
import java.util.Map;
import java.util.stream.Collectors;

import static io.javaoperatorsdk.operator.api.reconciler.Constants.WATCH_CURRENT_NAMESPACE;

/**
 * Reconciliation entry point for the {@code SecretSource} Kubernetes custom resource.
 * @author laurent
 */
@ControllerConfiguration(namespaces = WATCH_CURRENT_NAMESPACE)
@SuppressWarnings("unused")
@ApplicationScoped
public class SecretSourceReconciler extends AbstractMicrocksDependantReconciler
      implements Reconciler, Cleaner, EventSourceInitializer {

   /** Get a JBoss logging logger. */
   private final Logger logger = Logger.getLogger(getClass());

   /**
    * Default constructor with injected Kubernetes client.
    * @param client A Kubernetes client for interacting with the cluster
    */
   public SecretSourceReconciler(KubernetesClient client) {
      this.client = client;
      this.keycloakHelper = new KeycloakHelper(client);
   }

   @Override
   public Map prepareEventSources(EventSourceContext context) {
      /*
       * To create an event to a related SecretSource resource and trigger the reconciliation we need to
       * find which SecretSource this Secret custom resource is related to. To find the related customResourceId
       * of the SecretSource resource we traverse the cache and identify it based on naming convention.
       */
      final SecondaryToPrimaryMapper secretSourcesMatchingSecretName =
            (io.fabric8.kubernetes.api.model.Secret kubeSecret) -> context.getPrimaryCache()
                  .list(secretSource -> secretSource.getSpec().getSecrets().stream()
                        .filter(secretSpec -> secretSpec.getValuesFrom() != null)
                        .map(secretSpec -> secretSpec.getValuesFrom().getSecretRef())
                        .toList()
                        .contains(kubeSecret.getMetadata().getName()))
                  .map(ResourceID::fromResource)
                  .collect(Collectors.toSet());

      InformerConfiguration configuration =
            InformerConfiguration.from(io.fabric8.kubernetes.api.model.Secret.class, context)
                  .withSecondaryToPrimaryMapper(secretSourcesMatchingSecretName)
                  .withPrimaryToSecondaryMapper(
                        (SecretSource primary) -> primary.getSpec().getSecrets().stream()
                              .filter(secretSpec -> secretSpec.getValuesFrom() != null)
                              .map(secretSpec -> new ResourceID(secretSpec.getValuesFrom().getSecretRef(), primary.getMetadata().getNamespace()))
                              .collect(Collectors.toSet())
                  )
                  .build();

      return EventSourceInitializer
            .nameEventSources(new InformerEventSource<>(configuration, context));
   }

   @Override
   public UpdateControl reconcile(SecretSource secretSource, Context context) throws Exception {
      final String ns = secretSource.getMetadata().getNamespace();
      final SecretSourceSpec spec = secretSource.getSpec();

      boolean updateStatus = false;
      // Set a minimal status if not present.
      if (secretSource.getStatus() == null) {
         secretSource.setStatus(new SecretSourceStatus());
         updateStatus = true;
      }

      logger.infof("Starting reconcile operation for '%s'", secretSource.getMetadata().getName());

      // Check that microcks instance specification is there.
      UpdateControlOrMicrocks preparationControl = prepareReconciliationWithMicrocksInstance(secretSource);
      if (preparationControl.updateControl() != null) {
         return preparationControl.updateControl();
      }

      // Now we have a Microcks instance that is ready to receive API requests.
      Microcks microcks = preparationControl.microcks();

      // Build an ApiClient for Microcks instance.
      UpdateControlOrApiClient apiClientControl = buildApiClient(secretSource, microcks);
      if (apiClientControl.updateControl() != null) {
         return apiClientControl.updateControl();
      }

      // Now we have an authenticated & ready to use ApiClient for Microcks instance.
      ApiClient apiClient = apiClientControl.apiClient();

      // Deal with secrets specifications.
      for (SecretSpec secretSpec : spec.getSecrets()) {
         Condition condition = ConditionUtil.getOrCreateCondition(secretSource.getStatus(), secretSpec.getName());

         // Check if we have to load values from a Kubernetes secret.
         io.fabric8.kubernetes.api.model.Secret kubeSecret = null;
         if (secretSpec.getValuesFrom() != null) {
            SecretValuesFromSpec valuesFromSpec = secretSpec.getValuesFrom();
            kubeSecret = client.secrets().inNamespace(ns).withName(valuesFromSpec.getSecretRef()).get();

            if (kubeSecret == null) {
               logger.errorf("Kubernetes secret '%s' not found for '%s' in SecretSource '%s'",
                     valuesFromSpec.getSecretRef(), secretSpec.getName(), secretSource.getMetadata().getName());
               condition.setStatus(Status.ERROR);
               condition.setMessage("Kubernetes secret '" + valuesFromSpec.getSecretRef() + "' not found");
               secretSource.getStatus().setStatus(Status.ERROR);

               // Don't forget to touch condition time before jumping to next iteration.
               ConditionUtil.touchConditionTime(condition);
               updateStatus = true;
               continue;
            }
         }

         try {
            // Previously created secret id may be stored within condition message.
            String previousId = getSecretIdOrNull(condition);
            String secretId = ensureSecretIsPresent(apiClient, secretSpec, kubeSecret, previousId);
            condition.setStatus(Status.READY);
            // TODO: Store secretId in condition additional property instead.
            condition.setMessage(secretId);
         } catch (ApiException e) {
            logger.errorf("Error while loading secret '%s' for SecretSource '%s'", secretSpec.getName(), secretSource.getMetadata().getName());
            logger.errorf(API_EXCEPTION_ERROR_LOG, e.getMessage(), e.getResponseBody());
            secretSource.getStatus().setStatus(Status.ERROR);
            condition.setStatus(Status.ERROR);
         }

         ConditionUtil.touchConditionTime(condition);
         updateStatus = true;
      }

      logger.infof("Finishing reconcile operation for '%s'", secretSource.getMetadata().getName());

      if (updateStatus) {
         logger.info("Returning an updateStatus control. ========================");
         checkIfGloballyReady(secretSource);
         return UpdateControl.updateStatus(secretSource);
      }

      logger.info("Returning a noUpdate control. =============================");
      return UpdateControl.noUpdate();
   }

   @Override
   public DeleteControl cleanup(SecretSource secretSource, Context context) {
      final String ns = secretSource.getMetadata().getNamespace();
      final SecretSourceSpec spec = secretSource.getSpec();
      logger.infof("Starting cleanup operation for '%s'", secretSource.getMetadata().getName());

      // Check that microcks instance specification is there.
      DeleteControlOrMicrocks preparationControl = prepareCleanupWithMicrocksInstance(secretSource);
      if (preparationControl.deleteControl() != null) {
         return preparationControl.deleteControl();
      }

      // Now we have a Microcks instance that is at least present.
      Microcks microcks = preparationControl.microcks();

      // Check that microcks instance is in ready status and we have a status for SecretSourxe.
      if (microcks.getStatus().getStatus() == Status.READY && secretSource.getStatus() != null) {

         // Build an ApiClient for Microcks instance.
         UpdateControlOrApiClient apiClientControl = buildApiClient(secretSource, microcks);
         if (apiClientControl.updateControl() != null) {
            logger.error("Rescheduling cleanup operation in 30 seconds");
            return DeleteControl.noFinalizerRemoval().rescheduleAfter(Duration.ofSeconds(30));
         }

         // Now we have an authenticated & ready to use ApiClient for Microcks instance.
         ApiClient apiClient = apiClientControl.apiClient();
         ConfigApi configApi = new ConfigApi(apiClient);

         // remove secrets from Microcks instance.
         for (SecretSpec secretSpec : spec.getSecrets()) {
            Condition condition = ConditionUtil.getOrCreateCondition(secretSource.getStatus(), secretSpec.getName());
            String secretId = getSecretIdOrNull(condition);

            if (secretId != null) {
               try {
                  configApi.deleteSecret(secretId);
               } catch (ApiException e) {
                  logger.errorf("Error while deleting secret '%s' for SecretSource '%s'", secretSpec.getName(), secretSource.getMetadata().getName());
                  logger.errorf(API_EXCEPTION_ERROR_LOG, e.getMessage(), e.getResponseBody());
                  return DeleteControl.noFinalizerRemoval().rescheduleAfter(Duration.ofSeconds(30));
               }
            }
         }

         // If here then every importer has been removed!
         return DeleteControl.defaultDelete();
      }

      // Re-schedule cleanup operation in 30 seconds to wait for Microcks to be ready.
      logger.error("Rescheduling cleanup operation in 30 seconds");
      return DeleteControl.noFinalizerRemoval().rescheduleAfter(Duration.ofSeconds(30));
   }

   /** Get a secret id (or null if not exists) */
   protected String getSecretIdOrNull(Condition condition) {
      return condition.getMessage();
   }

   /** Ensure a secret exists by checking by id, updating if found or cre-creating if not found. */
   protected String ensureSecretIsPresent(ApiClient apiClient, SecretSpec secretSpec,
                                          io.fabric8.kubernetes.api.model.Secret kubeSecret, String previousId) throws ApiException {
      if (previousId != null) {
         // We have a previous secret id, we should check if it's still there.
         ConfigApi configApi = new ConfigApi(apiClient);
         Secret secret = configApi.getSecret(previousId);
         if (secret != null) {
            updateWithSecretSpec(secret, secretSpec, kubeSecret);
            configApi.updateSecret(previousId, secret);
            return previousId;
         }
      }
      return createSecret(apiClient, secretSpec, kubeSecret);
   }

   protected String createSecret(ApiClient apiClient, SecretSpec secretSpec, io.fabric8.kubernetes.api.model.Secret kubeSecret) throws ApiException {
      // Move SecretSpec into Microcks API model.
      Secret secret = new Secret();
      updateWithSecretSpec(secret, secretSpec, kubeSecret);
      // Use the apiClient to create the secret.
      ConfigApi configApi = new ConfigApi(apiClient);
      secret = configApi.createSecret(secret);
      return secret.getId();
   }

   protected void updateWithSecretSpec(Secret secret, SecretSpec secretSpec, io.fabric8.kubernetes.api.model.Secret kubeSecret) {
      secret.setName(secretSpec.getName());
      secret.setDescription(secretSpec.getDescription());
      if (secretSpec.getValuesFrom() != null) {
         SecretValuesFromSpec valuesFromSpec = secretSpec.getValuesFrom();

         // Copy from Kubernetes secret if key is present. Don't forget to decode it.
         if (shouldCopyFromSecret(valuesFromSpec.getUsernameKey(), kubeSecret)) {
            secret.setUsername(decodeSecretValue(kubeSecret.getData().get(valuesFromSpec.getUsernameKey())));
         }
         if (shouldCopyFromSecret(valuesFromSpec.getPasswordKey(), kubeSecret)) {
            secret.setPassword(decodeSecretValue(kubeSecret.getData().get(valuesFromSpec.getPasswordKey())));
         }
         if (shouldCopyFromSecret(valuesFromSpec.getTokenKey(), kubeSecret)) {
            secret.setToken(decodeSecretValue(kubeSecret.getData().get(valuesFromSpec.getTokenKey())));
         }
         if (shouldCopyFromSecret(valuesFromSpec.getTokenHeaderKey(), kubeSecret)) {
            secret.setTokenHeader(decodeSecretValue(kubeSecret.getData().get(valuesFromSpec.getTokenHeaderKey())));
         }
         if (shouldCopyFromSecret(valuesFromSpec.getCaCertPermKey(), kubeSecret)) {
            secret.setCaCertPem(decodeSecretValue(kubeSecret.getData().get(valuesFromSpec.getCaCertPermKey())));
         }
      } else {
         // Simply copy values from specification.
         secret.setUsername(secretSpec.getUsername());
         secret.setPassword(secretSpec.getPassword());
         secret.setToken(secretSpec.getToken());
         secret.setTokenHeader(secretSpec.getTokenHeader());
         secret.setCaCertPem(secretSpec.getCaCertPem());
      }
   }

   protected void checkIfGloballyReady(SecretSource secretSource) {
      boolean allReady = true;
      for (Condition condition : secretSource.getStatus().getConditions()) {
         if (condition.getStatus() != Status.READY) {
            allReady = false;
            break;
         }
      }
      if (allReady) {
         secretSource.getStatus().setStatus(Status.READY);
      }
   }

   private static boolean shouldCopyFromSecret(String key, io.fabric8.kubernetes.api.model.Secret kubeSecret) {
      return key != null && kubeSecret.getData().containsKey(key);
   }

   private static String decodeSecretValue(String encodedValue) {
      return new String(Base64.getDecoder().decode(encodedValue));
   }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy