mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
add auto redeploy, new secrets api, and new service token
This commit is contained in:
@@ -16,17 +16,11 @@ type KubeSecretReference struct {
|
|||||||
|
|
||||||
// InfisicalSecretSpec defines the desired state of InfisicalSecret
|
// InfisicalSecretSpec defines the desired state of InfisicalSecret
|
||||||
type InfisicalSecretSpec struct {
|
type InfisicalSecretSpec struct {
|
||||||
TokenSecretReference KubeSecretReference `json:"tokenSecretReference,omitempty"`
|
// +kubebuilder:validation:Required
|
||||||
|
TokenSecretReference KubeSecretReference `json:"tokenSecretReference,omitempty"`
|
||||||
|
// +kubebuilder:validation:Required
|
||||||
ManagedSecretReference KubeSecretReference `json:"managedSecretReference,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
|
|
||||||
// +kubebuilder:validation:Required
|
|
||||||
Environment string `json:"environment"`
|
|
||||||
|
|
||||||
// Infisical host to pull secrets from
|
// Infisical host to pull secrets from
|
||||||
// +kubebuilder:default="https://app.infisical.com/api"
|
// +kubebuilder:default="https://app.infisical.com/api"
|
||||||
HostAPI string `json:"hostAPI,omitempty"`
|
HostAPI string `json:"hostAPI,omitempty"`
|
||||||
|
|||||||
@@ -35,9 +35,6 @@ spec:
|
|||||||
spec:
|
spec:
|
||||||
description: InfisicalSecretSpec defines the desired state of InfisicalSecret
|
description: InfisicalSecretSpec defines the desired state of InfisicalSecret
|
||||||
properties:
|
properties:
|
||||||
environment:
|
|
||||||
description: The Infisical environment such as dev, prod, testing
|
|
||||||
type: string
|
|
||||||
hostAPI:
|
hostAPI:
|
||||||
default: https://app.infisical.com/api
|
default: https://app.infisical.com/api
|
||||||
description: Infisical host to pull secrets from
|
description: Infisical host to pull secrets from
|
||||||
@@ -54,9 +51,6 @@ spec:
|
|||||||
- secretName
|
- secretName
|
||||||
- secretNamespace
|
- secretNamespace
|
||||||
type: object
|
type: object
|
||||||
projectId:
|
|
||||||
description: The Infisical project id
|
|
||||||
type: string
|
|
||||||
tokenSecretReference:
|
tokenSecretReference:
|
||||||
properties:
|
properties:
|
||||||
secretName:
|
secretName:
|
||||||
@@ -69,9 +63,6 @@ spec:
|
|||||||
- secretName
|
- secretName
|
||||||
- secretNamespace
|
- secretNamespace
|
||||||
type: object
|
type: object
|
||||||
required:
|
|
||||||
- environment
|
|
||||||
- projectId
|
|
||||||
type: object
|
type: object
|
||||||
status:
|
status:
|
||||||
description: InfisicalSecretStatus defines the observed state of InfisicalSecret
|
description: InfisicalSecretStatus defines the observed state of InfisicalSecret
|
||||||
|
|||||||
26
k8-operator/config/samples/deployment.yaml
Normal file
26
k8-operator/config/samples/deployment.yaml
Normal file
@@ -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
|
||||||
@@ -3,11 +3,10 @@ kind: InfisicalSecret
|
|||||||
metadata:
|
metadata:
|
||||||
name: infisicalsecret-sample
|
name: infisicalsecret-sample
|
||||||
spec:
|
spec:
|
||||||
projectId: 62faf98ae0b05e8529b5da46
|
hostAPI: https://app.infisical.com/api
|
||||||
environment: dev
|
|
||||||
tokenSecretReference:
|
tokenSecretReference:
|
||||||
secretName: service-token
|
secretName: service-token
|
||||||
secretNamespace: first-project
|
secretNamespace: default
|
||||||
managedSecretReference:
|
managedSecretReference:
|
||||||
secretName: managed-secret
|
secretName: managed-secret
|
||||||
secretNamespace: first-project
|
secretNamespace: default
|
||||||
|
|||||||
7
k8-operator/config/samples/serviceTokenSecret.yaml
Normal file
7
k8-operator/config/samples/serviceTokenSecret.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Secret
|
||||||
|
metadata:
|
||||||
|
name: service-token
|
||||||
|
type: Opaque
|
||||||
|
data:
|
||||||
|
infisicalToken: <base64 infisical token here>
|
||||||
121
k8-operator/controllers/auto_redeployment.go
Normal file
121
k8-operator/controllers/auto_redeployment.go
Normal 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
|
||||||
|
}
|
||||||
95
k8-operator/controllers/conditions.go
Normal file
95
k8-operator/controllers/conditions.go
Normal 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")
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,16 +2,17 @@ package controllers
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
ctrl "sigs.k8s.io/controller-runtime"
|
ctrl "sigs.k8s.io/controller-runtime"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
|
||||||
|
|
||||||
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||||
secretsv1alpha1 "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
|
// InfisicalSecretReconciler reconciles a InfisicalSecret object
|
||||||
@@ -31,19 +32,18 @@ type InfisicalSecretReconciler struct {
|
|||||||
// For more details, check Reconcile and its Result here:
|
// For more details, check Reconcile and its Result here:
|
||||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.1/pkg/reconcile
|
// - 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) {
|
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
|
var infisicalSecretCR v1alpha1.InfisicalSecret
|
||||||
|
|
||||||
err := r.Get(ctx, req.NamespacedName, &infisicalSecretCR)
|
err := r.Get(ctx, req.NamespacedName, &infisicalSecretCR)
|
||||||
|
|
||||||
requeueTime := time.Minute * 5
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.IsNotFound(err) {
|
if errors.IsNotFound(err) {
|
||||||
log.Info("Infisical Secret not found")
|
fmt.Printf("Infisical Secret CRD not found [err=%v]", err)
|
||||||
return ctrl.Result{}, nil
|
return ctrl.Result{
|
||||||
|
Requeue: false,
|
||||||
|
}, nil
|
||||||
} else {
|
} 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{
|
return ctrl.Result{
|
||||||
RequeueAfter: requeueTime,
|
RequeueAfter: requeueTime,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -52,13 +52,28 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
|||||||
|
|
||||||
// Check if the resource is already marked for deletion
|
// Check if the resource is already marked for deletion
|
||||||
if infisicalSecretCR.GetDeletionTimestamp() != nil {
|
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)
|
err = r.ReconcileInfisicalSecret(ctx, infisicalSecretCR)
|
||||||
r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCR, err)
|
r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCR, err)
|
||||||
|
|
||||||
if err != nil {
|
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{
|
return ctrl.Result{
|
||||||
RequeueAfter: requeueTime,
|
RequeueAfter: requeueTime,
|
||||||
}, nil
|
}, nil
|
||||||
@@ -73,6 +88,6 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ
|
|||||||
// SetupWithManager sets up the controller with the Manager.
|
// SetupWithManager sets up the controller with the Manager.
|
||||||
func (r *InfisicalSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
func (r *InfisicalSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||||
return ctrl.NewControllerManagedBy(mgr).
|
return ctrl.NewControllerManagedBy(mgr).
|
||||||
For(&secretsv1alpha1.InfisicalSecret{}). // TODO we should also be watching secrets with the name specifed
|
For(&secretsv1alpha1.InfisicalSecret{}).
|
||||||
Complete(r)
|
Complete(r)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,16 +6,16 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||||
api "github.com/Infisical/infisical/k8-operator/packages/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/util"
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
"k8s.io/apimachinery/pkg/api/errors"
|
"k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/types"
|
"k8s.io/apimachinery/pkg/types"
|
||||||
)
|
)
|
||||||
|
|
||||||
const INFISICAL_TOKEN_SECRET_KEY_NAME = "infisicalToken"
|
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) {
|
func (r *InfisicalSecretReconciler) GetKubeSecretByNamespacedName(ctx context.Context, namespacedName types.NamespacedName) (*corev1.Secret, error) {
|
||||||
kubeSecret := &corev1.Secret{}
|
kubeSecret := &corev1.Secret{}
|
||||||
@@ -27,14 +27,14 @@ func (r *InfisicalSecretReconciler) GetKubeSecretByNamespacedName(ctx context.Co
|
|||||||
return kubeSecret, err
|
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{
|
tokenSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{
|
||||||
Namespace: infisicalSecret.Spec.TokenSecretReference.SecretNamespace,
|
Namespace: infisicalSecret.Spec.TokenSecretReference.SecretNamespace,
|
||||||
Name: infisicalSecret.Spec.TokenSecretReference.SecretName,
|
Name: infisicalSecret.Spec.TokenSecretReference.SecretName,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
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]
|
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
|
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)
|
plainProcessedSecrets := make(map[string][]byte)
|
||||||
for _, secret := range secretsFromAPI {
|
for _, secret := range secretsFromAPI {
|
||||||
plainProcessedSecrets[secret.Key] = []byte(secret.Value) // plain process
|
plainProcessedSecrets[secret.Key] = []byte(secret.Value) // plain process
|
||||||
@@ -56,6 +56,9 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
|
|||||||
ObjectMeta: metav1.ObjectMeta{
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
|
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
|
||||||
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
|
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
|
||||||
|
Annotations: map[string]string{
|
||||||
|
SECRET_VERSION_ANNOTATION: encryptedSecretsResponse.ETag,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Type: "Opaque",
|
Type: "Opaque",
|
||||||
Data: plainProcessedSecrets,
|
Data: plainProcessedSecrets,
|
||||||
@@ -70,13 +73,17 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context
|
|||||||
return nil
|
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)
|
plainProcessedSecrets := make(map[string][]byte)
|
||||||
for _, secret := range secretsFromAPI {
|
for _, secret := range secretsFromAPI {
|
||||||
plainProcessedSecrets[secret.Key] = []byte(secret.Value)
|
plainProcessedSecrets[secret.Key] = []byte(secret.Value)
|
||||||
}
|
}
|
||||||
|
|
||||||
managedKubeSecret.Data = plainProcessedSecrets
|
managedKubeSecret.Data = plainProcessedSecrets
|
||||||
|
managedKubeSecret.ObjectMeta.Annotations = map[string]string{
|
||||||
|
SECRET_VERSION_ANNOTATION: encryptedSecretsResponse.ETag,
|
||||||
|
}
|
||||||
|
|
||||||
err := r.Client.Update(ctx, &managedKubeSecret)
|
err := r.Client.Update(ctx, &managedKubeSecret)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to update Kubernetes secret because [%w]", err)
|
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 {
|
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)
|
r.SetInfisicalTokenLoadCondition(ctx, &infisicalSecret, err)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("unable to load Infisical Token from the specified Kubernetes secret with error [%w]", err)
|
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{
|
managedKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{
|
||||||
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
|
Name: infisicalSecret.Spec.ManagedSecretReference.SecretName,
|
||||||
Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace,
|
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)
|
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 {
|
if managedKubeSecret != nil {
|
||||||
return err
|
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 {
|
if managedKubeSecret == nil {
|
||||||
return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, secretsFromApi)
|
return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, plainTextSecretsFromApi, fullEncryptedSecretsResponse)
|
||||||
} else {
|
} 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,177 +1,80 @@
|
|||||||
package api
|
package api
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/Infisical/infisical/k8-operator/packages/crypto"
|
|
||||||
"github.com/Infisical/infisical/k8-operator/packages/models"
|
|
||||||
"github.com/go-resty/resty/v2"
|
"github.com/go-resty/resty/v2"
|
||||||
"golang.org/x/crypto/nacl/box"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func GetAllEnvironmentVariables(projectId string, envName string, infisicalToken string, hostAPI string) ([]models.SingleEnvironmentVariable, error) {
|
const USER_AGENT_NAME = "k8-operator"
|
||||||
envsFromApi, err := GetSecretsFromAPIUsingInfisicalToken(infisicalToken, envName, projectId, hostAPI)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return SubstituteSecrets(envsFromApi), nil
|
func CallGetEncryptedWorkspaceKey(httpClient *resty.Client, request GetEncryptedWorkspaceKeyRequest) (GetEncryptedWorkspaceKeyResponse, error) {
|
||||||
}
|
endpoint := fmt.Sprintf("%v/v2/workspace/%v/encrypted-key", API_HOST_URL, request.WorkspaceId)
|
||||||
|
var result GetEncryptedWorkspaceKeyResponse
|
||||||
func GetSecretsFromAPIUsingInfisicalToken(infisicalToken string, envName string, projectId string, hostAPI string) ([]models.SingleEnvironmentVariable, error) {
|
|
||||||
if infisicalToken == "" || projectId == "" || envName == "" {
|
|
||||||
return nil, errors.New("infisical token, project id and or environment name cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
splitToken := strings.Split(infisicalToken, ",")
|
|
||||||
JTWToken := splitToken[0]
|
|
||||||
temPrivateKey := splitToken[1]
|
|
||||||
|
|
||||||
// create http client
|
|
||||||
httpClient := resty.New().
|
|
||||||
SetAuthToken(JTWToken).
|
|
||||||
SetHeader("Accept", "application/json")
|
|
||||||
|
|
||||||
var pullSecretsByInfisicalTokenResponse models.PullSecretsByInfisicalTokenResponse
|
|
||||||
response, err := httpClient.
|
response, err := httpClient.
|
||||||
R().
|
R().
|
||||||
SetQueryParam("environment", envName).
|
SetResult(&result).
|
||||||
SetQueryParam("channel", "cli").
|
SetHeader("User-Agent", USER_AGENT_NAME).
|
||||||
SetResult(&pullSecretsByInfisicalTokenResponse).
|
Get(endpoint)
|
||||||
Get(fmt.Sprintf("%v/v1/secret/%v/service-token", hostAPI, projectId))
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unable to complete api request [err=%s]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if response.StatusCode() > 299 {
|
if response.StatusCode() > 299 {
|
||||||
return nil, fmt.Errorf(response.Status())
|
return GetEncryptedWorkspaceKeyResponse{}, fmt.Errorf("CallGetEncryptedWorkspaceKey: Unsuccessful response: [response=%s]", response)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get workspace key
|
return result, nil
|
||||||
workspaceKey, err := base64.StdEncoding.DecodeString(pullSecretsByInfisicalTokenResponse.Key.EncryptedKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
nonce, err := base64.StdEncoding.DecodeString(pullSecretsByInfisicalTokenResponse.Key.Nonce)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
senderPublicKey, err := base64.StdEncoding.DecodeString(pullSecretsByInfisicalTokenResponse.Key.Sender.PublicKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
currentUsersPrivateKey, err := base64.StdEncoding.DecodeString(temPrivateKey)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
workspaceKeyInBytes, _ := box.Open(nil, workspaceKey, (*[24]byte)(nonce), (*[32]byte)(senderPublicKey), (*[32]byte)(currentUsersPrivateKey))
|
|
||||||
var listOfEnv []models.SingleEnvironmentVariable
|
|
||||||
|
|
||||||
for _, secret := range pullSecretsByInfisicalTokenResponse.Secrets {
|
|
||||||
key_iv, _ := base64.StdEncoding.DecodeString(secret.SecretKey.Iv)
|
|
||||||
key_tag, _ := base64.StdEncoding.DecodeString(secret.SecretKey.Tag)
|
|
||||||
key_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretKey.Ciphertext)
|
|
||||||
|
|
||||||
plainTextKey, err := crypto.DecryptSymmetric(workspaceKeyInBytes, key_ciphertext, key_tag, key_iv)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
value_iv, _ := base64.StdEncoding.DecodeString(secret.SecretValue.Iv)
|
|
||||||
value_tag, _ := base64.StdEncoding.DecodeString(secret.SecretValue.Tag)
|
|
||||||
value_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretValue.Ciphertext)
|
|
||||||
|
|
||||||
plainTextValue, err := crypto.DecryptSymmetric(workspaceKeyInBytes, value_ciphertext, value_tag, value_iv)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
env := models.SingleEnvironmentVariable{
|
|
||||||
Key: string(plainTextKey),
|
|
||||||
Value: string(plainTextValue),
|
|
||||||
}
|
|
||||||
|
|
||||||
listOfEnv = append(listOfEnv, env)
|
|
||||||
}
|
|
||||||
|
|
||||||
return listOfEnv, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func getExpandedEnvVariable(secrets []models.SingleEnvironmentVariable, variableWeAreLookingFor string, hashMapOfCompleteVariables map[string]string, hashMapOfSelfRefs map[string]string) string {
|
func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDetailsResponse, error) {
|
||||||
if value, found := hashMapOfCompleteVariables[variableWeAreLookingFor]; found {
|
var tokenDetailsResponse GetServiceTokenDetailsResponse
|
||||||
return value
|
response, err := httpClient.
|
||||||
|
R().
|
||||||
|
SetResult(&tokenDetailsResponse).
|
||||||
|
SetHeader("User-Agent", USER_AGENT_NAME).
|
||||||
|
Get(fmt.Sprintf("%v/v2/service-token", API_HOST_URL))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unable to complete api request [err=%s]", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, secret := range secrets {
|
if response.IsError() {
|
||||||
if secret.Key == variableWeAreLookingFor {
|
return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unsuccessful response: [response=%s]", response)
|
||||||
regex := regexp.MustCompile(`\${([^\}]*)}`)
|
|
||||||
variablesToPopulate := regex.FindAllString(secret.Value, -1)
|
|
||||||
|
|
||||||
// case: variable is a constant so return its value
|
|
||||||
if len(variablesToPopulate) == 0 {
|
|
||||||
return secret.Value
|
|
||||||
}
|
|
||||||
|
|
||||||
valueToEdit := secret.Value
|
|
||||||
for _, variableWithSign := range variablesToPopulate {
|
|
||||||
variableWithoutSign := strings.Trim(variableWithSign, "}")
|
|
||||||
variableWithoutSign = strings.Trim(variableWithoutSign, "${")
|
|
||||||
|
|
||||||
// case: reference to self
|
|
||||||
if variableWithoutSign == secret.Key {
|
|
||||||
hashMapOfSelfRefs[variableWithoutSign] = variableWithoutSign
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
var expandedVariableValue string
|
|
||||||
|
|
||||||
if preComputedVariable, found := hashMapOfCompleteVariables[variableWithoutSign]; found {
|
|
||||||
expandedVariableValue = preComputedVariable
|
|
||||||
} else {
|
|
||||||
expandedVariableValue = getExpandedEnvVariable(secrets, variableWithoutSign, hashMapOfCompleteVariables, hashMapOfSelfRefs)
|
|
||||||
hashMapOfCompleteVariables[variableWithoutSign] = expandedVariableValue
|
|
||||||
}
|
|
||||||
|
|
||||||
// If after expanding all the vars above, is the current var a self ref? if so no replacement needed for it
|
|
||||||
if _, found := hashMapOfSelfRefs[variableWithoutSign]; found {
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
valueToEdit = strings.ReplaceAll(valueToEdit, variableWithSign, expandedVariableValue)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return valueToEdit
|
|
||||||
|
|
||||||
} else {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return "${" + variableWeAreLookingFor + "}"
|
return tokenDetailsResponse, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func SubstituteSecrets(secrets []models.SingleEnvironmentVariable) []models.SingleEnvironmentVariable {
|
func CallGetSecretsV2(httpClient *resty.Client, request GetEncryptedSecretsV2Request) (GetEncryptedSecretsV2Response, error) {
|
||||||
hashMapOfCompleteVariables := make(map[string]string)
|
var secretsResponse GetEncryptedSecretsV2Response = GetEncryptedSecretsV2Response{}
|
||||||
hashMapOfSelfRefs := make(map[string]string)
|
createHttpRequest := httpClient.
|
||||||
expandedSecrets := []models.SingleEnvironmentVariable{}
|
R().
|
||||||
|
SetResult(&secretsResponse.Secrets).
|
||||||
for _, secret := range secrets {
|
SetQueryParam("environment", request.EnvironmentName).
|
||||||
expandedVariable := getExpandedEnvVariable(secrets, secret.Key, hashMapOfCompleteVariables, hashMapOfSelfRefs)
|
SetHeader("User-Agent", USER_AGENT_NAME)
|
||||||
expandedSecrets = append(expandedSecrets, models.SingleEnvironmentVariable{
|
|
||||||
Key: secret.Key,
|
|
||||||
Value: expandedVariable,
|
|
||||||
})
|
|
||||||
|
|
||||||
|
if request.ETag != "" {
|
||||||
|
createHttpRequest.SetHeader("If-None-Match", request.ETag)
|
||||||
}
|
}
|
||||||
|
|
||||||
return expandedSecrets
|
response, err := createHttpRequest.Get(fmt.Sprintf("%v/v2/secret/workspace/%v", API_HOST_URL, request.WorkspaceId))
|
||||||
|
if err != nil {
|
||||||
|
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unable to complete api request [err=%s]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.IsError() {
|
||||||
|
return GetEncryptedSecretsV2Response{}, fmt.Errorf("CallGetSecretsV2: Unsuccessful response: [response=%s]", response)
|
||||||
|
}
|
||||||
|
|
||||||
|
if response.StatusCode() == 304 {
|
||||||
|
secretsResponse.Modified = false
|
||||||
|
} else {
|
||||||
|
secretsResponse.Modified = true
|
||||||
|
}
|
||||||
|
|
||||||
|
secretsResponse.ETag = response.Header().Get("etag")
|
||||||
|
|
||||||
|
return secretsResponse, nil
|
||||||
}
|
}
|
||||||
|
|||||||
75
k8-operator/packages/api/models.go
Normal file
75
k8-operator/packages/api/models.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type GetEncryptedWorkspaceKeyRequest struct {
|
||||||
|
WorkspaceId string `json:"workspaceId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetEncryptedWorkspaceKeyResponse struct {
|
||||||
|
ID string `json:"_id"`
|
||||||
|
EncryptedKey string `json:"encryptedKey"`
|
||||||
|
Nonce string `json:"nonce"`
|
||||||
|
Sender struct {
|
||||||
|
ID string `json:"_id"`
|
||||||
|
Email string `json:"email"`
|
||||||
|
RefreshVersion int `json:"refreshVersion"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
V int `json:"__v"`
|
||||||
|
FirstName string `json:"firstName"`
|
||||||
|
LastName string `json:"lastName"`
|
||||||
|
PublicKey string `json:"publicKey"`
|
||||||
|
} `json:"sender"`
|
||||||
|
Receiver string `json:"receiver"`
|
||||||
|
Workspace string `json:"workspace"`
|
||||||
|
V int `json:"__v"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetEncryptedSecretsV2Request struct {
|
||||||
|
EnvironmentName string `json:"environmentName"`
|
||||||
|
WorkspaceId string `json:"workspaceId"`
|
||||||
|
ETag string `json:"etag,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetEncryptedSecretsV2Response struct {
|
||||||
|
Secrets []struct {
|
||||||
|
ID string `json:"_id"`
|
||||||
|
Version int `json:"version"`
|
||||||
|
Workspace string `json:"workspace"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Environment string `json:"environment"`
|
||||||
|
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
|
||||||
|
SecretKeyIV string `json:"secretKeyIV"`
|
||||||
|
SecretKeyTag string `json:"secretKeyTag"`
|
||||||
|
SecretKeyHash string `json:"secretKeyHash"`
|
||||||
|
SecretValueCiphertext string `json:"secretValueCiphertext"`
|
||||||
|
SecretValueIV string `json:"secretValueIV"`
|
||||||
|
SecretValueTag string `json:"secretValueTag"`
|
||||||
|
SecretValueHash string `json:"secretValueHash"`
|
||||||
|
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
|
||||||
|
SecretCommentIV string `json:"secretCommentIV"`
|
||||||
|
SecretCommentTag string `json:"secretCommentTag"`
|
||||||
|
SecretCommentHash string `json:"secretCommentHash"`
|
||||||
|
V int `json:"__v"`
|
||||||
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
|
UpdatedAt time.Time `json:"updatedAt"`
|
||||||
|
User string `json:"user,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
Modified bool `json:"modified,omitempty"`
|
||||||
|
ETag string `json:"ETag,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetServiceTokenDetailsResponse struct {
|
||||||
|
ID string `json:"_id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Workspace string `json:"workspace"`
|
||||||
|
Environment string `json:"environment"`
|
||||||
|
User string `json:"user"`
|
||||||
|
EncryptedKey string `json:"encryptedKey"`
|
||||||
|
Iv string `json:"iv"`
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
}
|
||||||
3
k8-operator/packages/api/variables.go
Normal file
3
k8-operator/packages/api/variables.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
var API_HOST_URL string = "https://app.infisical.com/api"
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
package models
|
|
||||||
|
|
||||||
import "time"
|
|
||||||
|
|
||||||
type PullSecretsByInfisicalTokenResponse struct {
|
|
||||||
Secrets []struct {
|
|
||||||
ID string `json:"_id"`
|
|
||||||
Workspace string `json:"workspace"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Environment string `json:"environment"`
|
|
||||||
SecretKey struct {
|
|
||||||
Workspace string `json:"workspace"`
|
|
||||||
Ciphertext string `json:"ciphertext"`
|
|
||||||
Iv string `json:"iv"`
|
|
||||||
Tag string `json:"tag"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
} `json:"secretKey"`
|
|
||||||
SecretValue struct {
|
|
||||||
Workspace string `json:"workspace"`
|
|
||||||
Ciphertext string `json:"ciphertext"`
|
|
||||||
Iv string `json:"iv"`
|
|
||||||
Tag string `json:"tag"`
|
|
||||||
Hash string `json:"hash"`
|
|
||||||
} `json:"secretValue"`
|
|
||||||
} `json:"secrets"`
|
|
||||||
Key struct {
|
|
||||||
EncryptedKey string `json:"encryptedKey"`
|
|
||||||
Nonce string `json:"nonce"`
|
|
||||||
Sender struct {
|
|
||||||
PublicKey string `json:"publicKey"`
|
|
||||||
} `json:"sender"`
|
|
||||||
Receiver struct {
|
|
||||||
RefreshVersion int `json:"refreshVersion"`
|
|
||||||
ID string `json:"_id"`
|
|
||||||
Email string `json:"email"`
|
|
||||||
CustomerID string `json:"customerId"`
|
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
|
||||||
UpdatedAt time.Time `json:"updatedAt"`
|
|
||||||
V int `json:"__v"`
|
|
||||||
FirstName string `json:"firstName"`
|
|
||||||
LastName string `json:"lastName"`
|
|
||||||
PublicKey string `json:"publicKey"`
|
|
||||||
} `json:"receiver"`
|
|
||||||
Workspace string `json:"workspace"`
|
|
||||||
} `json:"key"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SingleEnvironmentVariable struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
187
k8-operator/packages/util/secrets.go
Normal file
187
k8-operator/packages/util/secrets.go
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/Infisical/infisical/k8-operator/packages/api"
|
||||||
|
"github.com/Infisical/infisical/k8-operator/packages/crypto"
|
||||||
|
"github.com/go-resty/resty/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SingleEnvironmentVariable struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Value string `json:"value"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
ID string `json:"_id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DecodedSymmetricEncryptionDetails = struct {
|
||||||
|
Cipher []byte
|
||||||
|
IV []byte
|
||||||
|
Tag []byte
|
||||||
|
Key []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func VerifyServiceToken(serviceToken string) (string, error) {
|
||||||
|
serviceTokenParts := strings.SplitN(serviceToken, ".", 4)
|
||||||
|
if len(serviceTokenParts) < 4 {
|
||||||
|
return "", fmt.Errorf("invalid service token entered. Please double check your service token and try again")
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceToken = fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
|
||||||
|
return serviceToken, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetServiceTokenDetails(infisicalToken string) (api.GetServiceTokenDetailsResponse, error) {
|
||||||
|
serviceTokenParts := strings.SplitN(infisicalToken, ".", 4)
|
||||||
|
if len(serviceTokenParts) < 4 {
|
||||||
|
return api.GetServiceTokenDetailsResponse{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
|
||||||
|
|
||||||
|
httpClient := resty.New()
|
||||||
|
httpClient.SetAuthToken(serviceToken).
|
||||||
|
SetHeader("Accept", "application/json")
|
||||||
|
|
||||||
|
serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
|
||||||
|
if err != nil {
|
||||||
|
return api.GetServiceTokenDetailsResponse{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return serviceTokenDetails, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPlainTextSecretsViaServiceToken(fullServiceToken string, etag string) ([]SingleEnvironmentVariable, api.GetEncryptedSecretsV2Response, error) {
|
||||||
|
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
|
||||||
|
if len(serviceTokenParts) < 4 {
|
||||||
|
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("invalid service token entered. Please double check your service token and try again")
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceToken := fmt.Sprintf("%v.%v.%v", serviceTokenParts[0], serviceTokenParts[1], serviceTokenParts[2])
|
||||||
|
|
||||||
|
httpClient := resty.New()
|
||||||
|
httpClient.SetAuthToken(serviceToken).
|
||||||
|
SetHeader("Accept", "application/json")
|
||||||
|
|
||||||
|
serviceTokenDetails, err := api.CallGetServiceTokenDetailsV2(httpClient)
|
||||||
|
if err != nil {
|
||||||
|
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to get service token details. [err=%v]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
encryptedSecretsResponse, err := api.CallGetSecretsV2(httpClient, api.GetEncryptedSecretsV2Request{
|
||||||
|
WorkspaceId: serviceTokenDetails.Workspace,
|
||||||
|
EnvironmentName: serviceTokenDetails.Environment,
|
||||||
|
ETag: etag,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, api.GetEncryptedSecretsV2Response{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
decodedSymmetricEncryptionDetails, err := GetBase64DecodedSymmetricEncryptionDetails(serviceTokenParts[3], serviceTokenDetails.EncryptedKey, serviceTokenDetails.Iv, serviceTokenDetails.Tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to decode symmetric encryption details [err=%v]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
plainTextWorkspaceKey, err := crypto.DecryptSymmetric([]byte(serviceTokenParts[3]), decodedSymmetricEncryptionDetails.Cipher, decodedSymmetricEncryptionDetails.Tag, decodedSymmetricEncryptionDetails.IV)
|
||||||
|
if err != nil {
|
||||||
|
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to decrypt the required workspace key")
|
||||||
|
}
|
||||||
|
|
||||||
|
plainTextSecrets, err := GetPlainTextSecrets(plainTextWorkspaceKey, encryptedSecretsResponse)
|
||||||
|
if err != nil {
|
||||||
|
return nil, api.GetEncryptedSecretsV2Response{}, fmt.Errorf("unable to decrypt your secrets [err=%v]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plainTextSecrets, encryptedSecretsResponse, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetBase64DecodedSymmetricEncryptionDetails(key string, cipher string, IV string, tag string) (DecodedSymmetricEncryptionDetails, error) {
|
||||||
|
cipherx, err := base64.StdEncoding.DecodeString(cipher)
|
||||||
|
if err != nil {
|
||||||
|
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode cipher text [err=%v]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
keyx, err := base64.StdEncoding.DecodeString(key)
|
||||||
|
if err != nil {
|
||||||
|
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode key [err=%v]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
IVx, err := base64.StdEncoding.DecodeString(IV)
|
||||||
|
if err != nil {
|
||||||
|
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode IV [err=%v]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tagx, err := base64.StdEncoding.DecodeString(tag)
|
||||||
|
if err != nil {
|
||||||
|
return DecodedSymmetricEncryptionDetails{}, fmt.Errorf("Base64DecodeSymmetricEncryptionDetails: Unable to decode tag [err=%v]", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return DecodedSymmetricEncryptionDetails{
|
||||||
|
Key: keyx,
|
||||||
|
Cipher: cipherx,
|
||||||
|
IV: IVx,
|
||||||
|
Tag: tagx,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetPlainTextSecrets(key []byte, encryptedSecretsResponse api.GetEncryptedSecretsV2Response) ([]SingleEnvironmentVariable, error) {
|
||||||
|
plainTextSecrets := []SingleEnvironmentVariable{}
|
||||||
|
for _, secret := range encryptedSecretsResponse.Secrets {
|
||||||
|
// Decrypt key
|
||||||
|
key_iv, err := base64.StdEncoding.DecodeString(secret.SecretKeyIV)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to decode secret IV for secret key")
|
||||||
|
}
|
||||||
|
|
||||||
|
key_tag, err := base64.StdEncoding.DecodeString(secret.SecretKeyTag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to decode secret authentication tag for secret key")
|
||||||
|
}
|
||||||
|
|
||||||
|
key_ciphertext, err := base64.StdEncoding.DecodeString(secret.SecretKeyCiphertext)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to decode secret cipher text for secret key")
|
||||||
|
}
|
||||||
|
|
||||||
|
plainTextKey, err := crypto.DecryptSymmetric(key, key_ciphertext, key_tag, key_iv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to symmetrically decrypt secret key")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt value
|
||||||
|
value_iv, err := base64.StdEncoding.DecodeString(secret.SecretValueIV)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to decode secret IV for secret value")
|
||||||
|
}
|
||||||
|
|
||||||
|
value_tag, err := base64.StdEncoding.DecodeString(secret.SecretValueTag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to decode secret authentication tag for secret value")
|
||||||
|
}
|
||||||
|
|
||||||
|
value_ciphertext, _ := base64.StdEncoding.DecodeString(secret.SecretValueCiphertext)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to decode secret cipher text for secret key")
|
||||||
|
}
|
||||||
|
|
||||||
|
plainTextValue, err := crypto.DecryptSymmetric(key, value_ciphertext, value_tag, value_iv)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to symmetrically decrypt secret value")
|
||||||
|
}
|
||||||
|
|
||||||
|
plainTextSecret := SingleEnvironmentVariable{
|
||||||
|
Key: string(plainTextKey),
|
||||||
|
Value: string(plainTextValue),
|
||||||
|
Type: string(secret.Type),
|
||||||
|
ID: secret.ID,
|
||||||
|
}
|
||||||
|
|
||||||
|
plainTextSecrets = append(plainTextSecrets, plainTextSecret)
|
||||||
|
}
|
||||||
|
|
||||||
|
return plainTextSecrets, nil
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user