Skip to content

Commit

Permalink
Merge pull request #227 from Infisical/k8-new-service-token-and-auto-…
Browse files Browse the repository at this point in the history
…redeploy

add auto redeploy, new secrets api, and new service token
  • Loading branch information
maidul98 authored Jan 16, 2023
2 parents cba57cf + 9f08b04 commit 818efe6
Show file tree
Hide file tree
Showing 17 changed files with 620 additions and 309 deletions.
2 changes: 1 addition & 1 deletion helm-charts/secrets-operator/Chart.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ type: application
# This is the chart version. This version number should be incremented each time you make changes
# to the chart and its templates, including the app version.
# Versions are expected to follow Semantic Versioning (https://semver.org/)
version: 0.1.0
version: 0.1.1
# This is the version number of the application being deployed. This version number should be
# incremented each time you make changes to the application. Versions are not expected to
# follow Semantic Versioning. They should reflect the version the application is using.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ spec:
spec:
description: InfisicalSecretSpec defines the desired state of InfisicalSecret
properties:
environment:
description: The Infisical environment such as dev, prod, testing
type: string
hostAPI:
default: https://app.infisical.com/api
description: Infisical host to pull secrets from
Expand All @@ -54,9 +51,6 @@ spec:
- secretName
- secretNamespace
type: object
projectId:
description: The Infisical project id
type: string
tokenSecretReference:
properties:
secretName:
Expand All @@ -69,9 +63,6 @@ spec:
- secretName
- secretNamespace
type: object
required:
- environment
- projectId
type: object
status:
description: InfisicalSecretStatus defines the observed state of InfisicalSecret
Expand Down
10 changes: 2 additions & 8 deletions k8-operator/api/v1alpha1/infisicalsecret_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,16 +16,10 @@ type KubeSecretReference struct {

// InfisicalSecretSpec defines the desired state of InfisicalSecret
type InfisicalSecretSpec struct {
TokenSecretReference KubeSecretReference `json:"tokenSecretReference,omitempty"`
ManagedSecretReference KubeSecretReference `json:"managedSecretReference,omitempty"`

// The Infisical project id
// +kubebuilder:validation:Required
ProjectId string `json:"projectId"`

// The Infisical environment such as dev, prod, testing
TokenSecretReference KubeSecretReference `json:"tokenSecretReference,omitempty"`
// +kubebuilder:validation:Required
Environment string `json:"environment"`
ManagedSecretReference KubeSecretReference `json:"managedSecretReference,omitempty"`

// Infisical host to pull secrets from
// +kubebuilder:default="https://app.infisical.com/api"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,6 @@ spec:
spec:
description: InfisicalSecretSpec defines the desired state of InfisicalSecret
properties:
environment:
description: The Infisical environment such as dev, prod, testing
type: string
hostAPI:
default: https://app.infisical.com/api
description: Infisical host to pull secrets from
Expand All @@ -54,9 +51,6 @@ spec:
- secretName
- secretNamespace
type: object
projectId:
description: The Infisical project id
type: string
tokenSecretReference:
properties:
secretName:
Expand All @@ -69,9 +63,6 @@ spec:
- secretName
- secretNamespace
type: object
required:
- environment
- projectId
type: object
status:
description: InfisicalSecretStatus defines the observed state of InfisicalSecret
Expand Down
26 changes: 26 additions & 0 deletions k8-operator/config/samples/deployment.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: nginx-deployment-2
labels:
app: nginx
annotations:
secrets.infisical.com/auto-reload: "true"
spec:
replicas: 3
selector:
matchLabels:
app: nginx
template:
metadata:
labels:
app: nginx
spec:
containers:
- name: nginx
image: nginx:1.14.2
envFrom:
- secretRef:
name: managed-secret
ports:
- containerPort: 80
7 changes: 3 additions & 4 deletions k8-operator/config/samples/sample.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,10 @@ kind: InfisicalSecret
metadata:
name: infisicalsecret-sample
spec:
projectId: 62faf98ae0b05e8529b5da46
environment: dev
hostAPI: https://app.infisical.com/api
tokenSecretReference:
secretName: service-token
secretNamespace: first-project
secretNamespace: default
managedSecretReference:
secretName: managed-secret
secretNamespace: first-project
secretNamespace: default
7 changes: 7 additions & 0 deletions k8-operator/config/samples/serviceTokenSecret.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
apiVersion: v1
kind: Secret
metadata:
name: service-token
type: Opaque
data:
infisicalToken: <base64 infisical token here>
121 changes: 121 additions & 0 deletions k8-operator/controllers/auto_redeployment.go
Original file line number Diff line number Diff line change
@@ -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
}
95 changes: 95 additions & 0 deletions k8-operator/controllers/conditions.go
Original file line number Diff line number Diff line change
@@ -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")
}
}
Loading

0 comments on commit 818efe6

Please sign in to comment.