mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-10-03 15:26:06 +00:00
Merge branch 'main' of github.com:Infisical/infisical into sid/k8s-operator
This commit is contained in:
@@ -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())
|
||||
})
|
||||
Reference in New Issue
Block a user