add auto redeploy, new secrets api, and new service token

This commit is contained in:
Maidul Islam
2023-01-15 16:47:09 -08:00
parent e552be0a81
commit 63f22c554a
14 changed files with 630 additions and 301 deletions

View File

@@ -0,0 +1,121 @@
package controllers
import (
"context"
"fmt"
"sync"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
v1 "k8s.io/api/apps/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
)
const DEPLOYMENT_SECRET_NAME_ANNOTATION_PREFIX = "secrets.infisical.com/managed-secret"
const AUTO_RELOAD_DEPLOYMENT_ANNOTATION = "secrets.infisical.com/auto-reload" // needs to be set to true for a deployment to start auto redeploying
func (r *InfisicalSecretReconciler) ReconcileDeploymentsWithManagedSecrets(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (int, error) {
listOfDeployments := &v1.DeploymentList{}
err := r.Client.List(ctx, listOfDeployments, &client.ListOptions{Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace})
if err != nil {
return 0, fmt.Errorf("unable to get deployments in the [namespace=%v] [err=%v]", infisicalSecret.Spec.ManagedSecretReference.SecretNamespace, err)
}
managedKubeSecretNameAndNamespace := types.NamespacedName{
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
}
managedKubeSecret := &corev1.Secret{}
err = r.Client.Get(ctx, managedKubeSecretNameAndNamespace, managedKubeSecret)
if err != nil {
return 0, fmt.Errorf("unable to fetch Kubernetes secret to update deployment: %v", err)
}
// Create a channel to receive errors from goroutines
errChan := make(chan error, len(listOfDeployments.Items))
wg := sync.WaitGroup{}
wg.Add(len(listOfDeployments.Items))
go func() {
wg.Wait()
close(errChan)
}()
// Iterate over the deployments and check if they use the managed secret
for _, deployment := range listOfDeployments.Items {
if deployment.Annotations[AUTO_RELOAD_DEPLOYMENT_ANNOTATION] == "true" && r.IsDeploymentUsingManagedSecret(deployment, infisicalSecret) {
// Start a goroutine to reconcile the deployment
go func(d v1.Deployment, s corev1.Secret) {
defer wg.Done()
if err := r.ReconcileDeployment(ctx, d, s); err != nil {
errChan <- err
}
}(deployment, *managedKubeSecret)
}
}
// Collect any errors that were sent through the channel
var errs []error
for err := range errChan {
errs = append(errs, err)
}
if len(errs) > 0 {
return 0, fmt.Errorf("unable to reconcile some deployments: %v", errs)
}
return len(listOfDeployments.Items), nil
}
// Check if the deployment uses managed secrets
func (r *InfisicalSecretReconciler) IsDeploymentUsingManagedSecret(deployment v1.Deployment, infisicalSecret v1alpha1.InfisicalSecret) bool {
managedSecretName := infisicalSecret.Spec.ManagedSecretReference.SecretName
for _, container := range deployment.Spec.Template.Spec.Containers {
for _, envFrom := range container.EnvFrom {
if envFrom.SecretRef != nil && envFrom.SecretRef.LocalObjectReference.Name == managedSecretName {
return true
}
}
for _, env := range container.Env {
if env.ValueFrom != nil && env.ValueFrom.SecretKeyRef != nil && env.ValueFrom.SecretKeyRef.LocalObjectReference.Name == managedSecretName {
return true
}
}
}
for _, volume := range deployment.Spec.Template.Spec.Volumes {
if volume.Secret != nil && volume.Secret.SecretName == managedSecretName {
return true
}
}
return false
}
// This function ensures that a deployment is in sync with a Kubernetes secret by comparing their versions.
// If the version of the secret is different from the version annotation on the deployment, the annotation is updated to trigger a restart of the deployment.
func (r *InfisicalSecretReconciler) ReconcileDeployment(ctx context.Context, deployment v1.Deployment, secret corev1.Secret) error {
annotationKey := fmt.Sprintf("%s.%s", DEPLOYMENT_SECRET_NAME_ANNOTATION_PREFIX, secret.Name)
annotationValue := secret.Annotations[SECRET_VERSION_ANNOTATION]
if deployment.Annotations[annotationKey] == annotationValue &&
deployment.Spec.Template.Annotations[annotationKey] == annotationValue {
fmt.Printf("The [deploymentName=%v] is already using the most up to date managed secrets. No action required.\n", deployment.ObjectMeta.Name)
return nil
}
fmt.Printf("deployment is using outdated managed secret. Starting re-deployment [deploymentName=%v]\n", deployment.ObjectMeta.Name)
if deployment.Spec.Template.Annotations == nil {
deployment.Spec.Template.Annotations = make(map[string]string)
}
deployment.Annotations[annotationKey] = annotationValue
deployment.Spec.Template.Annotations[annotationKey] = annotationValue
if err := r.Client.Update(ctx, &deployment); err != nil {
return fmt.Errorf("failed to update deployment annotation: %v", err)
}
return nil
}

View File

@@ -0,0 +1,95 @@
package controllers
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 *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, errorToConditionOn error) error {
if infisicalSecret.Status.Conditions == nil {
infisicalSecret.Status.Conditions = []metav1.Condition{}
}
if errorToConditionOn != nil {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/ReadyToSyncSecrets",
Status: metav1.ConditionFalse,
Reason: "Error",
Message: "Failed to sync secrets. This can be caused by invalid service token or an invalid API host that is set. Check operator logs for more info",
})
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/AutoRedeployReady",
Status: metav1.ConditionFalse,
Reason: "Stopped",
Message: "Auto redeployment has been stopped because the operator failed to sync secrets",
})
} else {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/ReadyToSyncSecrets",
Status: metav1.ConditionTrue,
Reason: "OK",
Message: "Infisical controller has started syncing your secrets",
})
}
return r.Client.Status().Update(ctx, infisicalSecret)
}
func (r *InfisicalSecretReconciler) SetInfisicalTokenLoadCondition(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, 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: "Infisical controller has located the Infisical token in provided Kubernetes secret",
})
} 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 {
fmt.Println("Could not set condition for LoadedInfisicalToken")
}
}
func (r *InfisicalSecretReconciler) SetInfisicalAutoRedeploymentReady(ctx context.Context, 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 {
fmt.Println("Could not set condition for AutoRedeployReady")
}
}

View File

@@ -2,16 +2,17 @@ package controllers
import (
"context"
"fmt"
"time"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/Infisical/infisical/k8-operator/packages/api"
)
// InfisicalSecretReconciler reconciles a InfisicalSecret object
@@ -31,19 +32,18 @@ type InfisicalSecretReconciler struct {
// For more details, check Reconcile and its Result here:
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.1/pkg/reconcile
func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
log := log.FromContext(ctx)
requeueTime := time.Minute * 1
var infisicalSecretCR v1alpha1.InfisicalSecret
err := r.Get(ctx, req.NamespacedName, &infisicalSecretCR)
requeueTime := time.Minute * 5
if err != nil {
if errors.IsNotFound(err) {
log.Info("Infisical Secret not found")
return ctrl.Result{}, nil
fmt.Printf("Infisical Secret CRD not found [err=%v]", err)
return ctrl.Result{
Requeue: false,
}, nil
} else {
log.Error(err, "Unable to fetch Infisical Secret from cluster. Will retry")
fmt.Printf("Unable to fetch Infisical Secret CRD from cluster because [err=%v]", err)
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
@@ -52,13 +52,28 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
// Check if the resource is already marked for deletion
if infisicalSecretCR.GetDeletionTimestamp() != nil {
return ctrl.Result{}, nil
return ctrl.Result{
Requeue: false,
}, nil
}
// set the api url based on the CRD
api.API_HOST_URL = infisicalSecretCR.Spec.HostAPI
err = r.ReconcileInfisicalSecret(ctx, infisicalSecretCR)
r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCR, err)
if err != nil {
log.Error(err, "Unable to reconcile Infisical Secret and will try again")
fmt.Printf("unable to reconcile Infisical Secret because [err=%v]. Will requeue after [requeueTime=%v]\n", err, requeueTime)
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
}
numDeployments, err := r.ReconcileDeploymentsWithManagedSecrets(ctx, infisicalSecretCR)
r.SetInfisicalAutoRedeploymentReady(ctx, &infisicalSecretCR, numDeployments, err)
if err != nil {
fmt.Printf("unable to reconcile auto redeployment because [err=%v]", err)
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
@@ -73,6 +88,6 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
// SetupWithManager sets up the controller with the Manager.
func (r *InfisicalSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
return ctrl.NewControllerManagedBy(mgr).
For(&secretsv1alpha1.InfisicalSecret{}). // TODO we should also be watching secrets with the name specifed
For(&secretsv1alpha1.InfisicalSecret{}).
Complete(r)
}

View File

@@ -6,16 +6,16 @@ import (
"strings"
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
api "github.com/Infisical/infisical/k8-operator/packages/api"
models "github.com/Infisical/infisical/k8-operator/packages/models"
"github.com/Infisical/infisical/k8-operator/packages/api"
"github.com/Infisical/infisical/k8-operator/packages/util"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
const INFISICAL_TOKEN_SECRET_KEY_NAME = "infisicalToken"
const SECRET_VERSION_ANNOTATION = "secrets.infisical.com/version" // used to set the version of secrets via Etag
func (r *InfisicalSecretReconciler) GetKubeSecretByNamespacedName(ctx context.Context, namespacedName types.NamespacedName) (*corev1.Secret, error) {
kubeSecret := &corev1.Secret{}
@@ -27,14 +27,14 @@ func (r *InfisicalSecretReconciler) GetKubeSecretByNamespacedName(ctx context.Co
return kubeSecret, err
}
func (r *InfisicalSecretReconciler) GetInfisicalToken(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (string, error) {
func (r *InfisicalSecretReconciler) GetInfisicalTokenFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (string, error) {
tokenSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{
Namespace: infisicalSecret.Spec.TokenSecretReference.SecretNamespace,
Name: infisicalSecret.Spec.TokenSecretReference.SecretName,
})
if err != nil {
return "", fmt.Errorf("failed to read Infisical token secret from secret named [%s] in namespace [%s]: with error [%w]", infisicalSecret.Spec.ManagedSecretReference.SecretName, infisicalSecret.Spec.ManagedSecretReference.SecretNamespace, 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[INFISICAL_TOKEN_SECRET_KEY_NAME]
@@ -45,7 +45,7 @@ func (r *InfisicalSecretReconciler) GetInfisicalToken(ctx context.Context, infis
return strings.Replace(string(infisicalServiceToken), " ", "", -1), nil
}
func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []models.SingleEnvironmentVariable) error {
func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []util.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) error {
plainProcessedSecrets := make(map[string][]byte)
for _, secret := range secretsFromAPI {
plainProcessedSecrets[secret.Key] = []byte(secret.Value) // plain process
@@ -56,6 +56,9 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
ObjectMeta: metav1.ObjectMeta{
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
Annotations: map[string]string{
SECRET_VERSION_ANNOTATION: encryptedSecretsResponse.ETag,
},
},
Type: "Opaque",
Data: plainProcessedSecrets,
@@ -70,13 +73,17 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
return nil
}
func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []models.SingleEnvironmentVariable) error {
func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, managedKubeSecret corev1.Secret, secretsFromAPI []util.SingleEnvironmentVariable, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) error {
plainProcessedSecrets := make(map[string][]byte)
for _, secret := range secretsFromAPI {
plainProcessedSecrets[secret.Key] = []byte(secret.Value)
}
managedKubeSecret.Data = plainProcessedSecrets
managedKubeSecret.ObjectMeta.Annotations = map[string]string{
SECRET_VERSION_ANNOTATION: encryptedSecretsResponse.ETag,
}
err := r.Client.Update(ctx, &managedKubeSecret)
if err != nil {
return fmt.Errorf("unable to update Kubernetes secret because [%w]", err)
@@ -87,12 +94,13 @@ func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context
}
func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) error {
infisicalToken, err := r.GetInfisicalToken(ctx, infisicalSecret)
infisicalToken, err := r.GetInfisicalTokenFromKubeSecret(ctx, infisicalSecret)
r.SetInfisicalTokenLoadCondition(ctx, &infisicalSecret, err)
if err != nil {
return fmt.Errorf("unable to load Infisical Token from the specified Kubernetes secret with error [%w]", err)
}
// Look for managed secret by name and namespace
managedKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
@@ -102,72 +110,28 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context
return fmt.Errorf("something went wrong when fetching the managed Kubernetes secret [%w]", err)
}
secretsFromApi, err := api.GetAllEnvironmentVariables(infisicalSecret.Spec.ProjectId, infisicalSecret.Spec.Environment, infisicalToken, infisicalSecret.Spec.HostAPI)
secretVersionBasedOnETag := ""
if err != nil {
return err
if managedKubeSecret != nil {
secretVersionBasedOnETag = managedKubeSecret.Annotations[SECRET_VERSION_ANNOTATION]
}
plainTextSecretsFromApi, fullEncryptedSecretsResponse, err := util.GetPlainTextSecretsViaServiceToken(infisicalToken, secretVersionBasedOnETag)
if err != nil {
return fmt.Errorf("failed to get secrets because [err=%v]\n", err)
}
if !fullEncryptedSecretsResponse.Modified {
fmt.Println("No secrets modified so reconcile not needed", "Etag:", fullEncryptedSecretsResponse.ETag, "Modified:", fullEncryptedSecretsResponse.Modified)
return nil
}
fmt.Println("secret is modified so it needs to be created or updated")
if managedKubeSecret == nil {
return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, secretsFromApi)
return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, plainTextSecretsFromApi, fullEncryptedSecretsResponse)
} else {
return r.UpdateInfisicalManagedKubeSecret(ctx, *managedKubeSecret, secretsFromApi)
return r.UpdateInfisicalManagedKubeSecret(ctx, *managedKubeSecret, plainTextSecretsFromApi, fullEncryptedSecretsResponse)
}
}
// Conditions
func (r *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, 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.ConditionTrue,
Reason: "OK",
Message: "Infisical controller has started syncing your secrets",
})
} else {
meta.SetStatusCondition(&infisicalSecret.Status.Conditions, metav1.Condition{
Type: "secrets.infisical.com/ReadyToSyncSecrets",
Status: metav1.ConditionFalse,
Reason: "Error",
Message: fmt.Sprintf("Failed to update secret because: %v", errorToConditionOn),
})
}
err := r.Client.Status().Update(ctx, infisicalSecret)
if err != nil {
fmt.Println("Could not set condition", err)
}
}
func (r *InfisicalSecretReconciler) SetInfisicalTokenLoadCondition(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, 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: "Infisical controller has located the Infisical token in provided Kubernetes secret",
})
} 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 because: %v", errorToConditionOn),
})
}
err := r.Client.Status().Update(ctx, infisicalSecret)
if err != nil {
fmt.Println("Could not set condition for LoadedInfisicalToken")
}
}