mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-09 06:28:15 +00:00
feat(k8-operator): push secrets
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/constants"
|
||||
"github.com/go-logr/logr"
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
const DEPLOYMENT_SECRET_NAME_ANNOTATION_PREFIX = "secrets.infisical.com/managed-secret"
|
||||
const AUTO_RELOAD_DEPLOYMENT_ANNOTATION = "secrets.infisical.com/auto-reload" // needs to be set to true for a deployment to start auto redeploying
|
||||
|
||||
func (r *InfisicalSecretReconciler) ReconcileDeploymentsWithManagedSecrets(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret) (int, error) {
|
||||
listOfDeployments := &v1.DeploymentList{}
|
||||
err := r.Client.List(ctx, listOfDeployments, &client.ListOptions{Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("unable to get deployments in the [namespace=%v] [err=%v]", infisicalSecret.Spec.ManagedSecretReference.SecretNamespace, err)
|
||||
}
|
||||
|
||||
managedKubeSecretNameAndNamespace := types.NamespacedName{
|
||||
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
|
||||
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
|
||||
}
|
||||
|
||||
managedKubeSecret := &corev1.Secret{}
|
||||
err = r.Client.Get(ctx, managedKubeSecretNameAndNamespace, managedKubeSecret)
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("unable to fetch Kubernetes secret to update deployment: %v", err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
// Iterate over the deployments and check if they use the managed secret
|
||||
for _, deployment := range listOfDeployments.Items {
|
||||
deployment := deployment
|
||||
if deployment.Annotations[AUTO_RELOAD_DEPLOYMENT_ANNOTATION] == "true" && r.IsDeploymentUsingManagedSecret(deployment, infisicalSecret) {
|
||||
// Start a goroutine to reconcile the deployment
|
||||
wg.Add(1)
|
||||
go func(d v1.Deployment, s corev1.Secret) {
|
||||
defer wg.Done()
|
||||
if err := r.ReconcileDeployment(ctx, logger, d, s); err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to reconcile deployment with [name=%v]. Will try next requeue", deployment.ObjectMeta.Name))
|
||||
}
|
||||
}(deployment, *managedKubeSecret)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Check if the deployment uses managed secrets
|
||||
func (r *InfisicalSecretReconciler) IsDeploymentUsingManagedSecret(deployment v1.Deployment, infisicalSecret v1alpha1.InfisicalSecret) bool {
|
||||
managedSecretName := infisicalSecret.Spec.ManagedSecretReference.SecretName
|
||||
for _, container := range deployment.Spec.Template.Spec.Containers {
|
||||
for _, envFrom := range container.EnvFrom {
|
||||
if envFrom.SecretRef != nil && envFrom.SecretRef.LocalObjectReference.Name == managedSecretName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
for _, env := range container.Env {
|
||||
if env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil && env.ValueFrom.SecretKeyRef.LocalObjectReference.Name == managedSecretName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, volume := range deployment.Spec.Template.Spec.Volumes {
|
||||
if volume.Secret != nil && volume.Secret.SecretName == managedSecretName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// This function ensures that a deployment is in sync with a Kubernetes secret by comparing their versions.
|
||||
// If the version of the secret is different from the version annotation on the deployment, the annotation is updated to trigger a restart of the deployment.
|
||||
func (r *InfisicalSecretReconciler) ReconcileDeployment(ctx context.Context, logger logr.Logger, deployment v1.Deployment, secret corev1.Secret) error {
|
||||
annotationKey := fmt.Sprintf("%s.%s", DEPLOYMENT_SECRET_NAME_ANNOTATION_PREFIX, secret.Name)
|
||||
annotationValue := secret.Annotations[constants.SECRET_VERSION_ANNOTATION]
|
||||
|
||||
if deployment.Annotations[annotationKey] == annotationValue &&
|
||||
deployment.Spec.Template.Annotations[annotationKey] == annotationValue {
|
||||
logger.Info(fmt.Sprintf("The [deploymentName=%v] is already using the most up to date managed secrets. No action required.", deployment.ObjectMeta.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("Deployment is using outdated managed secret. Starting re-deployment [deploymentName=%v]", deployment.ObjectMeta.Name))
|
||||
|
||||
if deployment.Spec.Template.Annotations == nil {
|
||||
deployment.Spec.Template.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
deployment.Annotations[annotationKey] = annotationValue
|
||||
deployment.Spec.Template.Annotations[annotationKey] = annotationValue
|
||||
|
||||
if err := r.Client.Update(ctx, &deployment); err != nil {
|
||||
return fmt.Errorf("failed to update deployment annotation: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/util"
|
||||
"github.com/go-logr/logr"
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func (r *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, errorToConditionOn error) error {
|
||||
if infisicalSecret.Status.Conditions == nil {
|
||||
infisicalSecret.Status.Conditions = []metav1.Condition{}
|
||||
}
|
||||
|
||||
if errorToConditionOn != nil {
|
||||
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
|
||||
Type: "secrets.infisical.com/ReadyToSyncSecrets",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "Error",
|
||||
Message: "Failed to sync secrets. This can be caused by invalid service token or an invalid API host that is set. Check operator logs for more info",
|
||||
})
|
||||
|
||||
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
|
||||
Type: "secrets.infisical.com/AutoRedeployReady",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "Stopped",
|
||||
Message: "Auto redeployment has been stopped because the operator failed to sync secrets",
|
||||
})
|
||||
} else {
|
||||
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
|
||||
Type: "secrets.infisical.com/ReadyToSyncSecrets",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "OK",
|
||||
Message: "Infisical controller has started syncing your secrets",
|
||||
})
|
||||
}
|
||||
|
||||
return r.Client.Status().Update(ctx, infisicalSecret)
|
||||
}
|
||||
|
||||
func (r *InfisicalSecretReconciler) SetInfisicalTokenLoadCondition(ctx context.Context, logger logr.Logger, infisicalSecret *v1alpha1.InfisicalSecret, authStrategy util.AuthStrategyType, errorToConditionOn error) {
|
||||
if infisicalSecret.Status.Conditions == nil {
|
||||
infisicalSecret.Status.Conditions = []metav1.Condition{}
|
||||
}
|
||||
|
||||
if errorToConditionOn == nil {
|
||||
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
|
||||
Type: "secrets.infisical.com/LoadedInfisicalToken",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "OK",
|
||||
Message: fmt.Sprintf("Infisical controller has loaded the Infisical token in provided Kubernetes secret, using %v authentication strategy", authStrategy),
|
||||
})
|
||||
} else {
|
||||
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
|
||||
Type: "secrets.infisical.com/LoadedInfisicalToken",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "Error",
|
||||
Message: fmt.Sprintf("Failed to load Infisical Token from the provided Kubernetes secret because: %v", errorToConditionOn),
|
||||
})
|
||||
}
|
||||
|
||||
err := r.Client.Status().Update(ctx, infisicalSecret)
|
||||
if err != nil {
|
||||
logger.Error(err, "Could not set condition for LoadedInfisicalToken")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *InfisicalSecretReconciler) SetInfisicalAutoRedeploymentReady(ctx context.Context, logger logr.Logger, infisicalSecret *v1alpha1.InfisicalSecret, numDeployments int, errorToConditionOn error) {
|
||||
if infisicalSecret.Status.Conditions == nil {
|
||||
infisicalSecret.Status.Conditions = []metav1.Condition{}
|
||||
}
|
||||
|
||||
if errorToConditionOn == nil {
|
||||
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
|
||||
Type: "secrets.infisical.com/AutoRedeployReady",
|
||||
Status: metav1.ConditionTrue,
|
||||
Reason: "OK",
|
||||
Message: fmt.Sprintf("Infisical has found %v deployments which are ready to be auto redeployed when secrets change", numDeployments),
|
||||
})
|
||||
} else {
|
||||
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
|
||||
Type: "secrets.infisical.com/AutoRedeployReady",
|
||||
Status: metav1.ConditionFalse,
|
||||
Reason: "Error",
|
||||
Message: fmt.Sprintf("Failed reconcile deployments because: %v", errorToConditionOn),
|
||||
})
|
||||
}
|
||||
|
||||
err := r.Client.Status().Update(ctx, infisicalSecret)
|
||||
if err != nil {
|
||||
logger.Error(err, "Could not set condition for AutoRedeployReady")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
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/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
|
||||
secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/api"
|
||||
controllerhelpers "github.com/Infisical/infisical/k8-operator/packages/controllerutil"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/util"
|
||||
"github.com/go-logr/logr"
|
||||
)
|
||||
|
||||
// InfisicalSecretReconciler reconciles a InfisicalSecret object
|
||||
type InfisicalSecretReconciler struct {
|
||||
client.Client
|
||||
BaseLogger logr.Logger
|
||||
Scheme *runtime.Scheme
|
||||
}
|
||||
|
||||
var resourceVariablesMap map[string]util.ResourceVariables = make(map[string]util.ResourceVariables)
|
||||
|
||||
//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/status,verbs=get;update;patch
|
||||
//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/finalizers,verbs=update
|
||||
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;delete
|
||||
//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;delete
|
||||
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;get;update
|
||||
//+kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;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.
|
||||
// For more details, check Reconcile and its Result here:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/[email protected]/pkg/reconcile
|
||||
|
||||
const FINALIZER_NAME = "secrets.finalizers.infisical.com"
|
||||
|
||||
// Maps the infisicalSecretCR.UID to a infisicalSdk.InfisicalClientInterface and AuthenticationDetails.
|
||||
// var resourceVariablesMap = make(map[string]ResourceVariables)
|
||||
|
||||
func (r *InfisicalSecretReconciler) GetLogger(req ctrl.Request) logr.Logger {
|
||||
return r.BaseLogger.WithValues("infisicalsecret", req.NamespacedName)
|
||||
}
|
||||
|
||||
func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
|
||||
logger := r.GetLogger(req)
|
||||
|
||||
var infisicalSecretCR secretsv1alpha1.InfisicalSecret
|
||||
requeueTime := time.Minute // seconds
|
||||
|
||||
if resourceVariablesMap == nil {
|
||||
resourceVariablesMap = make(map[string]util.ResourceVariables)
|
||||
}
|
||||
|
||||
err := r.Get(ctx, req.NamespacedName, &infisicalSecretCR)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return ctrl.Result{
|
||||
Requeue: false,
|
||||
}, nil
|
||||
} else {
|
||||
logger.Error(err, "unable to fetch Infisical Secret CRD from cluster")
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Remove finalizers if they exist. This is to support previous InfisicalSecret CRD's that have finalizers on them.
|
||||
// In order to delete secrets with finalizers, we first remove the finalizers so we can use the simplified and improved deletion process
|
||||
if !infisicalSecretCR.ObjectMeta.DeletionTimestamp.IsZero() && len(infisicalSecretCR.ObjectMeta.Finalizers) > 0 {
|
||||
infisicalSecretCR.ObjectMeta.Finalizers = []string{}
|
||||
if err := r.Update(ctx, &infisicalSecretCR); err != nil {
|
||||
logger.Error(err, fmt.Sprintf("Error removing finalizers from Infisical Secret %s", infisicalSecretCR.Name))
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
// Our finalizers have been removed, so the reconciler can do nothing.
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if infisicalSecretCR.Spec.ResyncInterval != 0 {
|
||||
requeueTime = time.Second * time.Duration(infisicalSecretCR.Spec.ResyncInterval)
|
||||
logger.Info(fmt.Sprintf("Manual re-sync interval set. Interval: %v", requeueTime))
|
||||
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Re-sync interval set. Interval: %v", requeueTime))
|
||||
}
|
||||
|
||||
// Check if the resource is already marked for deletion
|
||||
if infisicalSecretCR.GetDeletionTimestamp() != nil {
|
||||
return ctrl.Result{
|
||||
Requeue: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get modified/default config
|
||||
infisicalConfig, err := controllerhelpers.GetInfisicalConfigMap(ctx, r.Client)
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to fetch infisical-config. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if infisicalSecretCR.Spec.HostAPI == "" {
|
||||
api.API_HOST_URL = infisicalConfig["hostAPI"]
|
||||
} else {
|
||||
api.API_HOST_URL = infisicalSecretCR.Spec.HostAPI
|
||||
}
|
||||
|
||||
if infisicalSecretCR.Spec.TLS.CaRef.SecretName != "" {
|
||||
api.API_CA_CERTIFICATE, err = r.getInfisicalCaCertificateFromKubeSecret(ctx, infisicalSecretCR)
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to fetch CA certificate. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
logger.Info("Using custom CA certificate...")
|
||||
} else {
|
||||
api.API_CA_CERTIFICATE = ""
|
||||
}
|
||||
|
||||
err = r.ReconcileInfisicalSecret(ctx, logger, infisicalSecretCR)
|
||||
r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCR, err)
|
||||
|
||||
if err != nil {
|
||||
|
||||
logger.Error(err, fmt.Sprintf("unable to reconcile InfisicalSecret. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
numDeployments, err := r.ReconcileDeploymentsWithManagedSecrets(ctx, logger, infisicalSecretCR)
|
||||
r.SetInfisicalAutoRedeploymentReady(ctx, logger, &infisicalSecretCR, numDeployments, err)
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to reconcile auto redeployment. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Sync again after the specified time
|
||||
logger.Info(fmt.Sprintf("Operator will requeue after [%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *InfisicalSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&secretsv1alpha1.InfisicalSecret{}, builder.WithPredicates(predicate.Funcs{
|
||||
UpdateFunc: func(e event.UpdateEvent) bool {
|
||||
if resourceVariablesMap != nil {
|
||||
if rv, ok := resourceVariablesMap[string(e.ObjectNew.GetUID())]; ok {
|
||||
rv.CancelCtx()
|
||||
delete(resourceVariablesMap, string(e.ObjectNew.GetUID()))
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
DeleteFunc: func(e event.DeleteEvent) bool {
|
||||
if resourceVariablesMap != nil {
|
||||
if rv, ok := resourceVariablesMap[string(e.Object.GetUID())]; ok {
|
||||
rv.CancelCtx()
|
||||
delete(resourceVariablesMap, string(e.Object.GetUID()))
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
})).
|
||||
Complete(r)
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/api"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/constants"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/model"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/util"
|
||||
"github.com/go-logr/logr"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
|
||||
infisicalSdk "github.com/infisical/go-sdk"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8Errors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
func (r *InfisicalSecretReconciler) handleAuthentication(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (util.AuthenticationDetails, error) {
|
||||
|
||||
// ? Legacy support, service token auth
|
||||
infisicalToken, err := r.getInfisicalTokenFromKubeSecret(ctx, infisicalSecret)
|
||||
if err != nil {
|
||||
return util.AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err)
|
||||
}
|
||||
if infisicalToken != "" {
|
||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||
return util.AuthenticationDetails{AuthStrategy: util.AuthStrategy.SERVICE_TOKEN}, nil
|
||||
}
|
||||
|
||||
// ? Legacy support, service account auth
|
||||
serviceAccountCreds, err := r.getInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret)
|
||||
if err != nil {
|
||||
return util.AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err)
|
||||
}
|
||||
|
||||
if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" {
|
||||
infisicalClient.Auth().SetAccessToken(serviceAccountCreds.AccessKey)
|
||||
return util.AuthenticationDetails{AuthStrategy: util.AuthStrategy.SERVICE_ACCOUNT}, nil
|
||||
}
|
||||
|
||||
authStrategies := map[util.AuthStrategyType]func(ctx context.Context, reconcilerClient client.Client, secretCrd util.SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (util.AuthenticationDetails, error){
|
||||
util.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY: util.HandleUniversalAuth,
|
||||
util.AuthStrategy.KUBERNETES_MACHINE_IDENTITY: util.HandleKubernetesAuth,
|
||||
util.AuthStrategy.AWS_IAM_MACHINE_IDENTITY: util.HandleAwsIamAuth,
|
||||
util.AuthStrategy.AZURE_MACHINE_IDENTITY: util.HandleAzureAuth,
|
||||
util.AuthStrategy.GCP_ID_TOKEN_MACHINE_IDENTITY: util.HandleGcpIdTokenAuth,
|
||||
util.AuthStrategy.GCP_IAM_MACHINE_IDENTITY: util.HandleGcpIamAuth,
|
||||
}
|
||||
|
||||
for authStrategy, authHandler := range authStrategies {
|
||||
authDetails, err := authHandler(ctx, r.Client, util.SecretAuthInput{
|
||||
Secret: infisicalSecret,
|
||||
Type: util.SecretCrd.INFISICAL_SECRET,
|
||||
}, infisicalClient)
|
||||
|
||||
if err == nil {
|
||||
return authDetails, nil
|
||||
}
|
||||
|
||||
if !errors.Is(err, util.ErrAuthNotApplicable) {
|
||||
return util.AuthenticationDetails{}, fmt.Errorf("authentication failed for strategy [%s] [err=%w]", authStrategy, err)
|
||||
}
|
||||
}
|
||||
|
||||
return util.AuthenticationDetails{}, fmt.Errorf("no authentication method provided")
|
||||
|
||||
}
|
||||
|
||||
func (r *InfisicalSecretReconciler) getInfisicalTokenFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (string, error) {
|
||||
// default to new secret ref structure
|
||||
secretName := infisicalSecret.Spec.Authentication.ServiceToken.ServiceTokenSecretReference.SecretName
|
||||
secretNamespace := infisicalSecret.Spec.Authentication.ServiceToken.ServiceTokenSecretReference.SecretNamespace
|
||||
// fall back to previous secret ref
|
||||
if secretName == "" {
|
||||
secretName = infisicalSecret.Spec.TokenSecretReference.SecretName
|
||||
}
|
||||
|
||||
if secretNamespace == "" {
|
||||
secretNamespace = infisicalSecret.Spec.TokenSecretReference.SecretNamespace
|
||||
}
|
||||
|
||||
tokenSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
|
||||
Namespace: secretNamespace,
|
||||
Name: secretName,
|
||||
})
|
||||
|
||||
if k8Errors.IsNotFound(err) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to read Infisical token secret from secret named [%s] in namespace [%s]: with error [%w]", infisicalSecret.Spec.TokenSecretReference.SecretName, infisicalSecret.Spec.TokenSecretReference.SecretNamespace, err)
|
||||
}
|
||||
|
||||
infisicalServiceToken := tokenSecret.Data[constants.INFISICAL_TOKEN_SECRET_KEY_NAME]
|
||||
|
||||
return strings.Replace(string(infisicalServiceToken), " ", "", -1), nil
|
||||
}
|
||||
|
||||
func (r *InfisicalSecretReconciler) getInfisicalCaCertificateFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (caCertificate string, err error) {
|
||||
|
||||
caCertificateFromKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
|
||||
Namespace: infisicalSecret.Spec.TLS.CaRef.SecretNamespace,
|
||||
Name: infisicalSecret.Spec.TLS.CaRef.SecretName,
|
||||
})
|
||||
|
||||
if k8Errors.IsNotFound(err) {
|
||||
return "", fmt.Errorf("kubernetes secret containing custom CA certificate cannot be found. [err=%s]", err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("something went wrong when fetching your CA certificate [err=%s]", err)
|
||||
}
|
||||
|
||||
caCertificateFromSecret := string(caCertificateFromKubeSecret.Data[infisicalSecret.Spec.TLS.CaRef.SecretKey])
|
||||
|
||||
return caCertificateFromSecret, nil
|
||||
}
|
||||
|
||||
// Fetches service account credentials from a Kubernetes secret specified in the infisicalSecret object, extracts the access key, public key, and private key from the secret, and returns them as a ServiceAccountCredentials object.
|
||||
// If any keys are missing or an error occurs, returns an empty object or an error object, respectively.
|
||||
func (r *InfisicalSecretReconciler) getInfisicalServiceAccountCredentialsFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (serviceAccountDetails model.ServiceAccountDetails, err error) {
|
||||
serviceAccountCredsFromKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
|
||||
Namespace: infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretNamespace,
|
||||
Name: infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretName,
|
||||
})
|
||||
|
||||
if k8Errors.IsNotFound(err) {
|
||||
return model.ServiceAccountDetails{}, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return model.ServiceAccountDetails{}, fmt.Errorf("something went wrong when fetching your service account credentials [err=%s]", err)
|
||||
}
|
||||
|
||||
accessKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[constants.SERVICE_ACCOUNT_ACCESS_KEY]
|
||||
publicKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[constants.SERVICE_ACCOUNT_PUBLIC_KEY]
|
||||
privateKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[constants.SERVICE_ACCOUNT_PRIVATE_KEY]
|
||||
|
||||
if accessKeyFromSecret == nil || publicKeyFromSecret == nil || privateKeyFromSecret == nil {
|
||||
return model.ServiceAccountDetails{}, nil
|
||||
}
|
||||
|
||||
return model.ServiceAccountDetails{AccessKey: string(accessKeyFromSecret), PrivateKey: string(privateKeyFromSecret), PublicKey: string(publicKeyFromSecret)}, nil
|
||||
}
|
||||
|
||||
func (r *InfisicalSecretReconciler) createInfisicalManagedKubeSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
|
||||
plainProcessedSecrets := make(map[string][]byte)
|
||||
secretType := infisicalSecret.Spec.ManagedSecretReference.SecretType
|
||||
managedTemplateData := infisicalSecret.Spec.ManagedSecretReference.Template
|
||||
|
||||
if managedTemplateData == nil || managedTemplateData.IncludeAllSecrets {
|
||||
for _, secret := range secretsFromAPI {
|
||||
plainProcessedSecrets[secret.Key] = []byte(secret.Value) // plain process
|
||||
}
|
||||
}
|
||||
|
||||
if managedTemplateData != nil {
|
||||
secretKeyValue := make(map[string]model.SecretTemplateOptions)
|
||||
for _, secret := range secretsFromAPI {
|
||||
secretKeyValue[secret.Key] = model.SecretTemplateOptions{
|
||||
Value: secret.Value,
|
||||
SecretPath: secret.SecretPath,
|
||||
}
|
||||
}
|
||||
|
||||
for templateKey, userTemplate := range managedTemplateData.Data {
|
||||
tmpl, err := template.New("secret-templates").Parse(userTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to compile template: %s [err=%v]", templateKey, err)
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
err = tmpl.Execute(buf, secretKeyValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to execute template: %s [err=%v]", templateKey, err)
|
||||
}
|
||||
plainProcessedSecrets[templateKey] = buf.Bytes()
|
||||
}
|
||||
}
|
||||
|
||||
// copy labels and annotations from InfisicalSecret CRD
|
||||
labels := map[string]string{}
|
||||
for k, v := range infisicalSecret.Labels {
|
||||
labels[k] = v
|
||||
}
|
||||
|
||||
annotations := map[string]string{}
|
||||
systemPrefixes := []string{"kubectl.kubernetes.io/", "kubernetes.io/", "k8s.io/", "helm.sh/"}
|
||||
for k, v := range infisicalSecret.Annotations {
|
||||
isSystem := false
|
||||
for _, prefix := range systemPrefixes {
|
||||
if strings.HasPrefix(k, prefix) {
|
||||
isSystem = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isSystem {
|
||||
annotations[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
annotations[constants.SECRET_VERSION_ANNOTATION] = ETag
|
||||
|
||||
// create a new secret as specified by the managed secret spec of CRD
|
||||
newKubeSecretInstance := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
|
||||
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
|
||||
Annotations: annotations,
|
||||
Labels: labels,
|
||||
},
|
||||
Type: corev1.SecretType(secretType),
|
||||
Data: plainProcessedSecrets,
|
||||
}
|
||||
|
||||
if infisicalSecret.Spec.ManagedSecretReference.CreationPolicy == "Owner" {
|
||||
// Set InfisicalSecret instance as the owner and controller of the managed secret
|
||||
err := ctrl.SetControllerReference(&infisicalSecret, newKubeSecretInstance, r.Scheme)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
err := r.Client.Create(ctx, newKubeSecretInstance)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create the managed Kubernetes secret : %w", err)
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("Successfully created a managed Kubernetes secret with your Infisical secrets. Type: %s", secretType))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InfisicalSecretReconciler) updateInfisicalManagedKubeSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
|
||||
managedTemplateData := infisicalSecret.Spec.ManagedSecretReference.Template
|
||||
|
||||
plainProcessedSecrets := make(map[string][]byte)
|
||||
if managedTemplateData == nil || managedTemplateData.IncludeAllSecrets {
|
||||
for _, secret := range secretsFromAPI {
|
||||
plainProcessedSecrets[secret.Key] = []byte(secret.Value)
|
||||
}
|
||||
}
|
||||
|
||||
if managedTemplateData != nil {
|
||||
secretKeyValue := make(map[string]model.SecretTemplateOptions)
|
||||
for _, secret := range secretsFromAPI {
|
||||
secretKeyValue[secret.Key] = model.SecretTemplateOptions{
|
||||
Value: secret.Value,
|
||||
SecretPath: secret.SecretPath,
|
||||
}
|
||||
}
|
||||
|
||||
for templateKey, userTemplate := range managedTemplateData.Data {
|
||||
tmpl, err := template.New("secret-templates").Parse(userTemplate)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to compile template: %s [err=%v]", templateKey, err)
|
||||
}
|
||||
|
||||
buf := bytes.NewBuffer(nil)
|
||||
err = tmpl.Execute(buf, secretKeyValue)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to execute template: %s [err=%v]", templateKey, err)
|
||||
}
|
||||
plainProcessedSecrets[templateKey] = buf.Bytes()
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the Annotations map if it's nil
|
||||
if managedKubeSecret.ObjectMeta.Annotations == nil {
|
||||
managedKubeSecret.ObjectMeta.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
managedKubeSecret.Data = plainProcessedSecrets
|
||||
managedKubeSecret.ObjectMeta.Annotations[constants.SECRET_VERSION_ANNOTATION] = ETag
|
||||
|
||||
err := r.Client.Update(ctx, &managedKubeSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to update Kubernetes secret because [%w]", err)
|
||||
}
|
||||
|
||||
logger.Info("successfully updated managed Kubernetes secret")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InfisicalSecretReconciler) getResourceVariables(infisicalSecret v1alpha1.InfisicalSecret) util.ResourceVariables {
|
||||
|
||||
var resourceVariables util.ResourceVariables
|
||||
|
||||
if _, ok := resourceVariablesMap[string(infisicalSecret.UID)]; !ok {
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
|
||||
client := infisicalSdk.NewInfisicalClient(ctx, infisicalSdk.Config{
|
||||
SiteUrl: api.API_HOST_URL,
|
||||
CaCertificate: api.API_CA_CERTIFICATE,
|
||||
UserAgent: api.USER_AGENT_NAME,
|
||||
})
|
||||
|
||||
resourceVariablesMap[string(infisicalSecret.UID)] = util.ResourceVariables{
|
||||
InfisicalClient: client,
|
||||
CancelCtx: cancel,
|
||||
AuthDetails: util.AuthenticationDetails{},
|
||||
}
|
||||
|
||||
resourceVariables = resourceVariablesMap[string(infisicalSecret.UID)]
|
||||
|
||||
} else {
|
||||
resourceVariables = resourceVariablesMap[string(infisicalSecret.UID)]
|
||||
}
|
||||
|
||||
return resourceVariables
|
||||
|
||||
}
|
||||
|
||||
func (r *InfisicalSecretReconciler) updateResourceVariables(infisicalSecret v1alpha1.InfisicalSecret, resourceVariables util.ResourceVariables) {
|
||||
resourceVariablesMap[string(infisicalSecret.UID)] = resourceVariables
|
||||
}
|
||||
|
||||
func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret) error {
|
||||
|
||||
resourceVariables := r.getResourceVariables(infisicalSecret)
|
||||
infisicalClient := resourceVariables.InfisicalClient
|
||||
cancelCtx := resourceVariables.CancelCtx
|
||||
authDetails := resourceVariables.AuthDetails
|
||||
var err error
|
||||
|
||||
if authDetails.AuthStrategy == "" {
|
||||
logger.Info("No authentication strategy found. Attempting to authenticate")
|
||||
authDetails, err = r.handleAuthentication(ctx, infisicalSecret, infisicalClient)
|
||||
r.SetInfisicalTokenLoadCondition(ctx, logger, &infisicalSecret, authDetails.AuthStrategy, err)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to authenticate [err=%s]", err)
|
||||
}
|
||||
|
||||
r.updateResourceVariables(infisicalSecret, util.ResourceVariables{
|
||||
InfisicalClient: infisicalClient,
|
||||
CancelCtx: cancelCtx,
|
||||
AuthDetails: authDetails,
|
||||
})
|
||||
}
|
||||
|
||||
// Look for managed secret by name and namespace
|
||||
managedKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
|
||||
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
|
||||
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
|
||||
})
|
||||
|
||||
if err != nil && !k8Errors.IsNotFound(err) {
|
||||
return fmt.Errorf("something went wrong when fetching the managed Kubernetes secret [%w]", err)
|
||||
}
|
||||
|
||||
// Get exiting Etag if exists
|
||||
secretVersionBasedOnETag := ""
|
||||
if managedKubeSecret != nil {
|
||||
secretVersionBasedOnETag = managedKubeSecret.Annotations[constants.SECRET_VERSION_ANNOTATION]
|
||||
}
|
||||
|
||||
var plainTextSecretsFromApi []model.SingleEnvironmentVariable
|
||||
var updateDetails model.RequestUpdateUpdateDetails
|
||||
|
||||
if authDetails.AuthStrategy == util.AuthStrategy.SERVICE_ACCOUNT { // Service Account // ! Legacy auth method
|
||||
serviceAccountCreds, err := r.getInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err)
|
||||
}
|
||||
|
||||
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaServiceAccount(infisicalClient, serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName, secretVersionBasedOnETag)
|
||||
if err != nil {
|
||||
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
|
||||
}
|
||||
|
||||
logger.Info("ReconcileInfisicalSecret: Fetched secrets via service account")
|
||||
|
||||
} else if authDetails.AuthStrategy == util.AuthStrategy.SERVICE_TOKEN { // Service Tokens // ! Legacy / Deprecated auth method
|
||||
infisicalToken, err := r.getInfisicalTokenFromKubeSecret(ctx, infisicalSecret)
|
||||
if err != nil {
|
||||
return fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err)
|
||||
}
|
||||
|
||||
envSlug := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.EnvSlug
|
||||
secretsPath := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.SecretsPath
|
||||
recursive := infisicalSecret.Spec.Authentication.ServiceToken.SecretsScope.Recursive
|
||||
|
||||
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaServiceToken(infisicalClient, infisicalToken, secretVersionBasedOnETag, envSlug, secretsPath, recursive)
|
||||
if err != nil {
|
||||
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
|
||||
}
|
||||
|
||||
logger.Info("ReconcileInfisicalSecret: Fetched secrets via [type=SERVICE_TOKEN]")
|
||||
|
||||
} else if authDetails.IsMachineIdentityAuth { // * Machine Identity authentication, the SDK will be authenticated at this point
|
||||
plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaMachineIdentity(infisicalClient, secretVersionBasedOnETag, authDetails.MachineIdentityScope)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("ReconcileInfisicalSecret: Fetched secrets via machine identity [type=%v]", authDetails.AuthStrategy))
|
||||
|
||||
} else {
|
||||
return errors.New("no authentication method provided yet. Please configure a authentication method then try again")
|
||||
}
|
||||
|
||||
if !updateDetails.Modified {
|
||||
logger.Info("ReconcileInfisicalSecret: No secrets modified so reconcile not needed")
|
||||
return nil
|
||||
}
|
||||
|
||||
if managedKubeSecret == nil {
|
||||
return r.createInfisicalManagedKubeSecret(ctx, logger, infisicalSecret, plainTextSecretsFromApi, updateDetails.ETag)
|
||||
} else {
|
||||
return r.updateInfisicalManagedKubeSecret(ctx, logger, infisicalSecret, *managedKubeSecret, plainTextSecretsFromApi, updateDetails.ETag)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
"k8s.io/client-go/rest"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/envtest"
|
||||
logf "sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log/zap"
|
||||
|
||||
secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
//+kubebuilder:scaffold:imports
|
||||
)
|
||||
|
||||
// These tests use Ginkgo (BDD-style Go testing framework). Refer to
|
||||
// http://onsi.github.io/ginkgo/ to learn more about Ginkgo.
|
||||
|
||||
var cfg *rest.Config
|
||||
var k8sClient client.Client
|
||||
var testEnv *envtest.Environment
|
||||
|
||||
func TestAPIs(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
RunSpecs(t, "Controller Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
|
||||
|
||||
By("bootstrapping test environment")
|
||||
testEnv = &envtest.Environment{
|
||||
CRDDirectoryPaths: []string{filepath.Join("..", "config", "crd", "bases")},
|
||||
ErrorIfCRDPathMissing: true,
|
||||
}
|
||||
|
||||
var err error
|
||||
// cfg is defined in this file globally.
|
||||
cfg, err = testEnv.Start()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cfg).NotTo(BeNil())
|
||||
|
||||
err = secretsv1alpha1.AddToScheme(scheme.Scheme)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
//+kubebuilder:scaffold:scheme
|
||||
|
||||
k8sClient, err = client.New(cfg, client.Options{Scheme: scheme.Scheme})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(k8sClient).NotTo(BeNil())
|
||||
|
||||
})
|
||||
|
||||
var _ = AfterSuite(func() {
|
||||
By("tearing down the test environment")
|
||||
err := testEnv.Stop()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
Reference in New Issue
Block a user