Merge branch 'main' of github.com:Infisical/infisical into sid/k8s-operator

This commit is contained in:
sidwebworks
2025-08-12 16:49:27 +05:30
271 changed files with 10389 additions and 6590 deletions
@@ -0,0 +1,146 @@
package infisicaldynamicsecret
import (
"context"
"fmt"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (r *InfisicalDynamicSecretReconciler) SetReconcileAutoRedeploymentConditionStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, numDeployments int, errorToConditionOn error) {
if infisicalDynamicSecret.Status.Conditions == nil {
infisicalDynamicSecret.Status.Conditions = []metav1.Condition{}
}
if errorToConditionOn == nil {
meta.SetStatusCondition(&infisicalDynamicSecret.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 dynamic secret lease changes", numDeployments),
})
} else {
meta.SetStatusCondition(&infisicalDynamicSecret.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, infisicalDynamicSecret)
if err != nil {
logger.Error(err, "Could not set condition for AutoRedeployReady")
}
}
func (r *InfisicalDynamicSecretReconciler) SetAuthenticatedConditionStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) {
if infisicalDynamicSecret.Status.Conditions == nil {
infisicalDynamicSecret.Status.Conditions = []metav1.Condition{}
}
if errorToConditionOn == nil {
meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/Authenticated",
Status: metav1.ConditionTrue,
Reason: "OK",
Message: "Infisical has successfully authenticated with the Infisical API",
})
} else {
meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/Authenticated",
Status: metav1.ConditionFalse,
Reason: "Error",
Message: fmt.Sprintf("Failed to authenticate with Infisical API because: %v", errorToConditionOn),
})
}
err := r.Client.Status().Update(ctx, infisicalDynamicSecret)
if err != nil {
logger.Error(err, "Could not set condition for Authenticated")
}
}
func (r *InfisicalDynamicSecretReconciler) SetLeaseRenewalConditionStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) {
if infisicalDynamicSecret.Status.Conditions == nil {
infisicalDynamicSecret.Status.Conditions = []metav1.Condition{}
}
if errorToConditionOn == nil {
meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/LeaseRenewal",
Status: metav1.ConditionTrue,
Reason: "OK",
Message: "Infisical has successfully renewed the lease",
})
} else {
meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/LeaseRenewal",
Status: metav1.ConditionFalse,
Reason: "Error",
Message: fmt.Sprintf("Failed to renew the lease because: %v", errorToConditionOn),
})
}
err := r.Client.Status().Update(ctx, infisicalDynamicSecret)
if err != nil {
logger.Error(err, "Could not set condition for LeaseRenewal")
}
}
func (r *InfisicalDynamicSecretReconciler) SetCreatedLeaseConditionStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) {
if infisicalDynamicSecret.Status.Conditions == nil {
infisicalDynamicSecret.Status.Conditions = []metav1.Condition{}
}
if errorToConditionOn == nil {
meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/LeaseCreated",
Status: metav1.ConditionTrue,
Reason: "OK",
Message: "Infisical has successfully created the lease",
})
} else {
meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/LeaseCreated",
Status: metav1.ConditionFalse,
Reason: "Error",
Message: fmt.Sprintf("Failed to create the lease because: %v", errorToConditionOn),
})
}
err := r.Client.Status().Update(ctx, infisicalDynamicSecret)
if err != nil {
logger.Error(err, "Could not set condition for LeaseCreated")
}
}
func (r *InfisicalDynamicSecretReconciler) SetReconcileConditionStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) {
if infisicalDynamicSecret.Status.Conditions == nil {
infisicalDynamicSecret.Status.Conditions = []metav1.Condition{}
}
if errorToConditionOn == nil {
meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/Reconcile",
Status: metav1.ConditionTrue,
Reason: "OK",
Message: "Infisical has successfully reconciled the InfisicalDynamicSecret",
})
} else {
meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/Reconcile",
Status: metav1.ConditionFalse,
Reason: "Error",
Message: fmt.Sprintf("Failed to reconcile the InfisicalDynamicSecret because: %v", errorToConditionOn),
})
}
err := r.Client.Status().Update(ctx, infisicalDynamicSecret)
if err != nil {
logger.Error(err, "Could not set condition for Reconcile")
}
}
@@ -0,0 +1,120 @@
package infisicaldynamicsecret
import (
"context"
"fmt"
"math/rand"
"time"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/internal/api"
"github.com/Infisical/infisical/k8-operator/internal/util"
"github.com/go-logr/logr"
k8Errors "k8s.io/apimachinery/pkg/api/errors"
)
type InfisicalDynamicSecretHandler struct {
client.Client
Scheme *runtime.Scheme
Random *rand.Rand
IsNamespaceScoped bool
}
func NewInfisicalDynamicSecretHandler(client client.Client, scheme *runtime.Scheme, isNamespaceScoped bool) *InfisicalDynamicSecretHandler {
return &InfisicalDynamicSecretHandler{
Client: client,
Scheme: scheme,
Random: rand.New(rand.NewSource(time.Now().UnixNano())),
IsNamespaceScoped: isNamespaceScoped,
}
}
func (h *InfisicalDynamicSecretHandler) SetupAPIConfig(infisicalDynamicSecret v1alpha1.InfisicalDynamicSecret, infisicalConfig map[string]string) error {
if infisicalDynamicSecret.Spec.HostAPI == "" {
api.API_HOST_URL = infisicalConfig["hostAPI"]
} else {
api.API_HOST_URL = util.AppendAPIEndpoint(infisicalDynamicSecret.Spec.HostAPI)
}
return nil
}
func (h *InfisicalDynamicSecretHandler) getInfisicalCaCertificateFromKubeSecret(ctx context.Context, infisicalDynamicSecret v1alpha1.InfisicalDynamicSecret) (caCertificate string, err error) {
caCertificateFromKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, h.Client, types.NamespacedName{
Namespace: infisicalDynamicSecret.Spec.TLS.CaRef.SecretNamespace,
Name: infisicalDynamicSecret.Spec.TLS.CaRef.SecretName,
})
if k8Errors.IsNotFound(err) {
return "", fmt.Errorf("kubernetes secret containing custom CA certificate cannot be found. [err=%s]", err)
}
if util.IsNamespaceScopedError(err, h.IsNamespaceScoped) {
return "", fmt.Errorf("unable to fetch Kubernetes CA certificate secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the CA certificate secret is in the same namespace as the operator. [err=%v]", err)
}
if err != nil {
return "", fmt.Errorf("something went wrong when fetching your CA certificate [err=%s]", err)
}
caCertificateFromSecret := string(caCertificateFromKubeSecret.Data[infisicalDynamicSecret.Spec.TLS.CaRef.SecretKey])
return caCertificateFromSecret, nil
}
func (h *InfisicalDynamicSecretHandler) HandleCACertificate(ctx context.Context, infisicalDynamicSecret v1alpha1.InfisicalDynamicSecret) error {
if infisicalDynamicSecret.Spec.TLS.CaRef.SecretName != "" {
caCert, err := h.getInfisicalCaCertificateFromKubeSecret(ctx, infisicalDynamicSecret)
if err != nil {
return err
}
api.API_CA_CERTIFICATE = caCert
} else {
api.API_CA_CERTIFICATE = ""
}
return nil
}
func (h *InfisicalDynamicSecretHandler) ReconcileInfisicalDynamicSecret(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, resourceVariablesMap map[string]util.ResourceVariables) (time.Duration, error) {
reconciler := &InfisicalDynamicSecretReconciler{
Client: h.Client,
Scheme: h.Scheme,
Random: h.Random,
IsNamespaceScoped: h.IsNamespaceScoped,
}
return reconciler.ReconcileInfisicalDynamicSecret(ctx, logger, infisicalDynamicSecret, resourceVariablesMap)
}
func (h *InfisicalDynamicSecretHandler) HandleLeaseRevocation(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, resourceVariablesMap map[string]util.ResourceVariables) error {
reconciler := &InfisicalDynamicSecretReconciler{
Client: h.Client,
Scheme: h.Scheme,
Random: h.Random,
IsNamespaceScoped: h.IsNamespaceScoped,
}
return reconciler.HandleLeaseRevocation(ctx, logger, infisicalDynamicSecret, resourceVariablesMap)
}
func (h *InfisicalDynamicSecretHandler) SetReconcileConditionStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) {
reconciler := &InfisicalDynamicSecretReconciler{
Client: h.Client,
Scheme: h.Scheme,
Random: h.Random,
IsNamespaceScoped: h.IsNamespaceScoped,
}
reconciler.SetReconcileConditionStatus(ctx, logger, infisicalDynamicSecret, errorToConditionOn)
}
func (h *InfisicalDynamicSecretHandler) SetReconcileAutoRedeploymentConditionStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, numDeployments int, errorToConditionOn error) {
reconciler := &InfisicalDynamicSecretReconciler{
Client: h.Client,
Scheme: h.Scheme,
Random: h.Random,
IsNamespaceScoped: h.IsNamespaceScoped,
}
reconciler.SetReconcileAutoRedeploymentConditionStatus(ctx, logger, infisicalDynamicSecret, numDeployments, errorToConditionOn)
}
@@ -0,0 +1,431 @@
package infisicaldynamicsecret
import (
"context"
"fmt"
"math/rand"
"strings"
"time"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/internal/api"
"github.com/Infisical/infisical/k8-operator/internal/constants"
"github.com/Infisical/infisical/k8-operator/internal/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"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
)
type InfisicalDynamicSecretReconciler struct {
client.Client
Scheme *runtime.Scheme
Random *rand.Rand
IsNamespaceScoped bool
}
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) getResourceVariables(infisicalDynamicSecret v1alpha1.InfisicalDynamicSecret, resourceVariablesMap map[string]util.ResourceVariables) util.ResourceVariables {
var resourceVariables util.ResourceVariables
if _, ok := resourceVariablesMap[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,
})
resourceVariablesMap[string(infisicalDynamicSecret.UID)] = util.ResourceVariables{
InfisicalClient: client,
CancelCtx: cancel,
AuthDetails: util.AuthenticationDetails{},
}
resourceVariables = resourceVariablesMap[string(infisicalDynamicSecret.UID)]
} else {
resourceVariables = resourceVariablesMap[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)
}
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, resourceVariablesMap map[string]util.ResourceVariables) {
resourceVariablesMap[string(infisicalDynamicSecret.UID)] = resourceVariables
}
func (r *InfisicalDynamicSecretReconciler) HandleLeaseRevocation(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, resourceVariablesMap map[string]util.ResourceVariables) error {
if infisicalDynamicSecret.Spec.LeaseRevocationPolicy != string(constants.DYNAMIC_SECRET_LEASE_REVOCATION_POLICY_ENABLED) {
return nil
}
resourceVariables := r.getResourceVariables(*infisicalDynamicSecret, resourceVariablesMap)
infisicalClient := resourceVariables.InfisicalClient
logger.Info("Authenticating for lease revocation")
authDetails, err := util.HandleAuthentication(ctx, util.SecretAuthInput{
Secret: *infisicalDynamicSecret,
Type: util.SecretCrd.INFISICAL_DYNAMIC_SECRET,
}, r.Client, infisicalClient, r.IsNamespaceScoped)
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,
}, resourceVariablesMap)
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 {
if util.IsNamespaceScopedError(err, r.IsNamespaceScoped) {
return fmt.Errorf("unable to fetch Kubernetes destination secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the destination secret is in the same namespace as the operator. [err=%v]", err)
}
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, resourceVariablesMap map[string]util.ResourceVariables) (time.Duration, error) {
resourceVariables := r.getResourceVariables(*infisicalDynamicSecret, resourceVariablesMap)
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 = util.HandleAuthentication(ctx, util.SecretAuthInput{
Secret: *infisicalDynamicSecret,
Type: util.SecretCrd.INFISICAL_DYNAMIC_SECRET,
}, r.Client, infisicalClient, r.IsNamespaceScoped)
if err != nil {
return nextReconcile, fmt.Errorf("unable to authenticate [err=%s]", err)
}
r.updateResourceVariables(*infisicalDynamicSecret, util.ResourceVariables{
InfisicalClient: infisicalClient,
CancelCtx: cancelCtx,
AuthDetails: authDetails,
}, resourceVariablesMap)
}
destination, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
Name: infisicalDynamicSecret.Spec.ManagedSecretReference.SecretName,
Namespace: infisicalDynamicSecret.Spec.ManagedSecretReference.SecretNamespace,
})
if err != nil {
if util.IsNamespaceScopedError(err, r.IsNamespaceScoped) {
return nextReconcile, fmt.Errorf("unable to fetch Kubernetes destination secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the destination secret is in the same namespace as the operator. [err=%v]", err)
}
if 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)
destination, err = util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
Name: infisicalDynamicSecret.Spec.ManagedSecretReference.SecretName,
Namespace: infisicalDynamicSecret.Spec.ManagedSecretReference.SecretNamespace,
})
if err != nil {
if util.IsNamespaceScopedError(err, r.IsNamespaceScoped) {
return nextReconcile, fmt.Errorf("unable to fetch Kubernetes destination secret after creation. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the destination secret is in the same namespace as the operator. [err=%v]", err)
}
return nextReconcile, fmt.Errorf("unable to fetch destination secret after creation [err=%s]", err)
}
} else {
return nextReconcile, fmt.Errorf("unable to fetch destination secret")
}
}
if infisicalDynamicSecret.Status.Lease == nil {
err := r.CreateDynamicSecretLease(ctx, logger, infisicalClient, infisicalDynamicSecret, destination)
return defaultNextReconcile, err // Short requeue after creation
} else {
now := time.Now()
leaseExpiresAt := infisicalDynamicSecret.Status.Lease.ExpiresAt.Time
// Calculate from creation to expiration
originalLeaseDuration := leaseExpiresAt.Sub(infisicalDynamicSecret.Status.Lease.CreationTimestamp.Time)
// Generate a random percentage between 20% and 30%
jitterPercentage := 20 + r.Random.Intn(11) // Random int from 0 to 10, then add 20
renewalThreshold := originalLeaseDuration * time.Duration(jitterPercentage) / 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 || timeUntilExpiration < 30*time.Second {
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))
}
if err := r.Client.Status().Update(ctx, infisicalDynamicSecret); err != nil {
return nextReconcile, fmt.Errorf("unable to update InfisicalDynamicSecret status [err=%s]", err)
}
return nextReconcile, nil
}
@@ -0,0 +1,156 @@
package infisicalpushsecret
import (
"context"
"fmt"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (r *InfisicalPushSecretReconciler) SetReconcileStatusCondition(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, err error) error {
if infisicalPushSecret.Status.Conditions == nil {
infisicalPushSecret.Status.Conditions = []metav1.Condition{}
}
if err != nil {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/Reconcile",
Status: metav1.ConditionTrue,
Reason: "Error",
Message: fmt.Sprintf("Reconcile failed, secrets were not pushed to Infisical. Error: %s", err.Error()),
})
} else {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/Reconcile",
Status: metav1.ConditionFalse,
Reason: "OK",
Message: "Reconcile succeeded, secrets were pushed to Infisical",
})
}
return r.Client.Status().Update(ctx, infisicalPushSecret)
}
func (r *InfisicalPushSecretReconciler) SetFailedToReplaceSecretsStatusCondition(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, failMessage string) error {
if infisicalPushSecret.Status.Conditions == nil {
infisicalPushSecret.Status.Conditions = []metav1.Condition{}
}
if failMessage != "" {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/FailedToReplaceSecrets",
Status: metav1.ConditionTrue,
Reason: "Error",
Message: failMessage,
})
} else {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/FailedToReplaceSecrets",
Status: metav1.ConditionFalse,
Reason: "OK",
Message: "No errors, no secrets failed to be replaced in Infisical",
})
}
return r.Client.Status().Update(ctx, infisicalPushSecret)
}
func (r *InfisicalPushSecretReconciler) SetFailedToCreateSecretsStatusCondition(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, failMessage string) error {
if infisicalPushSecret.Status.Conditions == nil {
infisicalPushSecret.Status.Conditions = []metav1.Condition{}
}
if failMessage != "" {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/FailedToCreateSecrets",
Status: metav1.ConditionTrue,
Reason: "Error",
Message: failMessage,
})
} else {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/FailedToCreateSecrets",
Status: metav1.ConditionFalse,
Reason: "OK",
Message: "No errors encountered, no secrets failed to be created in Infisical",
})
}
return r.Client.Status().Update(ctx, infisicalPushSecret)
}
func (r *InfisicalPushSecretReconciler) SetFailedToUpdateSecretsStatusCondition(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, failMessage string) error {
if infisicalPushSecret.Status.Conditions == nil {
infisicalPushSecret.Status.Conditions = []metav1.Condition{}
}
if failMessage != "" {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/FailedToUpdateSecrets",
Status: metav1.ConditionTrue,
Reason: "Error",
Message: failMessage,
})
} else {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/FailedToUpdateSecrets",
Status: metav1.ConditionFalse,
Reason: "OK",
Message: "No errors encountered, no secrets failed to be updated in Infisical",
})
}
return r.Client.Status().Update(ctx, infisicalPushSecret)
}
func (r *InfisicalPushSecretReconciler) SetFailedToDeleteSecretsStatusCondition(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, failMessage string) error {
if infisicalPushSecret.Status.Conditions == nil {
infisicalPushSecret.Status.Conditions = []metav1.Condition{}
}
if failMessage != "" {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/FailedToDeleteSecrets",
Status: metav1.ConditionTrue,
Reason: "Error",
Message: failMessage,
})
} else {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/FailedToDeleteSecrets",
Status: metav1.ConditionFalse,
Reason: "OK",
Message: "No errors encountered, no secrets failed to be deleted",
})
}
return r.Client.Status().Update(ctx, infisicalPushSecret)
}
func (r *InfisicalPushSecretReconciler) SetAuthenticatedStatusCondition(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, errorToConditionOn error) error {
if infisicalPushSecret.Status.Conditions == nil {
infisicalPushSecret.Status.Conditions = []metav1.Condition{}
}
if errorToConditionOn != nil {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/Authenticated",
Status: metav1.ConditionFalse,
Reason: "Error",
Message: "Failed to authenticate with Infisical API. This can be caused by invalid service token or an invalid API host that is set. Check operator logs for more info",
})
} else {
meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/Authenticated",
Status: metav1.ConditionTrue,
Reason: "OK",
Message: "Successfully authenticated with Infisical API",
})
}
return r.Client.Status().Update(ctx, infisicalPushSecret)
}
@@ -0,0 +1,103 @@
package infisicalpushsecret
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/internal/api"
"github.com/Infisical/infisical/k8-operator/internal/util"
"github.com/go-logr/logr"
k8Errors "k8s.io/apimachinery/pkg/api/errors"
)
type InfisicalPushSecretHandler struct {
client.Client
Scheme *runtime.Scheme
IsNamespaceScoped bool
}
func NewInfisicalPushSecretHandler(client client.Client, scheme *runtime.Scheme, isNamespaceScoped bool) *InfisicalPushSecretHandler {
return &InfisicalPushSecretHandler{
Client: client,
Scheme: scheme,
IsNamespaceScoped: isNamespaceScoped,
}
}
func (h *InfisicalPushSecretHandler) SetupAPIConfig(infisicalPushSecret v1alpha1.InfisicalPushSecret, infisicalConfig map[string]string) error {
if infisicalPushSecret.Spec.HostAPI == "" {
api.API_HOST_URL = infisicalConfig["hostAPI"]
} else {
api.API_HOST_URL = util.AppendAPIEndpoint(infisicalPushSecret.Spec.HostAPI)
}
return nil
}
func (h *InfisicalPushSecretHandler) getInfisicalCaCertificateFromKubeSecret(ctx context.Context, infisicalPushSecret v1alpha1.InfisicalPushSecret) (caCertificate string, err error) {
caCertificateFromKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, h.Client, types.NamespacedName{
Namespace: infisicalPushSecret.Spec.TLS.CaRef.SecretNamespace,
Name: infisicalPushSecret.Spec.TLS.CaRef.SecretName,
})
if k8Errors.IsNotFound(err) {
return "", fmt.Errorf("kubernetes secret containing custom CA certificate cannot be found. [err=%s]", err)
}
if util.IsNamespaceScopedError(err, h.IsNamespaceScoped) {
return "", fmt.Errorf("unable to fetch Kubernetes CA certificate secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the CA certificate secret is in the same namespace as the operator. [err=%v]", err)
}
if err != nil {
return "", fmt.Errorf("something went wrong when fetching your CA certificate [err=%s]", err)
}
caCertificateFromSecret := string(caCertificateFromKubeSecret.Data[infisicalPushSecret.Spec.TLS.CaRef.SecretKey])
return caCertificateFromSecret, nil
}
func (h *InfisicalPushSecretHandler) HandleCACertificate(ctx context.Context, infisicalPushSecret v1alpha1.InfisicalPushSecret) error {
if infisicalPushSecret.Spec.TLS.CaRef.SecretName != "" {
caCert, err := h.getInfisicalCaCertificateFromKubeSecret(ctx, infisicalPushSecret)
if err != nil {
return err
}
api.API_CA_CERTIFICATE = caCert
} else {
api.API_CA_CERTIFICATE = ""
}
return nil
}
func (h *InfisicalPushSecretHandler) ReconcileInfisicalPushSecret(ctx context.Context, logger logr.Logger, infisicalPushSecret *v1alpha1.InfisicalPushSecret, resourceVariablesMap map[string]util.ResourceVariables) error {
reconciler := &InfisicalPushSecretReconciler{
Client: h.Client,
Scheme: h.Scheme,
IsNamespaceScoped: h.IsNamespaceScoped,
}
return reconciler.ReconcileInfisicalPushSecret(ctx, logger, infisicalPushSecret, resourceVariablesMap)
}
func (h *InfisicalPushSecretHandler) DeleteManagedSecrets(ctx context.Context, logger logr.Logger, infisicalPushSecret *v1alpha1.InfisicalPushSecret, resourceVariablesMap map[string]util.ResourceVariables) error {
reconciler := &InfisicalPushSecretReconciler{
Client: h.Client,
Scheme: h.Scheme,
IsNamespaceScoped: h.IsNamespaceScoped,
}
return reconciler.DeleteManagedSecrets(ctx, logger, infisicalPushSecret, resourceVariablesMap)
}
func (h *InfisicalPushSecretHandler) SetReconcileStatusCondition(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, err error) {
reconciler := &InfisicalPushSecretReconciler{
Client: h.Client,
Scheme: h.Scheme,
IsNamespaceScoped: h.IsNamespaceScoped,
}
reconciler.SetReconcileStatusCondition(ctx, infisicalPushSecret, err)
}
@@ -0,0 +1,573 @@
package infisicalpushsecret
import (
"bytes"
"context"
"fmt"
"strings"
tpl "text/template"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/internal/api"
"github.com/Infisical/infisical/k8-operator/internal/constants"
"github.com/Infisical/infisical/k8-operator/internal/model"
"github.com/Infisical/infisical/k8-operator/internal/template"
"github.com/Infisical/infisical/k8-operator/internal/util"
"github.com/go-logr/logr"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
generatorUtil "github.com/Infisical/infisical/k8-operator/internal/generator"
infisicalSdk "github.com/infisical/go-sdk"
"k8s.io/apimachinery/pkg/runtime"
)
type InfisicalPushSecretReconciler struct {
client.Client
Scheme *runtime.Scheme
IsNamespaceScoped bool
}
func (r *InfisicalPushSecretReconciler) getResourceVariables(infisicalPushSecret v1alpha1.InfisicalPushSecret, resourceVariablesMap map[string]util.ResourceVariables) util.ResourceVariables {
var resourceVariables util.ResourceVariables
if _, ok := resourceVariablesMap[string(infisicalPushSecret.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(infisicalPushSecret.UID)] = util.ResourceVariables{
InfisicalClient: client,
CancelCtx: cancel,
AuthDetails: util.AuthenticationDetails{},
}
resourceVariables = resourceVariablesMap[string(infisicalPushSecret.UID)]
} else {
resourceVariables = resourceVariablesMap[string(infisicalPushSecret.UID)]
}
return resourceVariables
}
func (r *InfisicalPushSecretReconciler) updateResourceVariables(infisicalPushSecret v1alpha1.InfisicalPushSecret, resourceVariables util.ResourceVariables, resourceVariablesMap map[string]util.ResourceVariables) {
resourceVariablesMap[string(infisicalPushSecret.UID)] = resourceVariables
}
func (r *InfisicalPushSecretReconciler) processGenerators(ctx context.Context, infisicalPushSecret v1alpha1.InfisicalPushSecret) (map[string]string, error) {
processedSecrets := make(map[string]string)
if len(infisicalPushSecret.Spec.Push.Generators) == 0 {
return processedSecrets, nil
}
for _, generator := range infisicalPushSecret.Spec.Push.Generators {
generatorRef := generator.GeneratorRef
clusterGenerator := &v1alpha1.ClusterGenerator{}
err := r.Client.Get(ctx, types.NamespacedName{Name: generatorRef.Name}, clusterGenerator)
if err != nil {
return nil, fmt.Errorf("unable to get ClusterGenerator resource [err=%s]", err)
}
if generatorRef.Kind == v1alpha1.GeneratorKindPassword {
// get the custom ClusterGenerator resource from the cluster
if clusterGenerator.Spec.Generator.PasswordSpec == nil {
return nil, fmt.Errorf("password spec is not defined in the ClusterGenerator resource")
}
password, err := generatorUtil.GeneratorPassword(*clusterGenerator.Spec.Generator.PasswordSpec)
if err != nil {
return nil, fmt.Errorf("unable to generate password [err=%s]", err)
}
processedSecrets[generator.DestinationSecretName] = password
}
if generatorRef.Kind == v1alpha1.GeneratorKindUUID {
uuid, err := generatorUtil.GeneratorUUID()
if err != nil {
return nil, fmt.Errorf("unable to generate UUID [err=%s]", err)
}
processedSecrets[generator.DestinationSecretName] = uuid
}
}
return processedSecrets, nil
}
func (r *InfisicalPushSecretReconciler) processTemplatedSecrets(infisicalPushSecret v1alpha1.InfisicalPushSecret, kubePushSecret *corev1.Secret, destination v1alpha1.InfisicalPushSecretDestination) (map[string]string, error) {
processedSecrets := make(map[string]string)
sourceSecrets := make(map[string]model.SecretTemplateOptions)
for key, value := range kubePushSecret.Data {
sourceSecrets[key] = model.SecretTemplateOptions{
Value: string(value),
SecretPath: destination.SecretsPath,
}
}
if infisicalPushSecret.Spec.Push.Secret.Template == nil || (infisicalPushSecret.Spec.Push.Secret.Template != nil && infisicalPushSecret.Spec.Push.Secret.Template.IncludeAllSecrets) {
for key, value := range kubePushSecret.Data {
processedSecrets[key] = string(value)
}
}
if infisicalPushSecret.Spec.Push.Secret.Template != nil &&
len(infisicalPushSecret.Spec.Push.Secret.Template.Data) > 0 {
for templateKey, userTemplate := range infisicalPushSecret.Spec.Push.Secret.Template.Data {
tmpl, err := tpl.New("push-secret-templates").Funcs(template.GetTemplateFunctions()).Parse(userTemplate)
if err != nil {
return nil, fmt.Errorf("unable to compile template: %s [err=%v]", templateKey, err)
}
buf := bytes.NewBuffer(nil)
err = tmpl.Execute(buf, sourceSecrets)
if err != nil {
return nil, fmt.Errorf("unable to execute template: %s [err=%v]", templateKey, err)
}
processedSecrets[templateKey] = buf.String()
}
}
return processedSecrets, nil
}
func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context.Context, logger logr.Logger, infisicalPushSecret *v1alpha1.InfisicalPushSecret, resourceVariablesMap map[string]util.ResourceVariables) error {
resourceVariables := r.getResourceVariables(*infisicalPushSecret, resourceVariablesMap)
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 = util.HandleAuthentication(ctx, util.SecretAuthInput{
Secret: *infisicalPushSecret,
Type: util.SecretCrd.INFISICAL_PUSH_SECRET,
}, r.Client, infisicalClient, r.IsNamespaceScoped)
r.SetAuthenticatedStatusCondition(ctx, infisicalPushSecret, err)
if err != nil {
return fmt.Errorf("unable to authenticate [err=%s]", err)
}
r.updateResourceVariables(*infisicalPushSecret, util.ResourceVariables{
InfisicalClient: infisicalClient,
CancelCtx: cancelCtx,
AuthDetails: authDetails,
}, resourceVariablesMap)
}
processedSecrets := make(map[string]string)
if infisicalPushSecret.Spec.Push.Secret != nil {
kubePushSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
Namespace: infisicalPushSecret.Spec.Push.Secret.SecretNamespace,
Name: infisicalPushSecret.Spec.Push.Secret.SecretName,
})
if err != nil {
if util.IsNamespaceScopedError(err, r.IsNamespaceScoped) {
return fmt.Errorf("unable to fetch Kubernetes destination secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the destination secret is in the same namespace as the operator. [err=%v]", err)
}
return fmt.Errorf("unable to fetch kube secret [err=%s]", err)
}
processedSecrets, err = r.processTemplatedSecrets(*infisicalPushSecret, kubePushSecret, infisicalPushSecret.Spec.Destination)
if err != nil {
return fmt.Errorf("unable to process templated secrets [err=%s]", err)
}
}
generatorSecrets, err := r.processGenerators(ctx, *infisicalPushSecret)
if err != nil {
return fmt.Errorf("unable to process generators [err=%s]", err)
}
for key, value := range generatorSecrets {
processedSecrets[key] = value
}
destination := infisicalPushSecret.Spec.Destination
existingSecrets, err := infisicalClient.Secrets().List(infisicalSdk.ListSecretsOptions{
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
IncludeImports: false,
})
getExistingSecretByKey := func(key string) *infisicalSdk.Secret {
for _, secret := range existingSecrets {
if secret.SecretKey == key {
return &secret
}
}
return nil
}
getExistingSecretById := func(id string) *infisicalSdk.Secret {
for _, secret := range existingSecrets {
if secret.ID == id {
return &secret
}
}
return nil
}
updateExistingSecretByKey := func(key string, newSecretValue string) {
for i := range existingSecrets {
if existingSecrets[i].SecretKey == key {
existingSecrets[i].SecretValue = newSecretValue
break
}
}
}
if err != nil {
return fmt.Errorf("unable to list secrets [err=%s]", err)
}
updatePolicy := infisicalPushSecret.Spec.UpdatePolicy
var secretsFailedToCreate []string
var secretsFailedToUpdate []string
var secretsFailedToDelete []string
var secretsFailedToReplaceById []string
// If the ManagedSecrets are nil, we know this is the first time the InfisicalPushSecret is being reconciled.
if infisicalPushSecret.Status.ManagedSecrets == nil {
infisicalPushSecret.Status.ManagedSecrets = make(map[string]string) // (string[id], string[key] )
for secretKey, secretValue := range processedSecrets {
if exists := getExistingSecretByKey(secretKey); exists != nil {
if updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) {
updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{
SecretKey: secretKey,
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
NewSecretValue: secretValue,
})
if err != nil {
secretsFailedToUpdate = append(secretsFailedToUpdate, secretKey)
logger.Info(fmt.Sprintf("unable to update secret [key=%s] [err=%s]", secretKey, err))
continue
}
infisicalPushSecret.Status.ManagedSecrets[updatedSecret.ID] = secretKey
}
} else {
createdSecret, err := infisicalClient.Secrets().Create(infisicalSdk.CreateSecretOptions{
SecretKey: secretKey,
SecretValue: secretValue,
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
})
if err != nil {
secretsFailedToCreate = append(secretsFailedToCreate, secretKey)
logger.Info(fmt.Sprintf("unable to create secret [key=%s] [err=%s]", secretKey, err))
continue
}
infisicalPushSecret.Status.ManagedSecrets[createdSecret.ID] = secretKey
}
}
} else {
// Loop over all the managed secrets, and find the corresponding existingSecret that has the same ID. If the key doesn't match, delete the secret, and re-create it with the correct key/value
for managedSecretId, managedSecretKey := range infisicalPushSecret.Status.ManagedSecrets {
existingSecret := getExistingSecretById(managedSecretId)
if existingSecret != nil {
if existingSecret.SecretKey != managedSecretKey {
// Secret key has changed, lets delete the secret and re-create it with the correct key
logger.Info(fmt.Sprintf("Secret with ID [id=%s] has changed key from [%s] to [%s]. Deleting and re-creating secret", managedSecretId, managedSecretKey, existingSecret.SecretKey))
deletedSecret, err := infisicalClient.Secrets().Delete(infisicalSdk.DeleteSecretOptions{
SecretKey: existingSecret.SecretKey,
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
})
if err != nil {
secretsFailedToReplaceById = append(secretsFailedToReplaceById, managedSecretKey)
logger.Info(fmt.Sprintf("unable to delete secret [key=%s] [err=%s]", managedSecretKey, err))
continue
}
createdSecret, err := infisicalClient.Secrets().Create(infisicalSdk.CreateSecretOptions{
SecretKey: managedSecretKey,
SecretValue: existingSecret.SecretValue,
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
})
if err != nil {
secretsFailedToReplaceById = append(secretsFailedToReplaceById, managedSecretKey)
logger.Info(fmt.Sprintf("unable to create secret [key=%s] [err=%s]", managedSecretKey, err))
continue
}
delete(infisicalPushSecret.Status.ManagedSecrets, deletedSecret.ID)
infisicalPushSecret.Status.ManagedSecrets[createdSecret.ID] = managedSecretKey
}
}
}
// We need to check if any of the secrets have been removed in the new kube secret
for _, managedSecretKey := range infisicalPushSecret.Status.ManagedSecrets {
if _, ok := processedSecrets[managedSecretKey]; !ok {
// Secret has been removed, verify that the secret is managed by the operator
if getExistingSecretByKey(managedSecretKey) != nil {
logger.Info(fmt.Sprintf("Secret with key [key=%s] has been removed from the kube secret. Deleting secret from Infisical", managedSecretKey))
deletedSecret, err := infisicalClient.Secrets().Delete(infisicalSdk.DeleteSecretOptions{
SecretKey: managedSecretKey,
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
})
if err != nil {
secretsFailedToDelete = append(secretsFailedToDelete, managedSecretKey)
logger.Info(fmt.Sprintf("unable to delete secret [key=%s] [err=%s]", managedSecretKey, err))
continue
}
delete(infisicalPushSecret.Status.ManagedSecrets, deletedSecret.ID)
}
}
}
// We need to check if any new secrets have been added in the kube secret
for currentSecretKey := range processedSecrets {
if exists := getExistingSecretByKey(currentSecretKey); exists == nil {
// Some secrets has been added, verify that the secret that has been added is not already managed by the operator
if _, ok := infisicalPushSecret.Status.ManagedSecrets[currentSecretKey]; !ok {
// Secret was not managed by the operator, lets add it
logger.Info(fmt.Sprintf("Secret with key [key=%s] has been added to the kube secret. Creating secret in Infisical", currentSecretKey))
createdSecret, err := infisicalClient.Secrets().Create(infisicalSdk.CreateSecretOptions{
SecretKey: currentSecretKey,
SecretValue: processedSecrets[currentSecretKey],
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
})
if err != nil {
secretsFailedToCreate = append(secretsFailedToCreate, currentSecretKey)
logger.Info(fmt.Sprintf("unable to create secret [key=%s] [err=%s]", currentSecretKey, err))
continue
}
infisicalPushSecret.Status.ManagedSecrets[createdSecret.ID] = currentSecretKey
}
} else {
if updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) {
existingSecret := getExistingSecretByKey(currentSecretKey)
if existingSecret != nil && existingSecret.SecretValue != processedSecrets[currentSecretKey] {
logger.Info(fmt.Sprintf("Secret with key [key=%s] has changed value. Updating secret in Infisical", currentSecretKey))
updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{
SecretKey: currentSecretKey,
NewSecretValue: processedSecrets[currentSecretKey],
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
})
if err != nil {
secretsFailedToUpdate = append(secretsFailedToUpdate, currentSecretKey)
logger.Info(fmt.Sprintf("unable to update secret [key=%s] [err=%s]", currentSecretKey, err))
continue
}
updateExistingSecretByKey(currentSecretKey, processedSecrets[currentSecretKey])
infisicalPushSecret.Status.ManagedSecrets[updatedSecret.ID] = currentSecretKey
}
}
}
}
// Check if any of the existing secrets values have changed
for secretKey, secretValue := range processedSecrets {
existingSecret := getExistingSecretByKey(secretKey)
if existingSecret != nil {
_, managedByOperator := infisicalPushSecret.Status.ManagedSecrets[existingSecret.ID]
if secretValue != existingSecret.SecretValue {
if managedByOperator || updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) {
logger.Info(fmt.Sprintf("Secret with key [key=%s] has changed value. Updating secret in Infisical", secretKey))
updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{
SecretKey: secretKey,
NewSecretValue: secretValue,
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
})
if err != nil {
secretsFailedToUpdate = append(secretsFailedToUpdate, secretKey)
logger.Info(fmt.Sprintf("unable to update secret [key=%s] [err=%s]", secretKey, err))
continue
}
infisicalPushSecret.Status.ManagedSecrets[updatedSecret.ID] = secretKey
}
}
}
}
}
var errorMessage string
if len(secretsFailedToCreate) > 0 {
errorMessage = fmt.Sprintf("Failed to create secrets: [%s]", strings.Join(secretsFailedToCreate, ", "))
} else {
errorMessage = ""
}
r.SetFailedToCreateSecretsStatusCondition(ctx, infisicalPushSecret, fmt.Sprintf("Failed to create secrets: [%s]", errorMessage))
if len(secretsFailedToUpdate) > 0 {
errorMessage = fmt.Sprintf("Failed to update secrets: [%s]", strings.Join(secretsFailedToUpdate, ", "))
} else {
errorMessage = ""
}
r.SetFailedToUpdateSecretsStatusCondition(ctx, infisicalPushSecret, fmt.Sprintf("Failed to update secrets: [%s]", errorMessage))
if len(secretsFailedToDelete) > 0 {
errorMessage = fmt.Sprintf("Failed to delete secrets: [%s]", strings.Join(secretsFailedToDelete, ", "))
} else {
errorMessage = ""
}
r.SetFailedToDeleteSecretsStatusCondition(ctx, infisicalPushSecret, errorMessage)
if len(secretsFailedToReplaceById) > 0 {
errorMessage = fmt.Sprintf("Failed to replace secrets: [%s]", strings.Join(secretsFailedToReplaceById, ", "))
} else {
errorMessage = ""
}
r.SetFailedToReplaceSecretsStatusCondition(ctx, infisicalPushSecret, errorMessage)
// Update the status of the InfisicalPushSecret
if err := r.Client.Status().Update(ctx, infisicalPushSecret); err != nil {
return fmt.Errorf("unable to update status of InfisicalPushSecret [err=%s]", err)
}
return nil
}
func (r *InfisicalPushSecretReconciler) DeleteManagedSecrets(ctx context.Context, logger logr.Logger, infisicalPushSecret *v1alpha1.InfisicalPushSecret, resourceVariablesMap map[string]util.ResourceVariables) error {
if infisicalPushSecret.Spec.DeletionPolicy != string(constants.PUSH_SECRET_DELETE_POLICY_ENABLED) {
return nil
}
resourceVariables := r.getResourceVariables(*infisicalPushSecret, resourceVariablesMap)
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 = util.HandleAuthentication(ctx, util.SecretAuthInput{
Secret: *infisicalPushSecret,
Type: util.SecretCrd.INFISICAL_PUSH_SECRET,
}, r.Client, infisicalClient, r.IsNamespaceScoped)
r.SetAuthenticatedStatusCondition(ctx, infisicalPushSecret, err)
if err != nil {
return fmt.Errorf("unable to authenticate [err=%s]", err)
}
r.updateResourceVariables(*infisicalPushSecret, util.ResourceVariables{
InfisicalClient: infisicalClient,
CancelCtx: cancelCtx,
AuthDetails: authDetails,
}, resourceVariablesMap)
}
destination := infisicalPushSecret.Spec.Destination
existingSecrets, err := resourceVariables.InfisicalClient.Secrets().List(infisicalSdk.ListSecretsOptions{
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
IncludeImports: false,
})
if err != nil {
return fmt.Errorf("unable to list secrets [err=%s]", err)
}
existingSecretsMappedById := make(map[string]infisicalSdk.Secret)
for _, secret := range existingSecrets {
existingSecretsMappedById[secret.ID] = secret
}
for managedSecretId, managedSecretKey := range infisicalPushSecret.Status.ManagedSecrets {
if _, ok := existingSecretsMappedById[managedSecretId]; ok {
logger.Info(fmt.Sprintf("Deleting secret with key [key=%s]", managedSecretKey))
_, err := infisicalClient.Secrets().Delete(infisicalSdk.DeleteSecretOptions{
SecretKey: managedSecretKey,
ProjectID: destination.ProjectID,
Environment: destination.EnvironmentSlug,
SecretPath: destination.SecretsPath,
})
if err != nil {
logger.Info(fmt.Sprintf("unable to delete secret [key=%s] [err=%s]", managedSecretKey, err))
continue
}
}
}
return nil
}
@@ -0,0 +1,100 @@
package infisicalsecret
import (
"context"
"fmt"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/internal/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, logger logr.Logger, infisicalSecret *v1alpha1.InfisicalSecret, secretsCount 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/ReadyToSyncSecrets",
Status: metav1.ConditionFalse,
Reason: "Error",
Message: fmt.Sprintf("Failed to sync secrets. This can be caused by invalid access token or an invalid API host that is set. Error: %v", errorToConditionOn),
})
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/AutoRedeployReady",
Status: metav1.ConditionFalse,
Reason: "Stopped",
Message: fmt.Sprintf("Auto redeployment has been stopped because the operator failed to sync secrets. Error: %v", errorToConditionOn),
})
} else {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/ReadyToSyncSecrets",
Status: metav1.ConditionTrue,
Reason: "OK",
Message: fmt.Sprintf("Infisical controller has started syncing your secrets. Last reconcile synced %d secrets", secretsCount),
})
}
err := r.Client.Status().Update(ctx, infisicalSecret)
if err != nil {
logger.Error(err, "Could not set condition for ReadyToSyncSecrets")
}
}
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,102 @@
package infisicalsecret
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/internal/api"
"github.com/Infisical/infisical/k8-operator/internal/util"
"github.com/go-logr/logr"
k8Errors "k8s.io/apimachinery/pkg/api/errors"
)
type InfisicalSecretHandler struct {
client.Client
Scheme *runtime.Scheme
IsNamespaceScoped bool
}
func NewInfisicalSecretHandler(client client.Client, scheme *runtime.Scheme, isNamespaceScoped bool) *InfisicalSecretHandler {
return &InfisicalSecretHandler{
Client: client,
Scheme: scheme,
IsNamespaceScoped: isNamespaceScoped,
}
}
func (h *InfisicalSecretHandler) SetupAPIConfig(infisicalSecret v1alpha1.InfisicalSecret, infisicalConfig map[string]string) error {
if infisicalSecret.Spec.HostAPI == "" {
api.API_HOST_URL = infisicalConfig["hostAPI"]
} else {
api.API_HOST_URL = util.AppendAPIEndpoint(infisicalSecret.Spec.HostAPI)
}
return nil
}
func (h *InfisicalSecretHandler) getInfisicalCaCertificateFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (caCertificate string, err error) {
caCertificateFromKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, h.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 {
if util.IsNamespaceScopedError(err, h.IsNamespaceScoped) {
return "", fmt.Errorf("unable to fetch Kubernetes CA certificate secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the CA certificate secret is in the same namespace as the operator. [err=%v]", err)
}
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 (h *InfisicalSecretHandler) HandleCACertificate(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) error {
if infisicalSecret.Spec.TLS.CaRef.SecretName != "" {
caCert, err := h.getInfisicalCaCertificateFromKubeSecret(ctx, infisicalSecret)
if err != nil {
return err
}
api.API_CA_CERTIFICATE = caCert
} else {
api.API_CA_CERTIFICATE = ""
}
return nil
}
func (h *InfisicalSecretHandler) ReconcileInfisicalSecret(ctx context.Context, logger logr.Logger, infisicalSecret *v1alpha1.InfisicalSecret, managedKubeSecretReferences []v1alpha1.ManagedKubeSecretConfig, managedKubeConfigMapReferences []v1alpha1.ManagedKubeConfigMapConfig, resourceVariablesMap map[string]util.ResourceVariables) (int, error) {
reconciler := &InfisicalSecretReconciler{
Client: h.Client,
Scheme: h.Scheme,
IsNamespaceScoped: h.IsNamespaceScoped,
}
return reconciler.ReconcileInfisicalSecret(ctx, logger, infisicalSecret, managedKubeSecretReferences, managedKubeConfigMapReferences, resourceVariablesMap)
}
func (h *InfisicalSecretHandler) SetReadyToSyncSecretsConditions(ctx context.Context, logger logr.Logger, infisicalSecret *v1alpha1.InfisicalSecret, secretsCount int, errorToConditionOn error) {
reconciler := &InfisicalSecretReconciler{
Client: h.Client,
Scheme: h.Scheme,
IsNamespaceScoped: h.IsNamespaceScoped,
}
reconciler.SetReadyToSyncSecretsConditions(ctx, logger, infisicalSecret, secretsCount, errorToConditionOn)
}
func (h *InfisicalSecretHandler) SetInfisicalAutoRedeploymentReady(ctx context.Context, logger logr.Logger, infisicalSecret *v1alpha1.InfisicalSecret, numDeployments int, errorToConditionOn error) {
reconciler := &InfisicalSecretReconciler{
Client: h.Client,
Scheme: h.Scheme,
IsNamespaceScoped: h.IsNamespaceScoped,
}
reconciler.SetInfisicalAutoRedeploymentReady(ctx, logger, infisicalSecret, numDeployments, errorToConditionOn)
}
@@ -0,0 +1,643 @@
package infisicalsecret
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"strings"
tpl "text/template"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/internal/api"
"github.com/Infisical/infisical/k8-operator/internal/constants"
"github.com/Infisical/infisical/k8-operator/internal/crypto"
"github.com/Infisical/infisical/k8-operator/internal/model"
"github.com/Infisical/infisical/k8-operator/internal/template"
"github.com/Infisical/infisical/k8-operator/internal/util"
"github.com/Infisical/infisical/k8-operator/internal/util/sse"
"github.com/go-logr/logr"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/event"
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"
)
const FINALIZER_NAME = "secrets.finalizers.infisical.com"
type InfisicalSecretReconciler struct {
client.Client
Scheme *runtime.Scheme
SourceCh chan event.TypedGenericEvent[client.Object]
IsNamespaceScoped bool
}
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) || (secretNamespace == "" && secretName == "") {
return "", nil
}
if err != nil {
if util.IsNamespaceScopedError(err, r.IsNamespaceScoped) {
return "", fmt.Errorf("unable to fetch Kubernetes CA certificate secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the CA certificate secret is in the same namespace as the operator. [err=%v]", err)
}
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
}
// 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) {
secretNamespace := infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretNamespace
secretName := infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretName
serviceAccountCredsFromKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
Namespace: secretNamespace,
Name: secretName,
})
if k8Errors.IsNotFound(err) || (secretNamespace == "" && secretName == "") {
return model.ServiceAccountDetails{}, nil
}
if err != nil {
if util.IsNamespaceScopedError(err, r.IsNamespaceScoped) {
return model.ServiceAccountDetails{}, fmt.Errorf("unable to fetch Kubernetes service account credentials secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the service account credentials secret is in the same namespace as the operator. [err=%v]", err)
}
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 convertBinaryToStringMap(binaryMap map[string][]byte) map[string]string {
stringMap := make(map[string]string)
for k, v := range binaryMap {
stringMap[k] = string(v)
}
return stringMap
}
func (r *InfisicalSecretReconciler) createInfisicalManagedKubeResource(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, managedSecretReferenceInterface interface{}, secretsFromAPI []model.SingleEnvironmentVariable, ETag string, resourceType constants.ManagedKubeResourceType) error {
plainProcessedSecrets := make(map[string][]byte)
var managedTemplateData *v1alpha1.SecretTemplate
if resourceType == constants.MANAGED_KUBE_RESOURCE_TYPE_SECRET {
managedTemplateData = managedSecretReferenceInterface.(v1alpha1.ManagedKubeSecretConfig).Template
} else if resourceType == constants.MANAGED_KUBE_RESOURCE_TYPE_CONFIG_MAP {
managedTemplateData = managedSecretReferenceInterface.(v1alpha1.ManagedKubeConfigMapConfig).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 := tpl.New("secret-templates").Funcs(template.GetTemplateFunctions()).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
}
}
if resourceType == constants.MANAGED_KUBE_RESOURCE_TYPE_SECRET {
managedSecretReference := managedSecretReferenceInterface.(v1alpha1.ManagedKubeSecretConfig)
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: managedSecretReference.SecretName,
Namespace: managedSecretReference.SecretNamespace,
Annotations: annotations,
Labels: labels,
},
Type: corev1.SecretType(managedSecretReference.SecretType),
Data: plainProcessedSecrets,
}
if 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", managedSecretReference.SecretType))
return nil
} else if resourceType == constants.MANAGED_KUBE_RESOURCE_TYPE_CONFIG_MAP {
managedSecretReference := managedSecretReferenceInterface.(v1alpha1.ManagedKubeConfigMapConfig)
// create a new config map as specified by the managed secret spec of CRD
newKubeConfigMapInstance := &corev1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: managedSecretReference.ConfigMapName,
Namespace: managedSecretReference.ConfigMapNamespace,
Annotations: annotations,
Labels: labels,
},
Data: convertBinaryToStringMap(plainProcessedSecrets),
}
if managedSecretReference.CreationPolicy == "Owner" {
// Set InfisicalSecret instance as the owner and controller of the managed config map
err := ctrl.SetControllerReference(&infisicalSecret, newKubeConfigMapInstance, r.Scheme)
if err != nil {
return err
}
}
err := r.Client.Create(ctx, newKubeConfigMapInstance)
if err != nil {
return fmt.Errorf("unable to create the managed Kubernetes config map : %w", err)
}
logger.Info(fmt.Sprintf("Successfully created a managed Kubernetes config map with your Infisical secrets. Type: %s", managedSecretReference.ConfigMapName))
return nil
}
return fmt.Errorf("invalid resource type")
}
func (r *InfisicalSecretReconciler) updateInfisicalManagedKubeSecret(ctx context.Context, logger logr.Logger, managedSecretReference v1alpha1.ManagedKubeSecretConfig, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
managedTemplateData := 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 := tpl.New("secret-templates").Funcs(template.GetTemplateFunctions()).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) updateInfisicalManagedConfigMap(ctx context.Context, logger logr.Logger, managedConfigMapReference v1alpha1.ManagedKubeConfigMapConfig, managedConfigMap corev1.ConfigMap, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error {
managedTemplateData := managedConfigMapReference.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 := tpl.New("secret-templates").Funcs(template.GetTemplateFunctions()).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 managedConfigMap.ObjectMeta.Annotations == nil {
managedConfigMap.ObjectMeta.Annotations = make(map[string]string)
}
managedConfigMap.Data = convertBinaryToStringMap(plainProcessedSecrets)
managedConfigMap.ObjectMeta.Annotations[constants.SECRET_VERSION_ANNOTATION] = ETag
err := r.Client.Update(ctx, &managedConfigMap)
if err != nil {
return fmt.Errorf("unable to update Kubernetes config map because [%w]", err)
}
logger.Info("successfully updated managed Kubernetes config map")
return nil
}
func (r *InfisicalSecretReconciler) fetchSecretsFromAPI(ctx context.Context, logger logr.Logger, authDetails util.AuthenticationDetails, infisicalClient infisicalSdk.InfisicalClientInterface, infisicalSecret v1alpha1.InfisicalSecret) ([]model.SingleEnvironmentVariable, error) {
if authDetails.AuthStrategy == util.AuthStrategy.SERVICE_ACCOUNT { // Service Account // ! Legacy auth method
serviceAccountCreds, err := r.getInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret)
if err != nil {
return nil, fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err)
}
plainTextSecretsFromApi, err := util.GetPlainTextSecretsViaServiceAccount(infisicalClient, serviceAccountCreds, infisicalSecret.Spec.Authentication.ServiceAccount.ProjectId, infisicalSecret.Spec.Authentication.ServiceAccount.EnvironmentName)
if err != nil {
return nil, fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
}
logger.Info("ReconcileInfisicalSecret: Fetched secrets via service account")
return plainTextSecretsFromApi, nil
} else if authDetails.AuthStrategy == util.AuthStrategy.SERVICE_TOKEN { // Service Tokens // ! Legacy / Deprecated auth method
infisicalToken, err := r.getInfisicalTokenFromKubeSecret(ctx, infisicalSecret)
if err != nil {
return nil, 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, err := util.GetPlainTextSecretsViaServiceToken(infisicalClient, infisicalToken, envSlug, secretsPath, recursive)
if err != nil {
return nil, fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
}
logger.Info("ReconcileInfisicalSecret: Fetched secrets via [type=SERVICE_TOKEN]")
return plainTextSecretsFromApi, nil
} else if authDetails.IsMachineIdentityAuth { // * Machine Identity authentication, the SDK will be authenticated at this point
plainTextSecretsFromApi, err := util.GetPlainTextSecretsViaMachineIdentity(infisicalClient, authDetails.MachineIdentityScope)
if err != nil {
return nil, fmt.Errorf("\nfailed to get secrets because [err=%v]", err)
}
logger.Info(fmt.Sprintf("ReconcileInfisicalSecret: Fetched secrets via machine identity [type=%v]", authDetails.AuthStrategy))
return plainTextSecretsFromApi, nil
} else {
return nil, errors.New("no authentication method provided. Please configure a authentication method then try again")
}
}
func (r *InfisicalSecretReconciler) getResourceVariables(infisicalSecret v1alpha1.InfisicalSecret, resourceVariablesMap map[string]util.ResourceVariables) 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{},
ServerSentEvents: sse.NewConnectionRegistry(ctx),
}
resourceVariables = resourceVariablesMap[string(infisicalSecret.UID)]
} else {
resourceVariables = resourceVariablesMap[string(infisicalSecret.UID)]
}
return resourceVariables
}
func (r *InfisicalSecretReconciler) updateResourceVariables(infisicalSecret v1alpha1.InfisicalSecret, resourceVariables util.ResourceVariables, resourceVariablesMap map[string]util.ResourceVariables) {
resourceVariablesMap[string(infisicalSecret.UID)] = resourceVariables
}
func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, logger logr.Logger, infisicalSecret *v1alpha1.InfisicalSecret, managedKubeSecretReferences []v1alpha1.ManagedKubeSecretConfig, managedKubeConfigMapReferences []v1alpha1.ManagedKubeConfigMapConfig, resourceVariablesMap map[string]util.ResourceVariables) (int, error) {
if infisicalSecret == nil {
return 0, fmt.Errorf("infisicalSecret is nil")
}
resourceVariables := r.getResourceVariables(*infisicalSecret, resourceVariablesMap)
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 = util.HandleAuthentication(ctx, util.SecretAuthInput{
Secret: *infisicalSecret,
Type: util.SecretCrd.INFISICAL_SECRET,
}, r.Client, infisicalClient, r.IsNamespaceScoped)
r.SetInfisicalTokenLoadCondition(ctx, logger, infisicalSecret, authDetails.AuthStrategy, err)
if err != nil {
return 0, fmt.Errorf("unable to authenticate [err=%s]", err)
}
r.updateResourceVariables(*infisicalSecret, util.ResourceVariables{
InfisicalClient: infisicalClient,
CancelCtx: cancelCtx,
AuthDetails: authDetails,
ServerSentEvents: sse.NewConnectionRegistry(ctx),
}, resourceVariablesMap)
}
plainTextSecretsFromApi, err := r.fetchSecretsFromAPI(ctx, logger, authDetails, infisicalClient, *infisicalSecret)
if err != nil {
return 0, fmt.Errorf("failed to fetch secrets from API for managed secrets [err=%s]", err)
}
secretsCount := len(plainTextSecretsFromApi)
if len(managedKubeSecretReferences) > 0 {
for _, managedSecretReference := range managedKubeSecretReferences {
// Look for managed secret by name and namespace
managedKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
Name: managedSecretReference.SecretName,
Namespace: managedSecretReference.SecretNamespace,
})
if err != nil && !k8Errors.IsNotFound(err) {
if util.IsNamespaceScopedError(err, r.IsNamespaceScoped) {
return 0, fmt.Errorf("unable to fetch Kubernetes secret. Your Operator installation is namespace scoped, and cannot read secrets outside of the namespace it is installed in. Please ensure the secret is in the same namespace as the operator. [err=%v]", err)
}
return 0, fmt.Errorf("something went wrong when fetching the managed Kubernetes secret [%w]", err)
}
newEtag := crypto.ComputeEtag([]byte(fmt.Sprintf("%v", plainTextSecretsFromApi)))
if managedKubeSecret == nil {
if err := r.createInfisicalManagedKubeResource(ctx, logger, *infisicalSecret, managedSecretReference, plainTextSecretsFromApi, newEtag, constants.MANAGED_KUBE_RESOURCE_TYPE_SECRET); err != nil {
return 0, fmt.Errorf("failed to create managed secret [err=%s]", err)
}
} else {
if err := r.updateInfisicalManagedKubeSecret(ctx, logger, managedSecretReference, *managedKubeSecret, plainTextSecretsFromApi, newEtag); err != nil {
return 0, fmt.Errorf("failed to update managed secret [err=%s]", err)
}
}
}
}
if len(managedKubeConfigMapReferences) > 0 {
for _, managedConfigMapReference := range managedKubeConfigMapReferences {
managedKubeConfigMap, err := util.GetKubeConfigMapByNamespacedName(ctx, r.Client, types.NamespacedName{
Name: managedConfigMapReference.ConfigMapName,
Namespace: managedConfigMapReference.ConfigMapNamespace,
})
if err != nil && !k8Errors.IsNotFound(err) {
if util.IsNamespaceScopedError(err, r.IsNamespaceScoped) {
return 0, fmt.Errorf("unable to fetch Kubernetes config map. Your Operator installation is namespace scoped, and cannot read config maps outside of the namespace it is installed in. Please ensure the config map is in the same namespace as the operator. [err=%v]", err)
}
return 0, fmt.Errorf("something went wrong when fetching the managed Kubernetes config map [%w]", err)
}
newEtag := crypto.ComputeEtag([]byte(fmt.Sprintf("%v", plainTextSecretsFromApi)))
if managedKubeConfigMap == nil {
if err := r.createInfisicalManagedKubeResource(ctx, logger, *infisicalSecret, managedConfigMapReference, plainTextSecretsFromApi, newEtag, constants.MANAGED_KUBE_RESOURCE_TYPE_CONFIG_MAP); err != nil {
return 0, fmt.Errorf("failed to create managed config map [err=%s]", err)
}
} else {
if err := r.updateInfisicalManagedConfigMap(ctx, logger, managedConfigMapReference, *managedKubeConfigMap, plainTextSecretsFromApi, newEtag); err != nil {
return 0, fmt.Errorf("failed to update managed config map [err=%s]", err)
}
}
}
}
return secretsCount, nil
}
func (r *InfisicalSecretReconciler) EnsureEventStream(ctx context.Context, logger logr.Logger, secret *v1alpha1.InfisicalSecret, variables util.ResourceVariables) error {
if !variables.AuthDetails.IsMachineIdentityAuth {
return fmt.Errorf("only machine identity is supported for subscriptions")
}
projectSlug := variables.AuthDetails.MachineIdentityScope.ProjectSlug
secretsPath := variables.AuthDetails.MachineIdentityScope.SecretsPath
envSlug := variables.AuthDetails.MachineIdentityScope.EnvSlug
infiscalClient := variables.InfisicalClient
conn := variables.ServerSentEvents
token := infiscalClient.Auth().GetAccessToken()
project, err := util.GetProjectBySlug(token, projectSlug)
if err != nil {
return fmt.Errorf("failed to get project [err=%s]", err)
}
if variables.AuthDetails.MachineIdentityScope.Recursive {
secretsPath = fmt.Sprint(secretsPath, "**")
}
conditions := &api.SubProjectEventsRequestCondition{
SecretPath: secretsPath,
EnvironmentSlug: envSlug,
}
body, err := json.Marshal(api.SubProjectEventsRequest{
ProjectID: project.ID,
Register: []api.SubProjectEventsRequestRegister{
{
Event: "secret:create",
Conditions: conditions,
},
{
Event: "secret:update",
Conditions: conditions,
},
{
Event: "secret:delete",
Conditions: conditions,
},
},
})
if err != nil {
return fmt.Errorf("CallSubscribeProjectEvents: unable to marshal body [err=%s]", err)
}
events, errors, err := conn.Subscribe(func() (*http.Request, error) {
headers := map[string]string{
"User-Agent": api.USER_AGENT_NAME,
"Authorization": fmt.Sprint("Bearer ", token),
}
req, err := http.NewRequestWithContext(ctx, "POST", fmt.Sprintf("%s/v1/events/subscribe/project-events", api.API_HOST_URL), strings.NewReader(string(body)))
if err != nil {
return nil, err
}
for k, v := range headers {
req.Header.Set(k, v)
}
return req, nil
})
if err != nil {
return fmt.Errorf("unable to connect to SSE server [err=%s]", err)
}
go func() {
outer:
for {
select {
case ev := <-events:
logger.Info("Received SSE Event", "event", ev)
r.SourceCh <- event.TypedGenericEvent[client.Object]{
Object: secret,
}
case err := <-errors:
logger.Error(err, "Error occurred")
break outer
case <-ctx.Done():
break outer
}
}
}()
return nil
}
func (r *InfisicalSecretReconciler) CloseEventStream(ctx context.Context, logger logr.Logger, variables util.ResourceVariables) error {
if !variables.AuthDetails.IsMachineIdentityAuth {
return fmt.Errorf("only machine identity is supported for subscriptions")
}
conn := variables.ServerSentEvents
if _, ok := conn.Get(); ok {
conn.Close()
}
return nil
}
@@ -0,0 +1,64 @@
package infisicalsecret
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())
})