mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-05 12:26:38 +00:00
feat(k8-operator): dynamic secrets
This commit is contained in:
@@ -0,0 +1,207 @@
|
||||
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/controller/controllerutil"
|
||||
"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"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/constants"
|
||||
controllerhelpers "github.com/Infisical/infisical/k8-operator/packages/controllerhelpers"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/util"
|
||||
"github.com/go-logr/logr"
|
||||
)
|
||||
|
||||
// InfisicalDynamicSecretReconciler reconciles a InfisicalDynamicSecret object
|
||||
type InfisicalDynamicSecretReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
|
||||
BaseLogger logr.Logger
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicaldynamicsecrets,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicaldynamicsecrets/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicaldynamicsecrets/finalizers,verbs=update
|
||||
|
||||
var infisicalDynamicSecretsResourceVariablesMap map[string]util.ResourceVariables = make(map[string]util.ResourceVariables)
|
||||
|
||||
func (r *InfisicalDynamicSecretReconciler) GetLogger(req ctrl.Request) logr.Logger {
|
||||
return r.BaseLogger.WithValues("infisicaldynamicsecret", req.NamespacedName)
|
||||
}
|
||||
|
||||
func (r *InfisicalDynamicSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
|
||||
logger := r.GetLogger(req)
|
||||
|
||||
var infisicalDynamicSecretCRD secretsv1alpha1.InfisicalDynamicSecret
|
||||
requeueTime := time.Second * 5
|
||||
|
||||
err := r.Get(ctx, req.NamespacedName, &infisicalDynamicSecretCRD)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
logger.Info("Infisical Dynamic Secret CRD not found")
|
||||
return ctrl.Result{
|
||||
Requeue: false,
|
||||
}, nil
|
||||
} else {
|
||||
logger.Error(err, "Unable to fetch Infisical Dynamic Secret CRD from cluster")
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Add finalizer if it doesn't exist
|
||||
if !controllerutil.ContainsFinalizer(&infisicalDynamicSecretCRD, constants.INFISICAL_DYNAMIC_SECRET_FINALIZER_NAME) {
|
||||
controllerutil.AddFinalizer(&infisicalDynamicSecretCRD, constants.INFISICAL_DYNAMIC_SECRET_FINALIZER_NAME)
|
||||
if err := r.Update(ctx, &infisicalDynamicSecretCRD); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's being deleted
|
||||
if !infisicalDynamicSecretCRD.DeletionTimestamp.IsZero() {
|
||||
logger.Info("Handling deletion of InfisicalDynamicSecret")
|
||||
if controllerutil.ContainsFinalizer(&infisicalDynamicSecretCRD, constants.INFISICAL_DYNAMIC_SECRET_FINALIZER_NAME) {
|
||||
// We remove finalizers before running deletion logic to be completely safe from stuck resources
|
||||
infisicalDynamicSecretCRD.ObjectMeta.Finalizers = []string{}
|
||||
if err := r.Update(ctx, &infisicalDynamicSecretCRD); err != nil {
|
||||
logger.Error(err, fmt.Sprintf("Error removing finalizers from InfisicalDynamicSecret %s", infisicalDynamicSecretCRD.Name))
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
err := r.HandleLeaseRevocation(ctx, logger, infisicalDynamicSecretCRD)
|
||||
|
||||
if infisicalDynamicSecretsResourceVariablesMap != nil {
|
||||
if rv, ok := infisicalDynamicSecretsResourceVariablesMap[string(infisicalDynamicSecretCRD.GetUID())]; ok {
|
||||
rv.CancelCtx()
|
||||
delete(infisicalDynamicSecretsResourceVariablesMap, string(infisicalDynamicSecretCRD.GetUID()))
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err // Even if this fails, we still want to delete the CRD
|
||||
}
|
||||
|
||||
}
|
||||
return ctrl.Result{}, 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 infisicalDynamicSecretCRD.Spec.HostAPI == "" {
|
||||
api.API_HOST_URL = infisicalConfig["hostAPI"]
|
||||
} else {
|
||||
api.API_HOST_URL = util.AppendAPIEndpoint(infisicalDynamicSecretCRD.Spec.HostAPI)
|
||||
}
|
||||
|
||||
if infisicalDynamicSecretCRD.Spec.TLS.CaRef.SecretName != "" {
|
||||
api.API_CA_CERTIFICATE, err = r.getInfisicalCaCertificateFromKubeSecret(ctx, infisicalDynamicSecretCRD)
|
||||
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 = ""
|
||||
}
|
||||
|
||||
nextReconcile, err := r.ReconcileInfisicalDynamicSecret(ctx, logger, infisicalDynamicSecretCRD)
|
||||
// r.SetSuccessfullyReconciledConditions(ctx, &infisicalDynamicSecretCRD, err)
|
||||
|
||||
if err == nil && nextReconcile.Seconds() >= 5 {
|
||||
requeueTime = nextReconcile
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to reconcile Infisical Push Secret. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
_, err = controllerhelpers.ReconcileDeploymentsWithManagedSecrets(ctx, r.Client, logger, infisicalDynamicSecretCRD.Spec.ManagedSecretReference)
|
||||
|
||||
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("Next reconciliation in [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *InfisicalDynamicSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
|
||||
// Custom predicate that allows both spec changes and deletions
|
||||
specChangeOrDelete := predicate.Funcs{
|
||||
UpdateFunc: func(e event.UpdateEvent) bool {
|
||||
// Only reconcile if spec/generation changed
|
||||
|
||||
isSpecOrGenerationChange := e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration()
|
||||
|
||||
if isSpecOrGenerationChange {
|
||||
if infisicalDynamicSecretsResourceVariablesMap != nil {
|
||||
if rv, ok := infisicalDynamicSecretsResourceVariablesMap[string(e.ObjectNew.GetUID())]; ok {
|
||||
rv.CancelCtx()
|
||||
delete(infisicalDynamicSecretsResourceVariablesMap, string(e.ObjectNew.GetUID()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isSpecOrGenerationChange
|
||||
},
|
||||
DeleteFunc: func(e event.DeleteEvent) bool {
|
||||
// Always reconcile on deletion
|
||||
|
||||
if infisicalDynamicSecretsResourceVariablesMap != nil {
|
||||
if rv, ok := infisicalDynamicSecretsResourceVariablesMap[string(e.Object.GetUID())]; ok {
|
||||
rv.CancelCtx()
|
||||
delete(infisicalDynamicSecretsResourceVariablesMap, string(e.Object.GetUID()))
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
CreateFunc: func(e event.CreateEvent) bool {
|
||||
// Reconcile on creation
|
||||
return true
|
||||
},
|
||||
GenericFunc: func(e event.GenericEvent) bool {
|
||||
// Ignore generic events
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&secretsv1alpha1.InfisicalDynamicSecret{}, builder.WithPredicates(
|
||||
specChangeOrDelete,
|
||||
)).
|
||||
Complete(r)
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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/util"
|
||||
"github.com/go-logr/logr"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
infisicalSdk "github.com/infisical/go-sdk"
|
||||
k8Errors "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
)
|
||||
|
||||
func (r *InfisicalDynamicSecretReconciler) createInfisicalManagedKubeSecret(ctx context.Context, logger logr.Logger, infisicalDynamicSecret v1alpha1.InfisicalDynamicSecret, versionAnnotationValue string) error {
|
||||
secretType := infisicalDynamicSecret.Spec.ManagedSecretReference.SecretType
|
||||
|
||||
// copy labels and annotations from InfisicalSecret CRD
|
||||
labels := map[string]string{}
|
||||
for k, v := range infisicalDynamicSecret.Labels {
|
||||
labels[k] = v
|
||||
}
|
||||
|
||||
annotations := map[string]string{}
|
||||
systemPrefixes := []string{"kubectl.kubernetes.io/", "kubernetes.io/", "k8s.io/", "helm.sh/"}
|
||||
for k, v := range infisicalDynamicSecret.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] = versionAnnotationValue
|
||||
|
||||
// create a new secret as specified by the managed secret spec of CRD
|
||||
newKubeSecretInstance := &corev1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: infisicalDynamicSecret.Spec.ManagedSecretReference.SecretName,
|
||||
Namespace: infisicalDynamicSecret.Spec.ManagedSecretReference.SecretNamespace,
|
||||
Annotations: annotations,
|
||||
Labels: labels,
|
||||
},
|
||||
Type: corev1.SecretType(secretType),
|
||||
}
|
||||
|
||||
if infisicalDynamicSecret.Spec.ManagedSecretReference.CreationPolicy == "Owner" {
|
||||
// Set InfisicalSecret instance as the owner and controller of the managed secret
|
||||
err := ctrl.SetControllerReference(&infisicalDynamicSecret, 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. [type: %s]", secretType))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InfisicalDynamicSecretReconciler) handleAuthentication(ctx context.Context, infisicalSecret v1alpha1.InfisicalDynamicSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (util.AuthenticationDetails, error) {
|
||||
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_DYNAMIC_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 *InfisicalDynamicSecretReconciler) getInfisicalCaCertificateFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalDynamicSecret) (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
|
||||
}
|
||||
|
||||
func (r *InfisicalDynamicSecretReconciler) getResourceVariables(infisicalDynamicSecret v1alpha1.InfisicalDynamicSecret) util.ResourceVariables {
|
||||
|
||||
var resourceVariables util.ResourceVariables
|
||||
|
||||
if _, ok := infisicalDynamicSecretsResourceVariablesMap[string(infisicalDynamicSecret.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,
|
||||
})
|
||||
|
||||
infisicalDynamicSecretsResourceVariablesMap[string(infisicalDynamicSecret.UID)] = util.ResourceVariables{
|
||||
InfisicalClient: client,
|
||||
CancelCtx: cancel,
|
||||
AuthDetails: util.AuthenticationDetails{},
|
||||
}
|
||||
|
||||
resourceVariables = infisicalDynamicSecretsResourceVariablesMap[string(infisicalDynamicSecret.UID)]
|
||||
|
||||
} else {
|
||||
resourceVariables = infisicalDynamicSecretsResourceVariablesMap[string(infisicalDynamicSecret.UID)]
|
||||
}
|
||||
|
||||
return resourceVariables
|
||||
}
|
||||
|
||||
func (r *InfisicalDynamicSecretReconciler) CreateDynamicSecretLease(ctx context.Context, logger logr.Logger, infisicalClient infisicalSdk.InfisicalClientInterface, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, destination *corev1.Secret) error {
|
||||
project, err := util.GetProjectByID(infisicalClient.Auth().GetAccessToken(), infisicalDynamicSecret.Spec.DynamicSecret.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request := infisicalSdk.CreateDynamicSecretLeaseOptions{
|
||||
DynamicSecretName: infisicalDynamicSecret.Spec.DynamicSecret.SecretName,
|
||||
ProjectSlug: project.Slug,
|
||||
SecretPath: infisicalDynamicSecret.Spec.DynamicSecret.SecretPath,
|
||||
EnvironmentSlug: infisicalDynamicSecret.Spec.DynamicSecret.EnvironmentSlug,
|
||||
}
|
||||
|
||||
if infisicalDynamicSecret.Spec.LeaseTTL != "" {
|
||||
request.TTL = infisicalDynamicSecret.Spec.LeaseTTL
|
||||
}
|
||||
|
||||
leaseData, dynamicSecret, lease, err := infisicalClient.DynamicSecrets().Leases().Create(request)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to create lease [err=%s]", err)
|
||||
}
|
||||
|
||||
newLeaseStatus := &v1alpha1.InfisicalDynamicSecretLease{
|
||||
ID: lease.Id,
|
||||
ExpiresAt: metav1.NewTime(lease.ExpireAt),
|
||||
CreationTimestamp: metav1.NewTime(time.Now()),
|
||||
Version: int64(lease.Version),
|
||||
}
|
||||
|
||||
infisicalDynamicSecret.Status.DynamicSecretID = dynamicSecret.Id
|
||||
infisicalDynamicSecret.Status.MaxTTL = dynamicSecret.MaxTTL
|
||||
infisicalDynamicSecret.Status.Lease = newLeaseStatus
|
||||
|
||||
// write the leaseData to the destination secret
|
||||
destinationData := map[string]string{}
|
||||
|
||||
for key, value := range leaseData {
|
||||
if strValue, ok := value.(string); ok {
|
||||
destinationData[key] = strValue
|
||||
} else {
|
||||
return fmt.Errorf("unable to convert value to string for key %s", key)
|
||||
}
|
||||
}
|
||||
|
||||
destination.StringData = destinationData
|
||||
destination.Annotations[constants.SECRET_VERSION_ANNOTATION] = fmt.Sprintf("%s-%d", lease.Id, lease.Version)
|
||||
|
||||
if err := r.Client.Update(ctx, destination); err != nil {
|
||||
return fmt.Errorf("unable to update destination secret [err=%s]", err)
|
||||
}
|
||||
|
||||
if err := r.Client.Status().Update(ctx, infisicalDynamicSecret); err != nil {
|
||||
return fmt.Errorf("unable to update InfisicalDynamicSecret status [err=%s]", err)
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("New lease successfully created [leaseId=%s]", lease.Id))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InfisicalDynamicSecretReconciler) RenewDynamicSecretLease(ctx context.Context, logger logr.Logger, infisicalClient infisicalSdk.InfisicalClientInterface, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, destination *corev1.Secret) error {
|
||||
project, err := util.GetProjectByID(infisicalClient.Auth().GetAccessToken(), infisicalDynamicSecret.Spec.DynamicSecret.ProjectID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
request := infisicalSdk.RenewDynamicSecretLeaseOptions{
|
||||
LeaseId: infisicalDynamicSecret.Status.Lease.ID,
|
||||
ProjectSlug: project.Slug,
|
||||
SecretPath: infisicalDynamicSecret.Spec.DynamicSecret.SecretPath,
|
||||
EnvironmentSlug: infisicalDynamicSecret.Spec.DynamicSecret.EnvironmentSlug,
|
||||
}
|
||||
|
||||
if infisicalDynamicSecret.Spec.LeaseTTL != "" {
|
||||
request.TTL = infisicalDynamicSecret.Spec.LeaseTTL
|
||||
}
|
||||
|
||||
lease, err := infisicalClient.DynamicSecrets().Leases().RenewById(request)
|
||||
|
||||
if err != nil {
|
||||
|
||||
if strings.Contains(err.Error(), "TTL cannot be larger than max ttl") || // Case 1: TTL is larger than the max TTL
|
||||
strings.Contains(err.Error(), "Dynamic secret lease with ID") { // Case 2: The lease has already expired and has been deleted
|
||||
return constants.ErrInvalidLease
|
||||
}
|
||||
|
||||
return fmt.Errorf("unable to renew lease [err=%s]", err)
|
||||
}
|
||||
|
||||
infisicalDynamicSecret.Status.Lease.ExpiresAt = metav1.NewTime(lease.ExpireAt)
|
||||
|
||||
// update the infisicalDynamicSecret status
|
||||
if err := r.Client.Status().Update(ctx, infisicalDynamicSecret); err != nil {
|
||||
return fmt.Errorf("unable to update InfisicalDynamicSecret status [err=%s]", err)
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("Lease successfully renewed [leaseId=%s]", lease.Id))
|
||||
return nil
|
||||
|
||||
}
|
||||
|
||||
func (r *InfisicalDynamicSecretReconciler) updateResourceVariables(infisicalDynamicSecret v1alpha1.InfisicalDynamicSecret, resourceVariables util.ResourceVariables) {
|
||||
infisicalDynamicSecretsResourceVariablesMap[string(infisicalDynamicSecret.UID)] = resourceVariables
|
||||
}
|
||||
|
||||
func (r *InfisicalDynamicSecretReconciler) HandleLeaseRevocation(ctx context.Context, logger logr.Logger, infisicalDynamicSecret v1alpha1.InfisicalDynamicSecret) error {
|
||||
if infisicalDynamicSecret.Spec.LeaseRevocationPolicy != string(constants.DYNAMIC_SECRET_LEASE_REVOCATION_POLICY_ENABLED) {
|
||||
return nil
|
||||
}
|
||||
|
||||
resourceVariables := r.getResourceVariables(infisicalDynamicSecret)
|
||||
infisicalClient := resourceVariables.InfisicalClient
|
||||
|
||||
logger.Info("Authenticating for lease revocation")
|
||||
authDetails, err := r.handleAuthentication(ctx, infisicalDynamicSecret, infisicalClient)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to authenticate for lease revocation [err=%s]", err)
|
||||
}
|
||||
|
||||
r.updateResourceVariables(infisicalDynamicSecret, util.ResourceVariables{
|
||||
InfisicalClient: infisicalClient,
|
||||
CancelCtx: resourceVariables.CancelCtx,
|
||||
AuthDetails: authDetails,
|
||||
})
|
||||
|
||||
if infisicalDynamicSecret.Status.Lease == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
project, err := util.GetProjectByID(infisicalClient.Auth().GetAccessToken(), infisicalDynamicSecret.Spec.DynamicSecret.ProjectID)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
infisicalClient.DynamicSecrets().Leases().DeleteById(infisicalSdk.DeleteDynamicSecretLeaseOptions{
|
||||
LeaseId: infisicalDynamicSecret.Status.Lease.ID,
|
||||
ProjectSlug: project.Slug,
|
||||
SecretPath: infisicalDynamicSecret.Spec.DynamicSecret.SecretPath,
|
||||
EnvironmentSlug: infisicalDynamicSecret.Spec.DynamicSecret.EnvironmentSlug,
|
||||
})
|
||||
|
||||
// update the destination data to remove the lease data
|
||||
destination, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
|
||||
Name: infisicalDynamicSecret.Spec.ManagedSecretReference.SecretName,
|
||||
Namespace: infisicalDynamicSecret.Spec.ManagedSecretReference.SecretNamespace,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("unable to fetch destination secret [err=%s]", err)
|
||||
}
|
||||
|
||||
destination.Data = map[string][]byte{}
|
||||
|
||||
if err := r.Client.Update(ctx, destination); err != nil {
|
||||
return fmt.Errorf("unable to update destination secret [err=%s]", err)
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("Lease successfully revoked [leaseId=%s]", infisicalDynamicSecret.Status.Lease.ID))
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx context.Context, logger logr.Logger, infisicalDynamicSecret v1alpha1.InfisicalDynamicSecret) (time.Duration, error) {
|
||||
|
||||
resourceVariables := r.getResourceVariables(infisicalDynamicSecret)
|
||||
infisicalClient := resourceVariables.InfisicalClient
|
||||
cancelCtx := resourceVariables.CancelCtx
|
||||
authDetails := resourceVariables.AuthDetails
|
||||
|
||||
defaultNextReconcile := 5 * time.Second
|
||||
nextReconcile := defaultNextReconcile
|
||||
|
||||
var err error
|
||||
|
||||
if authDetails.AuthStrategy == "" {
|
||||
logger.Info("No authentication strategy found. Attempting to authenticate")
|
||||
authDetails, err = r.handleAuthentication(ctx, infisicalDynamicSecret, infisicalClient)
|
||||
|
||||
if err != nil {
|
||||
return nextReconcile, fmt.Errorf("unable to authenticate [err=%s]", err)
|
||||
}
|
||||
|
||||
r.updateResourceVariables(infisicalDynamicSecret, util.ResourceVariables{
|
||||
InfisicalClient: infisicalClient,
|
||||
CancelCtx: cancelCtx,
|
||||
AuthDetails: authDetails,
|
||||
})
|
||||
}
|
||||
|
||||
destination, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
|
||||
Name: infisicalDynamicSecret.Spec.ManagedSecretReference.SecretName,
|
||||
Namespace: infisicalDynamicSecret.Spec.ManagedSecretReference.SecretNamespace,
|
||||
})
|
||||
|
||||
if err != nil && !k8Errors.IsNotFound(err) {
|
||||
annotationValue := ""
|
||||
if infisicalDynamicSecret.Status.Lease != nil {
|
||||
annotationValue = fmt.Sprintf("%s-%d", infisicalDynamicSecret.Status.Lease.ID, infisicalDynamicSecret.Status.Lease.Version)
|
||||
}
|
||||
r.createInfisicalManagedKubeSecret(ctx, logger, infisicalDynamicSecret, annotationValue)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if k8Errors.IsNotFound(err) {
|
||||
return nextReconcile, fmt.Errorf("destination secret not found")
|
||||
}
|
||||
|
||||
return nextReconcile, fmt.Errorf("unable to fetch destination secret")
|
||||
}
|
||||
|
||||
if infisicalDynamicSecret.Status.Lease == nil {
|
||||
r.CreateDynamicSecretLease(ctx, logger, infisicalClient, &infisicalDynamicSecret, destination)
|
||||
} else {
|
||||
now := time.Now()
|
||||
leaseExpiresAt := infisicalDynamicSecret.Status.Lease.ExpiresAt.Time
|
||||
|
||||
// Calculate from creation to expiration
|
||||
originalLeaseDuration := leaseExpiresAt.Sub(infisicalDynamicSecret.Status.Lease.CreationTimestamp.Time)
|
||||
|
||||
// 30% of the original duration (if the TTL has 30% or less of its time left, renew)
|
||||
renewalThreshold := originalLeaseDuration * 30 / 100
|
||||
timeUntilExpiration := time.Until(leaseExpiresAt)
|
||||
|
||||
nextReconcile = timeUntilExpiration / 2
|
||||
|
||||
// Max TTL
|
||||
if infisicalDynamicSecret.Status.MaxTTL != "" {
|
||||
maxTTLDuration, err := util.ConvertIntervalToDuration(infisicalDynamicSecret.Status.MaxTTL)
|
||||
if err != nil {
|
||||
return defaultNextReconcile, fmt.Errorf("unable to parse MaxTTL duration: %w", err)
|
||||
}
|
||||
|
||||
// Calculate when this dynamic secret will hit its max TTL
|
||||
maxTTLExpirationTime := infisicalDynamicSecret.Status.Lease.CreationTimestamp.Add(maxTTLDuration)
|
||||
|
||||
// Calculate remaining time until max TTL
|
||||
timeUntilMaxTTL := maxTTLExpirationTime.Sub(now)
|
||||
maxTTLThreshold := maxTTLDuration * 40 / 100
|
||||
|
||||
// If we have less than 40% of max TTL remaining or have exceeded it, create new lease
|
||||
if timeUntilMaxTTL <= maxTTLThreshold || now.After(maxTTLExpirationTime) {
|
||||
logger.Info(fmt.Sprintf("Approaching or exceeded max TTL [timeUntilMaxTTL=%v] [maxTTLThreshold=%v], creating new lease...",
|
||||
timeUntilMaxTTL,
|
||||
maxTTLThreshold))
|
||||
|
||||
err := r.CreateDynamicSecretLease(ctx, logger, infisicalClient, &infisicalDynamicSecret, destination)
|
||||
return defaultNextReconcile, err // Short requeue after creation
|
||||
}
|
||||
}
|
||||
|
||||
// Fail-safe: If the lease has expired we create a new dynamic secret directly.
|
||||
if now.After(leaseExpiresAt) {
|
||||
logger.Info("Lease has expired, creating new lease...")
|
||||
err = r.CreateDynamicSecretLease(ctx, logger, infisicalClient, &infisicalDynamicSecret, destination)
|
||||
return defaultNextReconcile, err // Short requeue after creation
|
||||
}
|
||||
|
||||
if timeUntilExpiration < renewalThreshold {
|
||||
logger.Info(fmt.Sprintf("Lease renewal needed [leaseId=%s] [timeUntilExpiration=%v] [threshold=%v]",
|
||||
infisicalDynamicSecret.Status.Lease.ID,
|
||||
timeUntilExpiration,
|
||||
renewalThreshold))
|
||||
|
||||
err = r.RenewDynamicSecretLease(ctx, logger, infisicalClient, &infisicalDynamicSecret, destination)
|
||||
|
||||
if err == constants.ErrInvalidLease {
|
||||
logger.Info("Failed to renew expired lease, creating new lease...")
|
||||
err = r.CreateDynamicSecretLease(ctx, logger, infisicalClient, &infisicalDynamicSecret, destination)
|
||||
}
|
||||
return defaultNextReconcile, err // Short requeue after renewal/creation
|
||||
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Lease renewal not needed yet [leaseId=%s] [timeUntilExpiration=%v] [threshold=%v]",
|
||||
infisicalDynamicSecret.Status.Lease.ID,
|
||||
timeUntilExpiration,
|
||||
renewalThreshold))
|
||||
}
|
||||
|
||||
// Small buffer (20% of the calculated time) to ensure we don't cut it too close
|
||||
nextReconcile = nextReconcile * 8 / 10
|
||||
|
||||
// Minimum and maximum bounds for the reconcile interval (5 min max, 5 min minimum)
|
||||
nextReconcile = max(5*time.Second, min(nextReconcile, 5*time.Minute))
|
||||
}
|
||||
|
||||
return nextReconcile, nil
|
||||
}
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/api"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/constants"
|
||||
controllerhelpers "github.com/Infisical/infisical/k8-operator/packages/controllerutil"
|
||||
controllerhelpers "github.com/Infisical/infisical/k8-operator/packages/controllerhelpers"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/util"
|
||||
"github.com/go-logr/logr"
|
||||
)
|
||||
@@ -105,7 +105,7 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl.
|
||||
|
||||
if infisicalPushSecretCRD.Spec.ResyncInterval != "" {
|
||||
|
||||
duration, err := util.ConvertResyncIntervalToDuration(infisicalPushSecretCRD.Spec.ResyncInterval)
|
||||
duration, err := util.ConvertIntervalToDuration(infisicalPushSecretCRD.Spec.ResyncInterval)
|
||||
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to convert resync interval to duration. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
@@ -141,7 +141,7 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl.
|
||||
if infisicalPushSecretCRD.Spec.HostAPI == "" {
|
||||
api.API_HOST_URL = infisicalConfig["hostAPI"]
|
||||
} else {
|
||||
api.API_HOST_URL = infisicalPushSecretCRD.Spec.HostAPI
|
||||
api.API_HOST_URL = util.AppendAPIEndpoint(infisicalPushSecretCRD.Spec.HostAPI)
|
||||
}
|
||||
|
||||
if infisicalPushSecretCRD.Spec.TLS.CaRef.SecretName != "" {
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -15,7 +15,7 @@ import (
|
||||
|
||||
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"
|
||||
controllerhelpers "github.com/Infisical/infisical/k8-operator/packages/controllerhelpers"
|
||||
"github.com/Infisical/infisical/k8-operator/packages/util"
|
||||
"github.com/go-logr/logr"
|
||||
)
|
||||
@@ -110,7 +110,7 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
||||
if infisicalSecretCRD.Spec.HostAPI == "" {
|
||||
api.API_HOST_URL = infisicalConfig["hostAPI"]
|
||||
} else {
|
||||
api.API_HOST_URL = infisicalSecretCRD.Spec.HostAPI
|
||||
api.API_HOST_URL = util.AppendAPIEndpoint(infisicalSecretCRD.Spec.HostAPI)
|
||||
}
|
||||
|
||||
if infisicalSecretCRD.Spec.TLS.CaRef.SecretName != "" {
|
||||
@@ -138,7 +138,7 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
||||
}, nil
|
||||
}
|
||||
|
||||
numDeployments, err := r.ReconcileDeploymentsWithManagedSecrets(ctx, logger, infisicalSecretCRD)
|
||||
numDeployments, err := controllerhelpers.ReconcileDeploymentsWithManagedSecrets(ctx, r.Client, logger, infisicalSecretCRD.Spec.ManagedSecretReference)
|
||||
r.SetInfisicalAutoRedeploymentReady(ctx, logger, &infisicalSecretCRD, numDeployments, err)
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to reconcile auto redeployment. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
|
||||
Reference in New Issue
Block a user