mirror of
https://github.com/awatertrevi/infisical.git
synced 2026-09-22 13:39:35 +00:00
Merge branch 'main' of github.com:Infisical/infisical into sid/k8s-operator
This commit is contained in:
169
k8-operator/internal/api/api.go
Normal file
169
k8-operator/internal/api/api.go
Normal file
@@ -0,0 +1,169 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/packages/model"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
const USER_AGENT_NAME = "k8-operator"
|
||||
|
||||
func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDetailsResponse, error) {
|
||||
var tokenDetailsResponse GetServiceTokenDetailsResponse
|
||||
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)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unsuccessful response: [response=%s]", response)
|
||||
}
|
||||
|
||||
return tokenDetailsResponse, nil
|
||||
}
|
||||
|
||||
func CallGetServiceTokenAccountDetailsV2(httpClient *resty.Client) (ServiceAccountDetailsResponse, error) {
|
||||
var serviceAccountDetailsResponse ServiceAccountDetailsResponse
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&serviceAccountDetailsResponse).
|
||||
SetHeader("User-Agent", USER_AGENT_NAME).
|
||||
Get(fmt.Sprintf("%v/v2/service-accounts/me", API_HOST_URL))
|
||||
|
||||
if err != nil {
|
||||
return ServiceAccountDetailsResponse{}, fmt.Errorf("CallGetServiceTokenAccountDetailsV2: Unable to complete api request [err=%s]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return ServiceAccountDetailsResponse{}, fmt.Errorf("CallGetServiceTokenAccountDetailsV2: Unsuccessful response: [response=%s]", response)
|
||||
}
|
||||
|
||||
return serviceAccountDetailsResponse, nil
|
||||
}
|
||||
|
||||
func CallUniversalMachineIdentityLogin(request MachineIdentityUniversalAuthLoginRequest) (MachineIdentityDetailsResponse, error) {
|
||||
var machineIdentityDetailsResponse MachineIdentityDetailsResponse
|
||||
|
||||
response, err := resty.New().
|
||||
R().
|
||||
SetResult(&machineIdentityDetailsResponse).
|
||||
SetBody(request).
|
||||
SetHeader("User-Agent", USER_AGENT_NAME).
|
||||
Post(fmt.Sprintf("%v/v1/auth/universal-auth/login", API_HOST_URL))
|
||||
|
||||
if err != nil {
|
||||
return MachineIdentityDetailsResponse{}, fmt.Errorf("CallUniversalMachineIdentityLogin: Unable to complete api request [err=%s]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return MachineIdentityDetailsResponse{}, fmt.Errorf("CallUniversalMachineIdentityLogin: Unsuccessful response: [response=%s]", response)
|
||||
}
|
||||
|
||||
return machineIdentityDetailsResponse, nil
|
||||
}
|
||||
|
||||
func CallUniversalMachineIdentityRefreshAccessToken(request MachineIdentityUniversalAuthRefreshRequest) (MachineIdentityDetailsResponse, error) {
|
||||
var universalAuthRefreshResponse MachineIdentityDetailsResponse
|
||||
|
||||
response, err := resty.New().
|
||||
R().
|
||||
SetResult(&universalAuthRefreshResponse).
|
||||
SetHeader("User-Agent", USER_AGENT_NAME).
|
||||
SetBody(request).
|
||||
Post(fmt.Sprintf("%v/v1/auth/token/renew", API_HOST_URL))
|
||||
|
||||
if err != nil {
|
||||
return MachineIdentityDetailsResponse{}, fmt.Errorf("CallUniversalAuthRefreshAccessToken: Unable to complete api request [err=%s]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return MachineIdentityDetailsResponse{}, fmt.Errorf("CallUniversalAuthRefreshAccessToken: Unsuccessful response [%v %v] [status-code=%v] [response=%v]", response.Request.Method, response.Request.URL, response.StatusCode(), response.String())
|
||||
}
|
||||
|
||||
return universalAuthRefreshResponse, nil
|
||||
}
|
||||
|
||||
func CallGetServiceAccountWorkspacePermissionsV2(httpClient *resty.Client) (ServiceAccountWorkspacePermissions, error) {
|
||||
var serviceAccountWorkspacePermissionsResponse ServiceAccountWorkspacePermissions
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&serviceAccountWorkspacePermissionsResponse).
|
||||
SetHeader("User-Agent", USER_AGENT_NAME).
|
||||
Get(fmt.Sprintf("%v/v2/service-accounts/<service-account-id>/permissions/workspace", API_HOST_URL))
|
||||
|
||||
if err != nil {
|
||||
return ServiceAccountWorkspacePermissions{}, fmt.Errorf("CallGetServiceAccountWorkspacePermissionsV2: Unable to complete api request [err=%s]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return ServiceAccountWorkspacePermissions{}, fmt.Errorf("CallGetServiceAccountWorkspacePermissionsV2: Unsuccessful response: [response=%s]", response)
|
||||
}
|
||||
|
||||
return serviceAccountWorkspacePermissionsResponse, nil
|
||||
}
|
||||
|
||||
func CallGetServiceAccountKeysV2(httpClient *resty.Client, request GetServiceAccountKeysRequest) (GetServiceAccountKeysResponse, error) {
|
||||
var serviceAccountKeysResponse GetServiceAccountKeysResponse
|
||||
response, err := httpClient.
|
||||
R().
|
||||
SetResult(&serviceAccountKeysResponse).
|
||||
SetHeader("User-Agent", USER_AGENT_NAME).
|
||||
Get(fmt.Sprintf("%v/v2/service-accounts/%v/keys", API_HOST_URL, request.ServiceAccountId))
|
||||
|
||||
if err != nil {
|
||||
return GetServiceAccountKeysResponse{}, fmt.Errorf("CallGetServiceAccountKeysV2: Unable to complete api request [err=%s]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return GetServiceAccountKeysResponse{}, fmt.Errorf("CallGetServiceAccountKeysV2: Unsuccessful response: [response=%s]", response)
|
||||
}
|
||||
|
||||
return serviceAccountKeysResponse, nil
|
||||
}
|
||||
|
||||
func CallGetProjectByID(httpClient *resty.Client, request GetProjectByIDRequest) (GetProjectByIDResponse, error) {
|
||||
|
||||
var projectResponse GetProjectByIDResponse
|
||||
|
||||
response, err := httpClient.
|
||||
R().SetResult(&projectResponse).
|
||||
SetHeader("User-Agent", USER_AGENT_NAME).
|
||||
Get(fmt.Sprintf("%s/v1/workspace/%s", API_HOST_URL, request.ProjectID))
|
||||
|
||||
if err != nil {
|
||||
return GetProjectByIDResponse{}, fmt.Errorf("CallGetProject: Unable to complete api request [err=%s]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return GetProjectByIDResponse{}, fmt.Errorf("CallGetProject: Unsuccessful response: [response=%s]", response)
|
||||
}
|
||||
|
||||
return projectResponse, nil
|
||||
|
||||
}
|
||||
|
||||
func CallGetProjectByIDv2(httpClient *resty.Client, request GetProjectByIDRequest) (model.Project, error) {
|
||||
var projectResponse model.Project
|
||||
|
||||
response, err := httpClient.
|
||||
R().SetResult(&projectResponse).
|
||||
SetHeader("User-Agent", USER_AGENT_NAME).
|
||||
Get(fmt.Sprintf("%s/v2/workspace/%s", API_HOST_URL, request.ProjectID))
|
||||
|
||||
if err != nil {
|
||||
return model.Project{}, fmt.Errorf("CallGetProject: Unable to complete api request [err=%s]", err)
|
||||
}
|
||||
|
||||
if response.IsError() {
|
||||
return model.Project{}, fmt.Errorf("CallGetProject: Unsuccessful response: [response=%s]", response)
|
||||
}
|
||||
|
||||
return projectResponse, nil
|
||||
|
||||
}
|
||||
225
k8-operator/internal/api/models.go
Normal file
225
k8-operator/internal/api/models.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/internal/model"
|
||||
)
|
||||
|
||||
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 GetEncryptedSecretsV3Request struct {
|
||||
Environment string `json:"environment"`
|
||||
WorkspaceId string `json:"workspaceId"`
|
||||
Recursive bool `json:"recursive"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
IncludeImport bool `json:"include_imports"`
|
||||
ETag string `json:"etag,omitempty"`
|
||||
}
|
||||
|
||||
type EncryptedSecretV3 struct {
|
||||
ID string `json:"_id"`
|
||||
Version int `json:"version"`
|
||||
Workspace string `json:"workspace"`
|
||||
Type string `json:"type"`
|
||||
Tags []struct {
|
||||
ID string `json:"_id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
Workspace string `json:"workspace"`
|
||||
} `json:"tags"`
|
||||
Environment string `json:"environment"`
|
||||
SecretKeyCiphertext string `json:"secretKeyCiphertext"`
|
||||
SecretKeyIV string `json:"secretKeyIV"`
|
||||
SecretKeyTag string `json:"secretKeyTag"`
|
||||
SecretValueCiphertext string `json:"secretValueCiphertext"`
|
||||
SecretValueIV string `json:"secretValueIV"`
|
||||
SecretValueTag string `json:"secretValueTag"`
|
||||
SecretCommentCiphertext string `json:"secretCommentCiphertext"`
|
||||
SecretCommentIV string `json:"secretCommentIV"`
|
||||
SecretCommentTag string `json:"secretCommentTag"`
|
||||
Algorithm string `json:"algorithm"`
|
||||
KeyEncoding string `json:"keyEncoding"`
|
||||
Folder string `json:"folder"`
|
||||
V int `json:"__v"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type DecryptedSecretV3 struct {
|
||||
ID string `json:"id"`
|
||||
Workspace string `json:"workspace"`
|
||||
Environment string `json:"environment"`
|
||||
Version int `json:"version"`
|
||||
Type string `json:"string"`
|
||||
SecretKey string `json:"secretKey"`
|
||||
SecretValue string `json:"secretValue"`
|
||||
SecretComment string `json:"secretComment"`
|
||||
}
|
||||
|
||||
type ImportedSecretV3 struct {
|
||||
Environment string `json:"environment"`
|
||||
FolderId string `json:"folderId"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
Secrets []EncryptedSecretV3 `json:"secrets"`
|
||||
}
|
||||
|
||||
type ImportedRawSecretV3 struct {
|
||||
Environment string `json:"environment"`
|
||||
FolderId string `json:"folderId"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
Secrets []DecryptedSecretV3 `json:"secrets"`
|
||||
}
|
||||
|
||||
type GetEncryptedSecretsV3Response struct {
|
||||
Secrets []EncryptedSecretV3 `json:"secrets"`
|
||||
ImportedSecrets []ImportedSecretV3 `json:"imports,omitempty"`
|
||||
Modified bool `json:"modified,omitempty"`
|
||||
ETag string `json:"ETag,omitempty"`
|
||||
}
|
||||
|
||||
type GetDecryptedSecretsV3Response struct {
|
||||
Secrets []DecryptedSecretV3 `json:"secrets"`
|
||||
ETag string `json:"ETag,omitempty"`
|
||||
Modified bool `json:"modified,omitempty"`
|
||||
Imports []ImportedRawSecretV3 `json:"imports,omitempty"`
|
||||
}
|
||||
|
||||
type GetDecryptedSecretsV3Request struct {
|
||||
ProjectID string `json:"workspaceId"`
|
||||
ProjectSlug string `json:"workspaceSlug"`
|
||||
Environment string `json:"environment"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
Recursive bool `json:"recursive"`
|
||||
ExpandSecretReferences bool `json:"expandSecretReferences"`
|
||||
ETag string `json:"etag,omitempty"`
|
||||
}
|
||||
|
||||
type GetServiceTokenDetailsResponse struct {
|
||||
ID string `json:"_id"`
|
||||
Name string `json:"name"`
|
||||
Workspace string `json:"workspace"`
|
||||
Environment string `json:"environment"`
|
||||
EncryptedKey string `json:"encryptedKey"`
|
||||
Iv string `json:"iv"`
|
||||
Tag string `json:"tag"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
}
|
||||
|
||||
type ServiceAccountDetailsResponse struct {
|
||||
ServiceAccount struct {
|
||||
ID string `json:"_id"`
|
||||
Name string `json:"name"`
|
||||
Organization string `json:"organization"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
LastUsed time.Time `json:"lastUsed"`
|
||||
ExpiresAt time.Time `json:"expiresAt"`
|
||||
} `json:"serviceAccount"`
|
||||
}
|
||||
|
||||
type MachineIdentityDetailsResponse struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
ExpiresIn int `json:"expiresIn"`
|
||||
AccessTokenMaxTTL int `json:"accessTokenMaxTTL"`
|
||||
TokenType string `json:"tokenType"`
|
||||
}
|
||||
|
||||
type ServiceAccountWorkspacePermission struct {
|
||||
ID string `json:"_id"`
|
||||
ServiceAccount string `json:"serviceAccount"`
|
||||
Workspace struct {
|
||||
ID string `json:"_id"`
|
||||
Name string `json:"name"`
|
||||
AutoCapitalization bool `json:"autoCapitalization"`
|
||||
Organization string `json:"organization"`
|
||||
Environments []struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
ID string `json:"_id"`
|
||||
} `json:"environments"`
|
||||
} `json:"workspace"`
|
||||
Environment string `json:"environment"`
|
||||
Read bool `json:"read"`
|
||||
Write bool `json:"write"`
|
||||
}
|
||||
|
||||
type ServiceAccountWorkspacePermissions struct {
|
||||
ServiceAccountWorkspacePermission []ServiceAccountWorkspacePermissions `json:"serviceAccountWorkspacePermissions"`
|
||||
}
|
||||
|
||||
type GetServiceAccountKeysRequest struct {
|
||||
ServiceAccountId string `json:"id"`
|
||||
}
|
||||
|
||||
type MachineIdentityUniversalAuthLoginRequest struct {
|
||||
ClientId string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
}
|
||||
|
||||
type MachineIdentityUniversalAuthRefreshRequest struct {
|
||||
AccessToken string `json:"accessToken"`
|
||||
}
|
||||
|
||||
type ServiceAccountKey struct {
|
||||
ID string `json:"_id"`
|
||||
EncryptedKey string `json:"encryptedKey"`
|
||||
Nonce string `json:"nonce"`
|
||||
Sender string `json:"sender"`
|
||||
ServiceAccount string `json:"serviceAccount"`
|
||||
Workspace string `json:"workspace"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type GetServiceAccountKeysResponse struct {
|
||||
ServiceAccountKeys []ServiceAccountKey `json:"serviceAccountKeys"`
|
||||
}
|
||||
|
||||
type GetProjectByIDRequest struct {
|
||||
ProjectID string
|
||||
}
|
||||
|
||||
type GetProjectByIDResponse struct {
|
||||
Project model.Project `json:"workspace"`
|
||||
}
|
||||
|
||||
type SubProjectEventsRequestRegister struct {
|
||||
Event string `json:"event"`
|
||||
Conditions *SubProjectEventsRequestCondition `json:"conditions"`
|
||||
}
|
||||
|
||||
type SubProjectEventsRequestCondition struct {
|
||||
EnvironmentSlug string `json:"environmentSlug"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
}
|
||||
|
||||
type SubProjectEventsRequest struct {
|
||||
ProjectID string `json:"projectId"`
|
||||
Register []SubProjectEventsRequestRegister `json:"register"`
|
||||
}
|
||||
|
||||
type SubProjectEventsResponse struct{}
|
||||
4
k8-operator/internal/api/variables.go
Normal file
4
k8-operator/internal/api/variables.go
Normal file
@@ -0,0 +1,4 @@
|
||||
package api
|
||||
|
||||
var API_HOST_URL string = "https://app.infisical.com/api"
|
||||
var API_CA_CERTIFICATE string = ""
|
||||
42
k8-operator/internal/constants/constants.go
Normal file
42
k8-operator/internal/constants/constants.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package constants
|
||||
|
||||
import "errors"
|
||||
|
||||
const SERVICE_ACCOUNT_ACCESS_KEY = "serviceAccountAccessKey"
|
||||
const SERVICE_ACCOUNT_PUBLIC_KEY = "serviceAccountPublicKey"
|
||||
const SERVICE_ACCOUNT_PRIVATE_KEY = "serviceAccountPrivateKey"
|
||||
|
||||
const INFISICAL_MACHINE_IDENTITY_CLIENT_ID = "clientId"
|
||||
const INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET = "clientSecret"
|
||||
|
||||
const INFISICAL_TOKEN_SECRET_KEY_NAME = "infisicalToken"
|
||||
const SECRET_VERSION_ANNOTATION = "secrets.infisical.com/version" // used to set the version of secrets via Etag
|
||||
const OPERATOR_SETTINGS_CONFIGMAP_NAME = "infisical-config"
|
||||
const OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE = "infisical-operator-system"
|
||||
const INFISICAL_DOMAIN = "https://app.infisical.com/api"
|
||||
|
||||
const INFISICAL_PUSH_SECRET_FINALIZER_NAME = "pushsecret.secrets.infisical.com/finalizer"
|
||||
const INFISICAL_DYNAMIC_SECRET_FINALIZER_NAME = "dynamicsecret.secrets.infisical.com/finalizer"
|
||||
|
||||
type PushSecretReplacePolicy string
|
||||
type PushSecretDeletionPolicy string
|
||||
|
||||
const (
|
||||
PUSH_SECRET_REPLACE_POLICY_ENABLED PushSecretReplacePolicy = "Replace"
|
||||
PUSH_SECRET_DELETE_POLICY_ENABLED PushSecretDeletionPolicy = "Delete"
|
||||
)
|
||||
|
||||
type ManagedKubeResourceType string
|
||||
|
||||
const (
|
||||
MANAGED_KUBE_RESOURCE_TYPE_SECRET ManagedKubeResourceType = "Secret"
|
||||
MANAGED_KUBE_RESOURCE_TYPE_CONFIG_MAP ManagedKubeResourceType = "ConfigMap"
|
||||
)
|
||||
|
||||
type DynamicSecretLeaseRevocationPolicy string
|
||||
|
||||
const (
|
||||
DYNAMIC_SECRET_LEASE_REVOCATION_POLICY_ENABLED DynamicSecretLeaseRevocationPolicy = "Revoke"
|
||||
)
|
||||
|
||||
var ErrInvalidLease = errors.New("invalid dynamic secret lease")
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
Copyright 2022.
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -18,46 +18,221 @@ package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
infisicaldynamicsecret "github.com/Infisical/infisical/k8-operator/internal/services/infisicaldynamicsecret"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/log"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
|
||||
secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/constants"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/controllerhelpers"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/util"
|
||||
"github.com/go-logr/logr"
|
||||
)
|
||||
|
||||
// InfisicalDynamicSecretReconciler reconciles a InfisicalDynamicSecret object
|
||||
type InfisicalDynamicSecretReconciler struct {
|
||||
client.Client
|
||||
Scheme *runtime.Scheme
|
||||
BaseLogger logr.Logger
|
||||
Scheme *runtime.Scheme
|
||||
Random *rand.Rand
|
||||
Namespace string
|
||||
IsNamespaceScoped bool
|
||||
}
|
||||
|
||||
var infisicalDynamicSecretsResourceVariablesMap map[string]util.ResourceVariables = make(map[string]util.ResourceVariables)
|
||||
|
||||
func (r *InfisicalDynamicSecretReconciler) GetLogger(req ctrl.Request) logr.Logger {
|
||||
return r.BaseLogger.WithValues("infisicaldynamicsecret", req.NamespacedName)
|
||||
}
|
||||
|
||||
// +kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicaldynamicsecrets,verbs=get;list;watch;create;update;patch;delete
|
||||
// +kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicaldynamicsecrets/status,verbs=get;update;patch
|
||||
// +kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicaldynamicsecrets/finalizers,verbs=update
|
||||
// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;delete
|
||||
// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;delete
|
||||
// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;get;update
|
||||
// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch
|
||||
//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list
|
||||
//+kubebuilder:rbac:groups="authentication.k8s.io",resources=tokenreviews,verbs=create
|
||||
//+kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||
// move the current state of the cluster closer to the desired state.
|
||||
// TODO(user): Modify the Reconcile function to compare the state specified by
|
||||
// the InfisicalDynamicSecret object against the actual cluster state, and then
|
||||
// perform operations to make the cluster state reflect the state specified by
|
||||
// the user.
|
||||
//
|
||||
// For more details, check Reconcile and its Result here:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.19.1/pkg/reconcile
|
||||
func (r *InfisicalDynamicSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
_ = log.FromContext(ctx)
|
||||
|
||||
// TODO(user): your logic here
|
||||
logger := r.GetLogger(req)
|
||||
|
||||
return ctrl.Result{}, nil
|
||||
var infisicalDynamicSecretCRD secretsv1alpha1.InfisicalDynamicSecret
|
||||
requeueTime := time.Second * 5
|
||||
|
||||
err := r.Get(ctx, req.NamespacedName, &infisicalDynamicSecretCRD)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
logger.Info("Infisical Dynamic Secret CRD not found")
|
||||
return ctrl.Result{
|
||||
Requeue: false,
|
||||
}, nil
|
||||
} else {
|
||||
logger.Error(err, "Unable to fetch Infisical Dynamic Secret CRD from cluster")
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Add finalizer if it doesn't exist
|
||||
if !controllerutil.ContainsFinalizer(&infisicalDynamicSecretCRD, constants.INFISICAL_DYNAMIC_SECRET_FINALIZER_NAME) {
|
||||
controllerutil.AddFinalizer(&infisicalDynamicSecretCRD, constants.INFISICAL_DYNAMIC_SECRET_FINALIZER_NAME)
|
||||
if err := r.Update(ctx, &infisicalDynamicSecretCRD); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's being deleted
|
||||
if !infisicalDynamicSecretCRD.DeletionTimestamp.IsZero() {
|
||||
logger.Info("Handling deletion of InfisicalDynamicSecret")
|
||||
if controllerutil.ContainsFinalizer(&infisicalDynamicSecretCRD, constants.INFISICAL_DYNAMIC_SECRET_FINALIZER_NAME) {
|
||||
// We remove finalizers before running deletion logic to be completely safe from stuck resources
|
||||
infisicalDynamicSecretCRD.ObjectMeta.Finalizers = []string{}
|
||||
if err := r.Update(ctx, &infisicalDynamicSecretCRD); err != nil {
|
||||
logger.Error(err, fmt.Sprintf("Error removing finalizers from InfisicalDynamicSecret %s", infisicalDynamicSecretCRD.Name))
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Initialize the business logic handler
|
||||
handler := infisicaldynamicsecret.NewInfisicalDynamicSecretHandler(r.Client, r.Scheme, r.IsNamespaceScoped)
|
||||
|
||||
err := handler.HandleLeaseRevocation(ctx, logger, &infisicalDynamicSecretCRD, infisicalDynamicSecretsResourceVariablesMap)
|
||||
|
||||
if infisicalDynamicSecretsResourceVariablesMap != nil {
|
||||
if rv, ok := infisicalDynamicSecretsResourceVariablesMap[string(infisicalDynamicSecretCRD.GetUID())]; ok {
|
||||
rv.CancelCtx()
|
||||
delete(infisicalDynamicSecretsResourceVariablesMap, string(infisicalDynamicSecretCRD.GetUID()))
|
||||
}
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return ctrl.Result{}, err // Even if this fails, we still want to delete the CRD
|
||||
}
|
||||
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
// Get modified/default config
|
||||
infisicalConfig, err := controllerhelpers.GetInfisicalConfigMap(ctx, r.Client, r.IsNamespaceScoped)
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to fetch infisical-config. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Initialize the business logic handler
|
||||
handler := infisicaldynamicsecret.NewInfisicalDynamicSecretHandler(r.Client, r.Scheme, r.IsNamespaceScoped)
|
||||
|
||||
// Setup API configuration through business logic
|
||||
err = handler.SetupAPIConfig(infisicalDynamicSecretCRD, infisicalConfig)
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to setup API configuration. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Handle CA certificate through business logic
|
||||
err = handler.HandleCACertificate(ctx, infisicalDynamicSecretCRD)
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to handle CA certificate. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
nextReconcile, err := handler.ReconcileInfisicalDynamicSecret(ctx, logger, &infisicalDynamicSecretCRD, infisicalDynamicSecretsResourceVariablesMap)
|
||||
handler.SetReconcileConditionStatus(ctx, logger, &infisicalDynamicSecretCRD, err)
|
||||
|
||||
if err == nil && nextReconcile.Seconds() >= 5 {
|
||||
requeueTime = nextReconcile
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to reconcile Infisical Dynamic Secret. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
numDeployments, err := controllerhelpers.ReconcileDeploymentsWithManagedSecrets(ctx, r.Client, logger, infisicalDynamicSecretCRD.Spec.ManagedSecretReference, r.IsNamespaceScoped)
|
||||
handler.SetReconcileAutoRedeploymentConditionStatus(ctx, logger, &infisicalDynamicSecretCRD, numDeployments, err)
|
||||
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to reconcile auto redeployment. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Sync again after the specified time
|
||||
logger.Info(fmt.Sprintf("Next reconciliation in [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// SetupWithManager sets up the controller with the Manager.
|
||||
func (r *InfisicalDynamicSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
|
||||
// Custom predicate that allows both spec changes and deletions
|
||||
specChangeOrDelete := predicate.Funcs{
|
||||
UpdateFunc: func(e event.UpdateEvent) bool {
|
||||
// Only reconcile if spec/generation changed
|
||||
|
||||
isSpecOrGenerationChange := e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration()
|
||||
|
||||
if isSpecOrGenerationChange {
|
||||
if infisicalDynamicSecretsResourceVariablesMap != nil {
|
||||
if rv, ok := infisicalDynamicSecretsResourceVariablesMap[string(e.ObjectNew.GetUID())]; ok {
|
||||
rv.CancelCtx()
|
||||
delete(infisicalDynamicSecretsResourceVariablesMap, string(e.ObjectNew.GetUID()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isSpecOrGenerationChange
|
||||
},
|
||||
DeleteFunc: func(e event.DeleteEvent) bool {
|
||||
// Always reconcile on deletion
|
||||
|
||||
if infisicalDynamicSecretsResourceVariablesMap != nil {
|
||||
if rv, ok := infisicalDynamicSecretsResourceVariablesMap[string(e.Object.GetUID())]; ok {
|
||||
rv.CancelCtx()
|
||||
delete(infisicalDynamicSecretsResourceVariablesMap, string(e.Object.GetUID()))
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
CreateFunc: func(e event.CreateEvent) bool {
|
||||
// Reconcile on creation
|
||||
return true
|
||||
},
|
||||
GenericFunc: func(e event.GenericEvent) bool {
|
||||
// Ignore generic events
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
For(&secretsv1alpha1.InfisicalDynamicSecret{}).
|
||||
Named("infisicaldynamicsecret").
|
||||
For(&secretsv1alpha1.InfisicalDynamicSecret{}, builder.WithPredicates(
|
||||
specChangeOrDelete,
|
||||
)).
|
||||
Complete(r)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
)
|
||||
|
||||
var _ = Describe("InfisicalDynamicSecret Controller", func() {
|
||||
Context("When reconciling a resource", func() {
|
||||
const resourceName = "test-resource"
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
typeNamespacedName := types.NamespacedName{
|
||||
Name: resourceName,
|
||||
Namespace: "default", // TODO(user):Modify as needed
|
||||
}
|
||||
infisicaldynamicsecret := &secretsv1alpha1.InfisicalDynamicSecret{}
|
||||
|
||||
BeforeEach(func() {
|
||||
By("creating the custom resource for the Kind InfisicalDynamicSecret")
|
||||
err := k8sClient.Get(ctx, typeNamespacedName, infisicaldynamicsecret)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
resource := &secretsv1alpha1.InfisicalDynamicSecret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: resourceName,
|
||||
Namespace: "default",
|
||||
},
|
||||
// TODO(user): Specify other spec details if needed.
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
||||
}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
// TODO(user): Cleanup logic after each test, like removing the resource instance.
|
||||
resource := &secretsv1alpha1.InfisicalDynamicSecret{}
|
||||
err := k8sClient.Get(ctx, typeNamespacedName, resource)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Cleanup the specific resource instance InfisicalDynamicSecret")
|
||||
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
|
||||
})
|
||||
It("should successfully reconcile the resource", func() {
|
||||
By("Reconciling the created resource")
|
||||
controllerReconciler := &InfisicalDynamicSecretReconciler{
|
||||
Client: k8sClient,
|
||||
Scheme: k8sClient.Scheme(),
|
||||
}
|
||||
|
||||
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
|
||||
NamespacedName: typeNamespacedName,
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
|
||||
// Example: If you expect a certain status condition after reconciliation, verify it here.
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,349 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
infisicalpushsecret "github.com/Infisical/infisical/k8-operator/internal/services/infisicalpushsecret"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/handler"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/constants"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/controllerhelpers"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/util"
|
||||
"github.com/go-logr/logr"
|
||||
)
|
||||
|
||||
// InfisicalPushSecretReconciler reconciles a InfisicalPushSecretSecret object
|
||||
type InfisicalPushSecretReconciler struct {
|
||||
client.Client
|
||||
BaseLogger logr.Logger
|
||||
Scheme *runtime.Scheme
|
||||
IsNamespaceScoped bool
|
||||
Namespace string
|
||||
}
|
||||
|
||||
var infisicalPushSecretResourceVariablesMap map[string]util.ResourceVariables = make(map[string]util.ResourceVariables)
|
||||
|
||||
func (r *InfisicalPushSecretReconciler) GetLogger(req ctrl.Request) logr.Logger {
|
||||
return r.BaseLogger.WithValues("infisicalpushsecret", req.NamespacedName)
|
||||
}
|
||||
|
||||
//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalpushsecrets,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalpushsecrets/status,verbs=get;update;patch
|
||||
//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalpushsecrets/finalizers,verbs=update
|
||||
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;delete
|
||||
//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;delete
|
||||
//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;get;update
|
||||
//+kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch
|
||||
//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list
|
||||
//+kubebuilder:rbac:groups="authentication.k8s.io",resources=tokenreviews,verbs=create
|
||||
//+kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create
|
||||
//+kubebuilder:rbac:groups=secrets.infisical.com,resources=clustergenerators,verbs=get;list;watch;create;update;patch;delete
|
||||
|
||||
func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
|
||||
logger := r.GetLogger(req)
|
||||
|
||||
var infisicalPushSecretCRD secretsv1alpha1.InfisicalPushSecret
|
||||
requeueTime := time.Minute // seconds
|
||||
|
||||
err := r.Get(ctx, req.NamespacedName, &infisicalPushSecretCRD)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
logger.Info("Infisical Push Secret CRD not found")
|
||||
// Initialize the business logic handler
|
||||
handler := infisicalpushsecret.NewInfisicalPushSecretHandler(r.Client, r.Scheme, r.IsNamespaceScoped)
|
||||
handler.DeleteManagedSecrets(ctx, logger, &infisicalPushSecretCRD, infisicalPushSecretResourceVariablesMap)
|
||||
|
||||
return ctrl.Result{
|
||||
Requeue: false,
|
||||
}, nil
|
||||
} else {
|
||||
logger.Error(err, "Unable to fetch Infisical Secret CRD from cluster")
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Add finalizer if it doesn't exist
|
||||
if !controllerutil.ContainsFinalizer(&infisicalPushSecretCRD, constants.INFISICAL_PUSH_SECRET_FINALIZER_NAME) {
|
||||
controllerutil.AddFinalizer(&infisicalPushSecretCRD, constants.INFISICAL_PUSH_SECRET_FINALIZER_NAME)
|
||||
if err := r.Update(ctx, &infisicalPushSecretCRD); err != nil {
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Check if it's being deleted
|
||||
if !infisicalPushSecretCRD.DeletionTimestamp.IsZero() {
|
||||
logger.Info("Handling deletion of InfisicalPushSecret")
|
||||
if controllerutil.ContainsFinalizer(&infisicalPushSecretCRD, constants.INFISICAL_PUSH_SECRET_FINALIZER_NAME) {
|
||||
// We remove finalizers before running deletion logic to be completely safe from stuck resources
|
||||
infisicalPushSecretCRD.ObjectMeta.Finalizers = []string{}
|
||||
if err := r.Update(ctx, &infisicalPushSecretCRD); err != nil {
|
||||
logger.Error(err, fmt.Sprintf("Error removing finalizers from InfisicalPushSecret %s", infisicalPushSecretCRD.Name))
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
|
||||
// Initialize the business logic handler
|
||||
handler := infisicalpushsecret.NewInfisicalPushSecretHandler(r.Client, r.Scheme, r.IsNamespaceScoped)
|
||||
|
||||
if err := handler.DeleteManagedSecrets(ctx, logger, &infisicalPushSecretCRD, infisicalPushSecretResourceVariablesMap); err != nil {
|
||||
return ctrl.Result{}, err // Even if this fails, we still want to delete the CRD
|
||||
}
|
||||
|
||||
}
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if infisicalPushSecretCRD.Spec.Push.Secret == nil && infisicalPushSecretCRD.Spec.Push.Generators == nil {
|
||||
logger.Info("No secret or generators found, skipping reconciliation. Please define ")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
duration, err := util.ConvertIntervalToDuration(infisicalPushSecretCRD.Spec.ResyncInterval)
|
||||
|
||||
if err != nil {
|
||||
// if resyncInterval is nil, we don't want to reconcile automatically
|
||||
if infisicalPushSecretCRD.Spec.ResyncInterval != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to convert resync interval to duration. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
} else {
|
||||
logger.Error(err, "unable to convert resync interval to duration")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
requeueTime = duration
|
||||
|
||||
if requeueTime != 0 {
|
||||
logger.Info(fmt.Sprintf("Manual re-sync interval set. Interval: %v", requeueTime))
|
||||
}
|
||||
|
||||
// Check if the resource is already marked for deletion
|
||||
if infisicalPushSecretCRD.GetDeletionTimestamp() != nil {
|
||||
return ctrl.Result{
|
||||
Requeue: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get modified/default config
|
||||
infisicalConfig, err := controllerhelpers.GetInfisicalConfigMap(ctx, r.Client, r.IsNamespaceScoped)
|
||||
if err != nil {
|
||||
if requeueTime != 0 {
|
||||
logger.Error(err, fmt.Sprintf("unable to fetch infisical-config. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
} else {
|
||||
logger.Error(err, "unable to fetch infisical-config")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize the business logic handler
|
||||
handler := infisicalpushsecret.NewInfisicalPushSecretHandler(r.Client, r.Scheme, r.IsNamespaceScoped)
|
||||
|
||||
// Setup API configuration through business logic
|
||||
err = handler.SetupAPIConfig(infisicalPushSecretCRD, infisicalConfig)
|
||||
if err != nil {
|
||||
if requeueTime != 0 {
|
||||
logger.Error(err, fmt.Sprintf("unable to setup API configuration. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
} else {
|
||||
logger.Error(err, "unable to setup API configuration")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Handle CA certificate through business logic
|
||||
err = handler.HandleCACertificate(ctx, infisicalPushSecretCRD)
|
||||
if err != nil {
|
||||
if requeueTime != 0 {
|
||||
logger.Error(err, fmt.Sprintf("unable to fetch CA certificate. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
} else {
|
||||
logger.Error(err, "unable to fetch CA certificate")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
err = handler.ReconcileInfisicalPushSecret(ctx, logger, &infisicalPushSecretCRD, infisicalPushSecretResourceVariablesMap)
|
||||
handler.SetReconcileStatusCondition(ctx, &infisicalPushSecretCRD, err)
|
||||
|
||||
if err != nil {
|
||||
if requeueTime != 0 {
|
||||
logger.Error(err, fmt.Sprintf("unable to reconcile Infisical Push Secret. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
} else {
|
||||
logger.Error(err, "unable to reconcile Infisical Push Secret")
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
}
|
||||
|
||||
// Sync again after the specified time
|
||||
if requeueTime != 0 {
|
||||
logger.Info(fmt.Sprintf("Operator will requeue after [%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
} else {
|
||||
logger.Info("Operator will reconcile on next spec change")
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (r *InfisicalPushSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
// Custom predicate that allows both spec changes and deletions
|
||||
specChangeOrDelete := predicate.Funcs{
|
||||
UpdateFunc: func(e event.UpdateEvent) bool {
|
||||
// Only reconcile if spec/generation changed
|
||||
|
||||
isSpecOrGenerationChange := e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration()
|
||||
|
||||
if isSpecOrGenerationChange {
|
||||
if infisicalPushSecretResourceVariablesMap != nil {
|
||||
if rv, ok := infisicalPushSecretResourceVariablesMap[string(e.ObjectNew.GetUID())]; ok {
|
||||
rv.CancelCtx()
|
||||
delete(infisicalPushSecretResourceVariablesMap, string(e.ObjectNew.GetUID()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return isSpecOrGenerationChange
|
||||
},
|
||||
DeleteFunc: func(e event.DeleteEvent) bool {
|
||||
// Always reconcile on deletion
|
||||
|
||||
if infisicalPushSecretResourceVariablesMap != nil {
|
||||
if rv, ok := infisicalPushSecretResourceVariablesMap[string(e.Object.GetUID())]; ok {
|
||||
rv.CancelCtx()
|
||||
delete(infisicalPushSecretResourceVariablesMap, string(e.Object.GetUID()))
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
},
|
||||
CreateFunc: func(e event.CreateEvent) bool {
|
||||
// Reconcile on creation
|
||||
return true
|
||||
},
|
||||
GenericFunc: func(e event.GenericEvent) bool {
|
||||
// Ignore generic events
|
||||
return false
|
||||
},
|
||||
}
|
||||
|
||||
controllerManager := ctrl.NewControllerManagedBy(mgr).
|
||||
For(&secretsv1alpha1.InfisicalPushSecret{}, builder.WithPredicates(
|
||||
specChangeOrDelete,
|
||||
)).
|
||||
Watches(
|
||||
&corev1.Secret{},
|
||||
handler.EnqueueRequestsFromMapFunc(r.findPushSecretsForSecret),
|
||||
)
|
||||
|
||||
if !r.IsNamespaceScoped {
|
||||
r.BaseLogger.Info("Watching ClusterGenerators for non-namespace scoped operator")
|
||||
controllerManager.Watches(
|
||||
&secretsv1alpha1.ClusterGenerator{},
|
||||
handler.EnqueueRequestsFromMapFunc(r.findPushSecretsForClusterGenerator),
|
||||
)
|
||||
} else {
|
||||
r.BaseLogger.Info("Not watching ClusterGenerators for namespace scoped operator")
|
||||
}
|
||||
|
||||
return controllerManager.Complete(r)
|
||||
}
|
||||
|
||||
func (r *InfisicalPushSecretReconciler) findPushSecretsForClusterGenerator(ctx context.Context, o client.Object) []reconcile.Request {
|
||||
pushSecrets := &secretsv1alpha1.InfisicalPushSecretList{}
|
||||
if err := r.List(ctx, pushSecrets); err != nil {
|
||||
return []reconcile.Request{}
|
||||
}
|
||||
|
||||
clusterGenerator, ok := o.(*secretsv1alpha1.ClusterGenerator)
|
||||
if !ok {
|
||||
return []reconcile.Request{}
|
||||
}
|
||||
|
||||
requests := []reconcile.Request{}
|
||||
|
||||
for _, pushSecret := range pushSecrets.Items {
|
||||
if pushSecret.Spec.Push.Generators != nil {
|
||||
for _, generator := range pushSecret.Spec.Push.Generators {
|
||||
if generator.GeneratorRef.Name == clusterGenerator.GetName() {
|
||||
requests = append(requests, reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: pushSecret.GetName(),
|
||||
Namespace: pushSecret.GetNamespace(),
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return requests
|
||||
}
|
||||
|
||||
func (r *InfisicalPushSecretReconciler) findPushSecretsForSecret(ctx context.Context, o client.Object) []reconcile.Request {
|
||||
pushSecrets := &secretsv1alpha1.InfisicalPushSecretList{}
|
||||
if err := r.List(ctx, pushSecrets); err != nil {
|
||||
return []reconcile.Request{}
|
||||
}
|
||||
|
||||
requests := []reconcile.Request{}
|
||||
for _, pushSecret := range pushSecrets.Items {
|
||||
if pushSecret.Spec.Push.Secret != nil &&
|
||||
pushSecret.Spec.Push.Secret.SecretName == o.GetName() &&
|
||||
pushSecret.Spec.Push.Secret.SecretNamespace == o.GetNamespace() {
|
||||
requests = append(requests, reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Name: pushSecret.GetName(),
|
||||
Namespace: pushSecret.GetNamespace(),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return requests
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
)
|
||||
|
||||
var _ = Describe("InfisicalPushSecretSecret Controller", func() {
|
||||
Context("When reconciling a resource", func() {
|
||||
const resourceName = "test-resource"
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
typeNamespacedName := types.NamespacedName{
|
||||
Name: resourceName,
|
||||
Namespace: "default", // TODO(user):Modify as needed
|
||||
}
|
||||
infisicalpushsecretsecret := &secretsv1alpha1.InfisicalPushSecret{}
|
||||
|
||||
BeforeEach(func() {
|
||||
By("creating the custom resource for the Kind InfisicalPushSecretSecret")
|
||||
err := k8sClient.Get(ctx, typeNamespacedName, infisicalpushsecretsecret)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
resource := &secretsv1alpha1.InfisicalPushSecret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: resourceName,
|
||||
Namespace: "default",
|
||||
},
|
||||
// TODO(user): Specify other spec details if needed.
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
||||
}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
// TODO(user): Cleanup logic after each test, like removing the resource instance.
|
||||
resource := &secretsv1alpha1.InfisicalPushSecret{}
|
||||
err := k8sClient.Get(ctx, typeNamespacedName, resource)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Cleanup the specific resource instance InfisicalPushSecretSecret")
|
||||
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
|
||||
})
|
||||
It("should successfully reconcile the resource", func() {
|
||||
By("Reconciling the created resource")
|
||||
controllerReconciler := &InfisicalPushSecretReconciler{
|
||||
Client: k8sClient,
|
||||
Scheme: k8sClient.Scheme(),
|
||||
}
|
||||
|
||||
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
|
||||
NamespacedName: typeNamespacedName,
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
|
||||
// Example: If you expect a certain status condition after reconciliation, verify it here.
|
||||
})
|
||||
})
|
||||
})
|
||||
256
k8-operator/internal/controller/infisicalsecret_controller.go
Normal file
256
k8-operator/internal/controller/infisicalsecret_controller.go
Normal file
@@ -0,0 +1,256 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
defaultErrors "errors"
|
||||
|
||||
infisicalsecret "github.com/Infisical/infisical/k8-operator/internal/services/infisicalsecret"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
ctrl "sigs.k8s.io/controller-runtime"
|
||||
"sigs.k8s.io/controller-runtime/pkg/builder"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/predicate"
|
||||
"sigs.k8s.io/controller-runtime/pkg/source"
|
||||
|
||||
secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/controllerhelpers"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/util"
|
||||
"github.com/go-logr/logr"
|
||||
)
|
||||
|
||||
// InfisicalSecretReconciler reconciles a InfisicalSecret object
|
||||
type InfisicalSecretReconciler struct {
|
||||
client.Client
|
||||
BaseLogger logr.Logger
|
||||
Scheme *runtime.Scheme
|
||||
|
||||
SourceCh chan event.TypedGenericEvent[client.Object]
|
||||
Namespace string
|
||||
IsNamespaceScoped bool
|
||||
}
|
||||
|
||||
var infisicalSecretResourceVariablesMap map[string]util.ResourceVariables = make(map[string]util.ResourceVariables)
|
||||
|
||||
func (r *InfisicalSecretReconciler) GetLogger(req ctrl.Request) logr.Logger {
|
||||
return r.BaseLogger.WithValues("infisicalsecret", req.NamespacedName)
|
||||
}
|
||||
|
||||
//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets,verbs=get;list;watch;create;update;patch;delete
|
||||
//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/status,verbs=get;update;patch
|
||||
//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/finalizers,verbs=update
|
||||
//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;delete
|
||||
//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;delete
|
||||
//+kubebuilder:rbac:groups=apps,resources=deployments;daemonsets;statefulsets,verbs=list;watch;get;update
|
||||
//+kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch
|
||||
//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list
|
||||
//+kubebuilder:rbac:groups="authentication.k8s.io",resources=tokenreviews,verbs=create
|
||||
//+kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create
|
||||
|
||||
// Reconcile is part of the main kubernetes reconciliation loop which aims to
|
||||
// move the current state of the cluster closer to the desired state.
|
||||
// TODO(user): Modify the Reconcile function to compare the state specified by
|
||||
// the InfisicalSecret object against the actual cluster state, and then
|
||||
// perform operations to make the cluster state reflect the state specified by
|
||||
// the user.
|
||||
//
|
||||
// For more details, check Reconcile and its Result here:
|
||||
// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.21.0/pkg/reconcile
|
||||
func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
|
||||
logger := r.GetLogger(req)
|
||||
|
||||
logger.Info("Reconcile called")
|
||||
|
||||
var infisicalSecretCRD secretsv1alpha1.InfisicalSecret
|
||||
requeueTime := time.Minute // seconds
|
||||
|
||||
err := r.Get(ctx, req.NamespacedName, &infisicalSecretCRD)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return ctrl.Result{
|
||||
Requeue: false,
|
||||
}, nil
|
||||
} else {
|
||||
logger.Error(err, "unable to fetch Infisical Secret CRD from cluster")
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
|
||||
// It's important we don't directly modify the CRD object, so we create a copy of it and move existing data into it.
|
||||
managedKubeSecretReferences := infisicalSecretCRD.Spec.ManagedKubeSecretReferences
|
||||
managedKubeConfigMapReferences := infisicalSecretCRD.Spec.ManagedKubeConfigMapReferences
|
||||
|
||||
if infisicalSecretCRD.Spec.ManagedSecretReference.SecretName != "" && managedKubeSecretReferences != nil && len(managedKubeSecretReferences) > 0 {
|
||||
errMessage := "InfisicalSecret CRD cannot have both managedSecretReference and managedKubeSecretReferences"
|
||||
logger.Error(defaultErrors.New(errMessage), errMessage)
|
||||
return ctrl.Result{}, defaultErrors.New(errMessage)
|
||||
}
|
||||
|
||||
if infisicalSecretCRD.Spec.ManagedSecretReference.SecretName != "" {
|
||||
logger.Info("\n\n\nThe field `managedSecretReference` will be deprecated in the near future, please use `managedKubeSecretReferences` instead.\n\nRefer to the documentation for more information: https://infisical.com/docs/integrations/platforms/kubernetes/infisical-secret-crd\n\n\n")
|
||||
|
||||
if managedKubeSecretReferences == nil {
|
||||
managedKubeSecretReferences = []secretsv1alpha1.ManagedKubeSecretConfig{}
|
||||
}
|
||||
managedKubeSecretReferences = append(managedKubeSecretReferences, infisicalSecretCRD.Spec.ManagedSecretReference)
|
||||
}
|
||||
|
||||
if len(managedKubeSecretReferences) == 0 && len(managedKubeConfigMapReferences) == 0 {
|
||||
errMessage := "InfisicalSecret CRD must have at least one managed secret reference set in the `managedKubeSecretReferences` or `managedKubeConfigMapReferences` field"
|
||||
logger.Error(defaultErrors.New(errMessage), errMessage)
|
||||
return ctrl.Result{}, defaultErrors.New(errMessage)
|
||||
}
|
||||
|
||||
// Remove finalizers if they exist. This is to support previous InfisicalSecret CRD's that have finalizers on them.
|
||||
// In order to delete secrets with finalizers, we first remove the finalizers so we can use the simplified and improved deletion process
|
||||
if !infisicalSecretCRD.ObjectMeta.DeletionTimestamp.IsZero() && len(infisicalSecretCRD.ObjectMeta.Finalizers) > 0 {
|
||||
infisicalSecretCRD.ObjectMeta.Finalizers = []string{}
|
||||
if err := r.Update(ctx, &infisicalSecretCRD); err != nil {
|
||||
logger.Error(err, fmt.Sprintf("Error removing finalizers from Infisical Secret %s", infisicalSecretCRD.Name))
|
||||
return ctrl.Result{}, err
|
||||
}
|
||||
// Our finalizers have been removed, so the reconciler can do nothing.
|
||||
return ctrl.Result{}, nil
|
||||
}
|
||||
|
||||
if infisicalSecretCRD.Spec.ResyncInterval != 0 {
|
||||
requeueTime = time.Second * time.Duration(infisicalSecretCRD.Spec.ResyncInterval)
|
||||
logger.Info(fmt.Sprintf("Manual re-sync interval set. Interval: %v", requeueTime))
|
||||
|
||||
} else {
|
||||
logger.Info(fmt.Sprintf("Re-sync interval set. Interval: %v", requeueTime))
|
||||
}
|
||||
|
||||
// Check if the resource is already marked for deletion
|
||||
if infisicalSecretCRD.GetDeletionTimestamp() != nil {
|
||||
return ctrl.Result{
|
||||
Requeue: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Get modified/default config
|
||||
infisicalConfig, err := controllerhelpers.GetInfisicalConfigMap(ctx, r.Client, r.IsNamespaceScoped)
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to fetch infisical-config. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Initialize the business logic handler
|
||||
handler := infisicalsecret.NewInfisicalSecretHandler(r.Client, r.Scheme, r.IsNamespaceScoped)
|
||||
|
||||
// Setup API configuration through business logic
|
||||
err = handler.SetupAPIConfig(infisicalSecretCRD, infisicalConfig)
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to setup API configuration. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Handle CA certificate through business logic
|
||||
err = handler.HandleCACertificate(ctx, infisicalSecretCRD)
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to handle CA certificate. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
secretsCount, err := handler.ReconcileInfisicalSecret(ctx, logger, &infisicalSecretCRD, managedKubeSecretReferences, managedKubeConfigMapReferences, infisicalSecretResourceVariablesMap)
|
||||
handler.SetReadyToSyncSecretsConditions(ctx, logger, &infisicalSecretCRD, secretsCount, err)
|
||||
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to reconcile InfisicalSecret. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
numDeployments, err := controllerhelpers.ReconcileDeploymentsWithMultipleManagedSecrets(ctx, r.Client, logger, managedKubeSecretReferences, r.IsNamespaceScoped)
|
||||
handler.SetInfisicalAutoRedeploymentReady(ctx, logger, &infisicalSecretCRD, numDeployments, err)
|
||||
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to reconcile auto redeployment. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
if infisicalSecretCRD.Spec.InstantUpdates {
|
||||
logger.Info("Instant updates are enabled")
|
||||
|
||||
if err := r.EnsureEventStream(ctx, logger, &infisicalSecretCRD); err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to ensure event stream. Will requeue after [requeueTime=%v]", requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
} else {
|
||||
r.CloseEventStream(ctx, logger, &infisicalSecretCRD)
|
||||
}
|
||||
|
||||
// Sync again after the specified time
|
||||
logger.Info(fmt.Sprintf("Successfully synced %d secrets. Operator will requeue after [%v]", secretsCount, requeueTime))
|
||||
return ctrl.Result{
|
||||
RequeueAfter: requeueTime,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *InfisicalSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
|
||||
r.SourceCh = make(chan event.TypedGenericEvent[client.Object])
|
||||
|
||||
return ctrl.NewControllerManagedBy(mgr).
|
||||
WatchesRawSource(
|
||||
source.Channel[client.Object](r.SourceCh, &util.EnqueueDelayedEventHandler{Delay: time.Second * 3}),
|
||||
).
|
||||
For(&secretsv1alpha1.InfisicalSecret{}, builder.WithPredicates(predicate.Funcs{
|
||||
UpdateFunc: func(e event.UpdateEvent) bool {
|
||||
if e.ObjectOld.GetGeneration() == e.ObjectNew.GetGeneration() {
|
||||
return false // Skip reconciliation for status-only changes
|
||||
}
|
||||
|
||||
if infisicalSecretResourceVariablesMap != nil {
|
||||
if rv, ok := infisicalSecretResourceVariablesMap[string(e.ObjectNew.GetUID())]; ok {
|
||||
rv.CancelCtx()
|
||||
delete(infisicalSecretResourceVariablesMap, string(e.ObjectNew.GetUID()))
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
DeleteFunc: func(e event.DeleteEvent) bool {
|
||||
if infisicalSecretResourceVariablesMap != nil {
|
||||
if rv, ok := infisicalSecretResourceVariablesMap[string(e.Object.GetUID())]; ok {
|
||||
rv.CancelCtx()
|
||||
delete(infisicalSecretResourceVariablesMap, string(e.Object.GetUID()))
|
||||
}
|
||||
}
|
||||
return true
|
||||
},
|
||||
})).
|
||||
Complete(r)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
. "github.com/onsi/ginkgo/v2"
|
||||
. "github.com/onsi/gomega"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
)
|
||||
|
||||
var _ = Describe("InfisicalSecret Controller", func() {
|
||||
Context("When reconciling a resource", func() {
|
||||
const resourceName = "test-resource"
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
typeNamespacedName := types.NamespacedName{
|
||||
Name: resourceName,
|
||||
Namespace: "default", // TODO(user):Modify as needed
|
||||
}
|
||||
infisicalsecret := &secretsv1alpha1.InfisicalSecret{}
|
||||
|
||||
BeforeEach(func() {
|
||||
By("creating the custom resource for the Kind InfisicalSecret")
|
||||
err := k8sClient.Get(ctx, typeNamespacedName, infisicalsecret)
|
||||
if err != nil && errors.IsNotFound(err) {
|
||||
resource := &secretsv1alpha1.InfisicalSecret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: resourceName,
|
||||
Namespace: "default",
|
||||
},
|
||||
// TODO(user): Specify other spec details if needed.
|
||||
}
|
||||
Expect(k8sClient.Create(ctx, resource)).To(Succeed())
|
||||
}
|
||||
})
|
||||
|
||||
AfterEach(func() {
|
||||
// TODO(user): Cleanup logic after each test, like removing the resource instance.
|
||||
resource := &secretsv1alpha1.InfisicalSecret{}
|
||||
err := k8sClient.Get(ctx, typeNamespacedName, resource)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
By("Cleanup the specific resource instance InfisicalSecret")
|
||||
Expect(k8sClient.Delete(ctx, resource)).To(Succeed())
|
||||
})
|
||||
It("should successfully reconcile the resource", func() {
|
||||
By("Reconciling the created resource")
|
||||
controllerReconciler := &InfisicalSecretReconciler{
|
||||
Client: k8sClient,
|
||||
Scheme: k8sClient.Scheme(),
|
||||
}
|
||||
|
||||
_, err := controllerReconciler.Reconcile(ctx, reconcile.Request{
|
||||
NamespacedName: typeNamespacedName,
|
||||
})
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
// TODO(user): Add more specific assertions depending on your controller's reconciliation logic.
|
||||
// Example: If you expect a certain status condition after reconciliation, verify it here.
|
||||
})
|
||||
})
|
||||
})
|
||||
116
k8-operator/internal/controller/suite_test.go
Normal file
116
k8-operator/internal/controller/suite_test.go
Normal file
@@ -0,0 +1,116 @@
|
||||
/*
|
||||
Copyright 2025.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
|
||||
package controller
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"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 (
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
testEnv *envtest.Environment
|
||||
cfg *rest.Config
|
||||
k8sClient client.Client
|
||||
)
|
||||
|
||||
func TestControllers(t *testing.T) {
|
||||
RegisterFailHandler(Fail)
|
||||
|
||||
RunSpecs(t, "Controller Suite")
|
||||
}
|
||||
|
||||
var _ = BeforeSuite(func() {
|
||||
logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true)))
|
||||
|
||||
ctx, cancel = context.WithCancel(context.TODO())
|
||||
|
||||
var err error
|
||||
err = secretsv1alpha1.AddToScheme(scheme.Scheme)
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
|
||||
// +kubebuilder:scaffold:scheme
|
||||
|
||||
By("bootstrapping test environment")
|
||||
testEnv = &envtest.Environment{
|
||||
CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")},
|
||||
ErrorIfCRDPathMissing: true,
|
||||
}
|
||||
|
||||
// Retrieve the first found binary directory to allow running tests from IDEs
|
||||
if getFirstFoundEnvTestBinaryDir() != "" {
|
||||
testEnv.BinaryAssetsDirectory = getFirstFoundEnvTestBinaryDir()
|
||||
}
|
||||
|
||||
// cfg is defined in this file globally.
|
||||
cfg, err = testEnv.Start()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
Expect(cfg).NotTo(BeNil())
|
||||
|
||||
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")
|
||||
cancel()
|
||||
err := testEnv.Stop()
|
||||
Expect(err).NotTo(HaveOccurred())
|
||||
})
|
||||
|
||||
// getFirstFoundEnvTestBinaryDir locates the first binary in the specified path.
|
||||
// ENVTEST-based tests depend on specific binaries, usually located in paths set by
|
||||
// controller-runtime. When running tests directly (e.g., via an IDE) without using
|
||||
// Makefile targets, the 'BinaryAssetsDirectory' must be explicitly configured.
|
||||
//
|
||||
// This function streamlines the process by finding the required binaries, similar to
|
||||
// setting the 'KUBEBUILDER_ASSETS' environment variable. To ensure the binaries are
|
||||
// properly set up, run 'make setup-envtest' beforehand.
|
||||
func getFirstFoundEnvTestBinaryDir() string {
|
||||
basePath := filepath.Join("..", "..", "bin", "k8s")
|
||||
entries, err := os.ReadDir(basePath)
|
||||
if err != nil {
|
||||
logf.Log.Error(err, "Failed to read directory", "path", basePath)
|
||||
return ""
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() {
|
||||
return filepath.Join(basePath, entry.Name())
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
304
k8-operator/internal/controllerhelpers/controllerhelpers.go
Normal file
304
k8-operator/internal/controllerhelpers/controllerhelpers.go
Normal file
@@ -0,0 +1,304 @@
|
||||
package controllerhelpers
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/constants"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/util"
|
||||
"github.com/go-logr/logr"
|
||||
v1 "k8s.io/api/apps/v1"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8Errors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
controllerClient "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 ReconcileDeploymentsWithManagedSecrets(ctx context.Context, client controllerClient.Client, logger logr.Logger, managedSecret v1alpha1.ManagedKubeSecretConfig, isNamespaceScoped bool) (int, error) {
|
||||
listOfDeployments := &v1.DeploymentList{}
|
||||
|
||||
err := client.List(ctx, listOfDeployments, &controllerClient.ListOptions{Namespace: managedSecret.SecretNamespace})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("unable to get deployments in the [namespace=%v] [err=%v]", managedSecret.SecretNamespace, err)
|
||||
}
|
||||
|
||||
listOfDaemonSets := &v1.DaemonSetList{}
|
||||
err = client.List(ctx, listOfDaemonSets, &controllerClient.ListOptions{Namespace: managedSecret.SecretNamespace})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("unable to get daemonSets in the [namespace=%v] [err=%v]", managedSecret.SecretNamespace, err)
|
||||
}
|
||||
|
||||
listOfStatefulSets := &v1.StatefulSetList{}
|
||||
err = client.List(ctx, listOfStatefulSets, &controllerClient.ListOptions{Namespace: managedSecret.SecretNamespace})
|
||||
if err != nil {
|
||||
return 0, fmt.Errorf("unable to get statefulSets in the [namespace=%v] [err=%v]", managedSecret.SecretNamespace, err)
|
||||
}
|
||||
|
||||
managedKubeSecretNameAndNamespace := types.NamespacedName{
|
||||
Namespace: managedSecret.SecretNamespace,
|
||||
Name: managedSecret.SecretName,
|
||||
}
|
||||
|
||||
managedKubeSecret := &corev1.Secret{}
|
||||
err = client.Get(ctx, managedKubeSecretNameAndNamespace, managedKubeSecret)
|
||||
if err != nil {
|
||||
if util.IsNamespaceScopedError(err, isNamespaceScoped) {
|
||||
return 0, fmt.Errorf("unable to fetch Kubernetes secret to update deployment. Your Operator is namespace scoped, and cannot read secrets outside of its namespace. Please ensure the secret is in the same namespace as the operator. [err=%v]", err)
|
||||
}
|
||||
|
||||
return 0, fmt.Errorf("unable to fetch Kubernetes secret to update deployment: %v", err)
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
|
||||
// Iterate over the deployments and check if they use the managed secret
|
||||
for _, deployment := range listOfDeployments.Items {
|
||||
deployment := deployment
|
||||
if deployment.Annotations[AUTO_RELOAD_DEPLOYMENT_ANNOTATION] == "true" && IsDeploymentUsingManagedSecret(deployment, managedSecret) {
|
||||
// Start a goroutine to reconcile the deployment
|
||||
wg.Add(1)
|
||||
go func(deployment v1.Deployment, managedSecret corev1.Secret) {
|
||||
defer wg.Done()
|
||||
if err := ReconcileDeployment(ctx, client, logger, deployment, managedSecret); err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to reconcile deployment with [name=%v]. Will try next requeue", deployment.ObjectMeta.Name))
|
||||
}
|
||||
}(deployment, *managedKubeSecret)
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over the daemonSets and check if they use the managed secret
|
||||
for _, daemonSet := range listOfDaemonSets.Items {
|
||||
daemonSet := daemonSet
|
||||
if daemonSet.Annotations[AUTO_RELOAD_DEPLOYMENT_ANNOTATION] == "true" && IsDaemonSetUsingManagedSecret(daemonSet, managedSecret) {
|
||||
wg.Add(1)
|
||||
go func(deployment v1.DaemonSet, managedSecret corev1.Secret) {
|
||||
defer wg.Done()
|
||||
if err := ReconcileDaemonSet(ctx, client, logger, daemonSet, managedSecret); err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to reconcile daemonset with [name=%v]. Will try next requeue", deployment.ObjectMeta.Name))
|
||||
}
|
||||
}(daemonSet, *managedKubeSecret)
|
||||
}
|
||||
}
|
||||
|
||||
// Iterate over the statefulSets and check if they use the managed secret
|
||||
for _, statefulSet := range listOfStatefulSets.Items {
|
||||
statefulSet := statefulSet
|
||||
if statefulSet.Annotations[AUTO_RELOAD_DEPLOYMENT_ANNOTATION] == "true" && IsStatefulSetUsingManagedSecret(statefulSet, managedSecret) {
|
||||
wg.Add(1)
|
||||
go func(statefulSet v1.StatefulSet, managedSecret corev1.Secret) {
|
||||
defer wg.Done()
|
||||
if err := ReconcileStatefulSet(ctx, client, logger, statefulSet, managedSecret); err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to reconcile statefulset with [name=%v]. Will try next requeue", statefulSet.ObjectMeta.Name))
|
||||
}
|
||||
}(statefulSet, *managedKubeSecret)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func ReconcileDeploymentsWithMultipleManagedSecrets(ctx context.Context, client controllerClient.Client, logger logr.Logger, managedSecrets []v1alpha1.ManagedKubeSecretConfig, isNamespaceScoped bool) (int, error) {
|
||||
for _, managedSecret := range managedSecrets {
|
||||
_, err := ReconcileDeploymentsWithManagedSecrets(ctx, client, logger, managedSecret, isNamespaceScoped)
|
||||
if err != nil {
|
||||
logger.Error(err, fmt.Sprintf("unable to reconcile deployments with managed secret [name=%v]", managedSecret.SecretName))
|
||||
return 0, err
|
||||
}
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
// Check if the deployment uses managed secrets
|
||||
func IsDeploymentUsingManagedSecret(deployment v1.Deployment, managedSecret v1alpha1.ManagedKubeSecretConfig) bool {
|
||||
managedSecretName := managedSecret.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
|
||||
}
|
||||
|
||||
func IsDaemonSetUsingManagedSecret(daemonSet v1.DaemonSet, managedSecret v1alpha1.ManagedKubeSecretConfig) bool {
|
||||
managedSecretName := managedSecret.SecretName
|
||||
for _, container := range daemonSet.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 daemonSet.Spec.Template.Spec.Volumes {
|
||||
if volume.Secret != nil && volume.Secret.SecretName == managedSecretName {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func IsStatefulSetUsingManagedSecret(statefulSet v1.StatefulSet, managedSecret v1alpha1.ManagedKubeSecretConfig) bool {
|
||||
managedSecretName := managedSecret.SecretName
|
||||
for _, container := range statefulSet.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 statefulSet.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 ReconcileDeployment(ctx context.Context, client controllerClient.Client, logger logr.Logger, deployment v1.Deployment, secret corev1.Secret) error {
|
||||
annotationKey := fmt.Sprintf("%s.%s", DEPLOYMENT_SECRET_NAME_ANNOTATION_PREFIX, secret.Name)
|
||||
annotationValue := secret.Annotations[constants.SECRET_VERSION_ANNOTATION]
|
||||
|
||||
if deployment.Annotations[annotationKey] == annotationValue &&
|
||||
deployment.Spec.Template.Annotations[annotationKey] == annotationValue {
|
||||
logger.Info(fmt.Sprintf("The [deploymentName=%v] is already using the most up to date managed secrets. No action required.", deployment.ObjectMeta.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("Deployment is using outdated managed secret. Starting re-deployment [deploymentName=%v]", 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 := client.Update(ctx, &deployment); err != nil {
|
||||
return fmt.Errorf("failed to update deployment annotation: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReconcileDaemonSet(ctx context.Context, client controllerClient.Client, logger logr.Logger, daemonSet v1.DaemonSet, secret corev1.Secret) error {
|
||||
annotationKey := fmt.Sprintf("%s.%s", DEPLOYMENT_SECRET_NAME_ANNOTATION_PREFIX, secret.Name)
|
||||
annotationValue := secret.Annotations[constants.SECRET_VERSION_ANNOTATION]
|
||||
|
||||
if daemonSet.Annotations[annotationKey] == annotationValue &&
|
||||
daemonSet.Spec.Template.Annotations[annotationKey] == annotationValue {
|
||||
logger.Info(fmt.Sprintf("The [daemonSetName=%v] is already using the most up to date managed secrets. No action required.", daemonSet.ObjectMeta.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("DaemonSet is using outdated managed secret. Starting re-deployment [daemonSetName=%v]", daemonSet.ObjectMeta.Name))
|
||||
|
||||
if daemonSet.Spec.Template.Annotations == nil {
|
||||
daemonSet.Spec.Template.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
daemonSet.Annotations[annotationKey] = annotationValue
|
||||
daemonSet.Spec.Template.Annotations[annotationKey] = annotationValue
|
||||
|
||||
if err := client.Update(ctx, &daemonSet); err != nil {
|
||||
return fmt.Errorf("failed to update daemonSet annotation: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ReconcileStatefulSet(ctx context.Context, client controllerClient.Client, logger logr.Logger, statefulSet v1.StatefulSet, secret corev1.Secret) error {
|
||||
annotationKey := fmt.Sprintf("%s.%s", DEPLOYMENT_SECRET_NAME_ANNOTATION_PREFIX, secret.Name)
|
||||
annotationValue := secret.Annotations[constants.SECRET_VERSION_ANNOTATION]
|
||||
|
||||
if statefulSet.Annotations[annotationKey] == annotationValue &&
|
||||
statefulSet.Spec.Template.Annotations[annotationKey] == annotationValue {
|
||||
logger.Info(fmt.Sprintf("The [statefulSetName=%v] is already using the most up to date managed secrets. No action required.", statefulSet.ObjectMeta.Name))
|
||||
return nil
|
||||
}
|
||||
|
||||
logger.Info(fmt.Sprintf("StatefulSet is using outdated managed secret. Starting re-deployment [statefulSetName=%v]", statefulSet.ObjectMeta.Name))
|
||||
|
||||
if statefulSet.Spec.Template.Annotations == nil {
|
||||
statefulSet.Spec.Template.Annotations = make(map[string]string)
|
||||
}
|
||||
|
||||
statefulSet.Annotations[annotationKey] = annotationValue
|
||||
statefulSet.Spec.Template.Annotations[annotationKey] = annotationValue
|
||||
|
||||
if err := client.Update(ctx, &statefulSet); err != nil {
|
||||
return fmt.Errorf("failed to update statefulSet annotation: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetInfisicalConfigMap(ctx context.Context, client client.Client, isNamespaceScoped bool) (configMap map[string]string, errToReturn error) {
|
||||
// default key values
|
||||
defaultConfigMapData := make(map[string]string)
|
||||
defaultConfigMapData["hostAPI"] = constants.INFISICAL_DOMAIN
|
||||
|
||||
// this will never work if we're namespace scoped, because the operator can't read outside of its namespace by our current RBAC rules.
|
||||
// This is how it has always worked, but the error has been masked as 'not found' in V3 kubebuilder.
|
||||
if isNamespaceScoped {
|
||||
return defaultConfigMapData, nil
|
||||
}
|
||||
|
||||
kubeConfigMap := &corev1.ConfigMap{}
|
||||
err := client.Get(ctx, types.NamespacedName{
|
||||
Namespace: constants.OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE,
|
||||
Name: constants.OPERATOR_SETTINGS_CONFIGMAP_NAME,
|
||||
}, kubeConfigMap)
|
||||
|
||||
if err != nil {
|
||||
if k8Errors.IsNotFound(err) {
|
||||
kubeConfigMap = nil
|
||||
} else {
|
||||
return nil, fmt.Errorf("GetConfigMapByNamespacedName: unable to fetch config map in [namespacedName=%s] [err=%s]", constants.OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE, err)
|
||||
}
|
||||
}
|
||||
|
||||
if kubeConfigMap == nil {
|
||||
return defaultConfigMapData, nil
|
||||
} else {
|
||||
for key, value := range defaultConfigMapData {
|
||||
_, exists := kubeConfigMap.Data[key]
|
||||
if !exists {
|
||||
kubeConfigMap.Data[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return kubeConfigMap.Data, nil
|
||||
}
|
||||
}
|
||||
42
k8-operator/internal/crypto/crypto.go
Normal file
42
k8-operator/internal/crypto/crypto.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package crypto
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"fmt"
|
||||
"hash/crc32"
|
||||
|
||||
"golang.org/x/crypto/nacl/box"
|
||||
)
|
||||
|
||||
func DecryptSymmetric(key []byte, encryptedPrivateKey []byte, tag []byte, IV []byte) ([]byte, error) {
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
aesgcm, err := cipher.NewGCMWithNonceSize(block, len(IV))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var nonce = IV
|
||||
var ciphertext = append(encryptedPrivateKey, tag...)
|
||||
|
||||
plaintext, err := aesgcm.Open(nil, nonce, ciphertext, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return plaintext, nil
|
||||
}
|
||||
|
||||
func DecryptAsymmetric(ciphertext []byte, nonce []byte, publicKey []byte, privateKey []byte) (plainText []byte) {
|
||||
plainTextToReturn, _ := box.Open(nil, ciphertext, (*[24]byte)(nonce), (*[32]byte)(publicKey), (*[32]byte)(privateKey))
|
||||
return plainTextToReturn
|
||||
}
|
||||
|
||||
func ComputeEtag(data []byte) string {
|
||||
crc := crc32.ChecksumIEEE(data)
|
||||
return fmt.Sprintf(`W/"secrets-%d-%08X"`, len(data), crc)
|
||||
}
|
||||
1
k8-operator/internal/generator/generator.go
Normal file
1
k8-operator/internal/generator/generator.go
Normal file
@@ -0,0 +1 @@
|
||||
package generator
|
||||
76
k8-operator/internal/generator/password.go
Normal file
76
k8-operator/internal/generator/password.go
Normal file
@@ -0,0 +1,76 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/sethvargo/go-password/password"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultLength = 24
|
||||
defaultSymbolChars = "~!@#$%^&*()_+`-={}|[]\\:\"<>?,./"
|
||||
digitFactor = 0.25
|
||||
symbolFactor = 0.25
|
||||
)
|
||||
|
||||
func generateSafePassword(
|
||||
passLen int,
|
||||
symbols int,
|
||||
symbolCharacters string,
|
||||
digits int,
|
||||
noUpper bool,
|
||||
allowRepeat bool,
|
||||
) (string, error) {
|
||||
gen, err := password.NewGenerator(&password.GeneratorInput{
|
||||
Symbols: symbolCharacters,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return gen.Generate(
|
||||
passLen,
|
||||
digits,
|
||||
symbols,
|
||||
noUpper,
|
||||
allowRepeat,
|
||||
)
|
||||
}
|
||||
|
||||
func GeneratorPassword(spec v1alpha1.PasswordSpec) (string, error) {
|
||||
|
||||
symbolCharacters := defaultSymbolChars
|
||||
|
||||
if spec.SymbolCharacters != nil && *spec.SymbolCharacters != "" {
|
||||
symbolCharacters = *spec.SymbolCharacters
|
||||
}
|
||||
|
||||
passwordLength := defaultLength
|
||||
|
||||
if spec.Length != 0 {
|
||||
passwordLength = spec.Length
|
||||
}
|
||||
|
||||
digits := int(float32(passwordLength) * digitFactor)
|
||||
if spec.Digits != nil {
|
||||
digits = *spec.Digits
|
||||
}
|
||||
|
||||
symbols := int(float32(passwordLength) * symbolFactor)
|
||||
if spec.Symbols != nil {
|
||||
symbols = *spec.Symbols
|
||||
}
|
||||
|
||||
pass, err := generateSafePassword(
|
||||
passwordLength,
|
||||
symbols,
|
||||
symbolCharacters,
|
||||
digits,
|
||||
spec.NoUpper,
|
||||
spec.AllowRepeat,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return pass, nil
|
||||
}
|
||||
10
k8-operator/internal/generator/uuid.go
Normal file
10
k8-operator/internal/generator/uuid.go
Normal file
@@ -0,0 +1,10 @@
|
||||
package generator
|
||||
|
||||
import (
|
||||
"github.com/google/uuid"
|
||||
)
|
||||
|
||||
func GeneratorUUID() (string, error) {
|
||||
uuid := uuid.New().String()
|
||||
return uuid, nil
|
||||
}
|
||||
42
k8-operator/internal/model/model.go
Normal file
42
k8-operator/internal/model/model.go
Normal file
@@ -0,0 +1,42 @@
|
||||
package model
|
||||
|
||||
type ServiceAccountDetails struct {
|
||||
AccessKey string
|
||||
PublicKey string
|
||||
PrivateKey string
|
||||
}
|
||||
|
||||
type UniversalAuthIdentityDetails struct {
|
||||
ClientId string
|
||||
ClientSecret string
|
||||
}
|
||||
|
||||
type LdapIdentityDetails struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
type SingleEnvironmentVariable struct {
|
||||
Key string `json:"key"`
|
||||
Value string `json:"value"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
Type string `json:"type"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
|
||||
type SecretTemplateOptions struct {
|
||||
Value string `json:"value"`
|
||||
SecretPath string `json:"secretPath"`
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
OrgID string `json:"orgId"`
|
||||
Environments []struct {
|
||||
Name string `json:"name"`
|
||||
Slug string `json:"slug"`
|
||||
ID string `json:"id"`
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
120
k8-operator/internal/services/infisicaldynamicsecret/handler.go
Normal file
120
k8-operator/internal/services/infisicaldynamicsecret/handler.go
Normal file
@@ -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
|
||||
}
|
||||
156
k8-operator/internal/services/infisicalpushsecret/conditions.go
Normal file
156
k8-operator/internal/services/infisicalpushsecret/conditions.go
Normal file
@@ -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)
|
||||
}
|
||||
103
k8-operator/internal/services/infisicalpushsecret/handler.go
Normal file
103
k8-operator/internal/services/infisicalpushsecret/handler.go
Normal file
@@ -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)
|
||||
}
|
||||
573
k8-operator/internal/services/infisicalpushsecret/reconciler.go
Normal file
573
k8-operator/internal/services/infisicalpushsecret/reconciler.go
Normal file
@@ -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
|
||||
}
|
||||
100
k8-operator/internal/services/infisicalsecret/conditions.go
Normal file
100
k8-operator/internal/services/infisicalsecret/conditions.go
Normal file
@@ -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")
|
||||
}
|
||||
}
|
||||
102
k8-operator/internal/services/infisicalsecret/handler.go
Normal file
102
k8-operator/internal/services/infisicalsecret/handler.go
Normal file
@@ -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)
|
||||
}
|
||||
643
k8-operator/internal/services/infisicalsecret/reconciler.go
Normal file
643
k8-operator/internal/services/infisicalsecret/reconciler.go
Normal file
@@ -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
|
||||
}
|
||||
64
k8-operator/internal/services/infisicalsecret/suite_test.go
Normal file
64
k8-operator/internal/services/infisicalsecret/suite_test.go
Normal file
@@ -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())
|
||||
})
|
||||
18
k8-operator/internal/template/base64.go
Normal file
18
k8-operator/internal/template/base64.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func decodeBase64ToBytes(encodedString string) string {
|
||||
decoded, err := base64.StdEncoding.DecodeString(encodedString)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
return string(decoded)
|
||||
}
|
||||
|
||||
func encodeBase64(plainString string) string {
|
||||
return base64.StdEncoding.EncodeToString([]byte(plainString))
|
||||
}
|
||||
43
k8-operator/internal/template/jwk.go
Normal file
43
k8-operator/internal/template/jwk.go
Normal file
@@ -0,0 +1,43 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
|
||||
"github.com/lestrrat-go/jwx/v2/jwk"
|
||||
)
|
||||
|
||||
func jwkPublicKeyPem(jwkjson string) string {
|
||||
k, err := jwk.ParseKey([]byte(jwkjson))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("[jwkPublicKeyPem] Error: %v", err))
|
||||
}
|
||||
var rawkey any
|
||||
err = k.Raw(&rawkey)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("[jwkPublicKeyPem] Error: %v", err))
|
||||
}
|
||||
mpk, err := x509.MarshalPKIXPublicKey(rawkey)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("[jwkPublicKeyPem] Error: %v", err))
|
||||
}
|
||||
return pemEncode(mpk, "PUBLIC KEY")
|
||||
}
|
||||
|
||||
func jwkPrivateKeyPem(jwkjson string) string {
|
||||
k, err := jwk.ParseKey([]byte(jwkjson))
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("[jwkPrivateKeyPem] Error: %v", err))
|
||||
}
|
||||
var mpk []byte
|
||||
var pk any
|
||||
err = k.Raw(&pk)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("[jwkPrivateKeyPem] Error: %v", err))
|
||||
}
|
||||
mpk, err = x509.MarshalPKCS8PrivateKey(pk)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("[jwkPrivateKeyPem] Error: %v", err))
|
||||
}
|
||||
return pemEncode(mpk, "PRIVATE KEY")
|
||||
}
|
||||
98
k8-operator/internal/template/pem.go
Normal file
98
k8-operator/internal/template/pem.go
Normal file
@@ -0,0 +1,98 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
errJunk = "error filtering pem: found junk"
|
||||
|
||||
certTypeLeaf = "leaf"
|
||||
certTypeIntermediate = "intermediate"
|
||||
certTypeRoot = "root"
|
||||
)
|
||||
|
||||
func filterPEM(pemType, input string) string {
|
||||
data := []byte(input)
|
||||
var blocks []byte
|
||||
var block *pem.Block
|
||||
var rest []byte
|
||||
for {
|
||||
block, rest = pem.Decode(data)
|
||||
data = rest
|
||||
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
if !strings.EqualFold(block.Type, pemType) {
|
||||
continue
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := pem.Encode(&buf, block)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("[filterPEM] Error: %v", err))
|
||||
}
|
||||
blocks = append(blocks, buf.Bytes()...)
|
||||
}
|
||||
|
||||
if len(blocks) == 0 && len(rest) != 0 {
|
||||
panic(fmt.Sprintf("[filterPEM] Error: %v", errJunk))
|
||||
}
|
||||
|
||||
return string(blocks)
|
||||
}
|
||||
|
||||
func filterCertChain(certType, input string) string {
|
||||
ordered := fetchX509CertChains([]byte(input))
|
||||
|
||||
switch certType {
|
||||
case certTypeLeaf:
|
||||
cert := ordered[0]
|
||||
if cert.AuthorityKeyId != nil && !bytes.Equal(cert.AuthorityKeyId, cert.SubjectKeyId) {
|
||||
return pemEncode(ordered[0].Raw, pemTypeCertificate)
|
||||
}
|
||||
case certTypeIntermediate:
|
||||
if len(ordered) < 2 {
|
||||
return ""
|
||||
}
|
||||
var pemData []byte
|
||||
for _, cert := range ordered[1:] {
|
||||
if isRootCertificate(cert) {
|
||||
break
|
||||
}
|
||||
b := &pem.Block{
|
||||
Type: pemTypeCertificate,
|
||||
Bytes: cert.Raw,
|
||||
}
|
||||
pemData = append(pemData, pem.EncodeToMemory(b)...)
|
||||
}
|
||||
return string(pemData)
|
||||
case certTypeRoot:
|
||||
cert := ordered[len(ordered)-1]
|
||||
if isRootCertificate(cert) {
|
||||
return pemEncode(cert.Raw, pemTypeCertificate)
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
func isRootCertificate(cert *x509.Certificate) bool {
|
||||
return cert.AuthorityKeyId == nil || bytes.Equal(cert.AuthorityKeyId, cert.SubjectKeyId)
|
||||
}
|
||||
|
||||
func pemEncode(thing []byte, kind string) string {
|
||||
buf := bytes.NewBuffer(nil)
|
||||
err := pem.Encode(buf, &pem.Block{Type: kind, Bytes: thing})
|
||||
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("[pemEncode] Error: %v", err))
|
||||
}
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
117
k8-operator/internal/template/pem_chain.go
Normal file
117
k8-operator/internal/template/pem_chain.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
const (
|
||||
errNilCert = "certificate is nil"
|
||||
errFoundDisjunctCert = "found multiple leaf or disjunct certificates"
|
||||
errNoLeafFound = "no leaf certificate found"
|
||||
errChainCycle = "constructing chain resulted in cycle"
|
||||
)
|
||||
|
||||
type node struct {
|
||||
cert *x509.Certificate
|
||||
parent *node
|
||||
isParent bool
|
||||
}
|
||||
|
||||
func fetchX509CertChains(data []byte) []*x509.Certificate {
|
||||
var newCertChain []*x509.Certificate
|
||||
nodes := pemToNodes(data)
|
||||
|
||||
// at the end of this computation, the output will be a single linked list
|
||||
// the tail of the list will be the root node (which has no parents)
|
||||
// the head of the list will be the leaf node (whose parent will be intermediate certs)
|
||||
// (head) leaf -> intermediates -> root (tail)
|
||||
for i := range nodes {
|
||||
for j := range nodes {
|
||||
// ignore same node to prevent generating a cycle
|
||||
if i == j {
|
||||
continue
|
||||
}
|
||||
// if ith node AuthorityKeyId is same as jth node SubjectKeyId, jth node was used
|
||||
// to sign the ith certificate
|
||||
if bytes.Equal(nodes[i].cert.AuthorityKeyId, nodes[j].cert.SubjectKeyId) {
|
||||
nodes[j].isParent = true
|
||||
nodes[i].parent = nodes[j]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var foundLeaf bool
|
||||
var leaf *node
|
||||
for i := range nodes {
|
||||
if !nodes[i].isParent {
|
||||
if foundLeaf {
|
||||
panic(fmt.Sprintf("[fetchX509CertChains] Error: %v", errFoundDisjunctCert))
|
||||
}
|
||||
// this is the leaf node as it's not a parent for any other node
|
||||
leaf = nodes[i]
|
||||
foundLeaf = true
|
||||
}
|
||||
}
|
||||
|
||||
if leaf == nil {
|
||||
panic(fmt.Sprintf("[fetchX509CertChains] Error: %v", errNoLeafFound))
|
||||
}
|
||||
|
||||
processedNodes := 0
|
||||
// iterate through the directed list and append the nodes to new cert chain
|
||||
for leaf != nil {
|
||||
processedNodes++
|
||||
// ensure we aren't stuck in a cyclic loop
|
||||
if processedNodes > len(nodes) {
|
||||
panic(fmt.Sprintf("[fetchX509CertChains] Error: %v", errChainCycle))
|
||||
}
|
||||
newCertChain = append(newCertChain, leaf.cert)
|
||||
leaf = leaf.parent
|
||||
}
|
||||
return newCertChain
|
||||
}
|
||||
|
||||
func fetchCertChains(data []byte) []byte {
|
||||
var pemData []byte
|
||||
newCertChain := fetchX509CertChains(data)
|
||||
|
||||
for _, cert := range newCertChain {
|
||||
b := &pem.Block{
|
||||
Type: pemTypeCertificate,
|
||||
Bytes: cert.Raw,
|
||||
}
|
||||
pemData = append(pemData, pem.EncodeToMemory(b)...)
|
||||
}
|
||||
return pemData
|
||||
}
|
||||
|
||||
func pemToNodes(data []byte) []*node {
|
||||
nodes := make([]*node, 0)
|
||||
for {
|
||||
// decode pem to der first
|
||||
block, rest := pem.Decode(data)
|
||||
data = rest
|
||||
|
||||
if block == nil {
|
||||
break
|
||||
}
|
||||
cert, err := x509.ParseCertificate(block.Bytes)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("[pemToNodes] Error: %v", err))
|
||||
}
|
||||
|
||||
if cert == nil {
|
||||
panic(fmt.Sprintf("[pemToNodes] Error: %v", errNilCert))
|
||||
}
|
||||
nodes = append(nodes, &node{
|
||||
cert: cert,
|
||||
parent: nil,
|
||||
isParent: false,
|
||||
})
|
||||
}
|
||||
return nodes
|
||||
}
|
||||
144
k8-operator/internal/template/pkcs12.go
Normal file
144
k8-operator/internal/template/pkcs12.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
|
||||
gopkcs12 "software.sslmate.com/src/go-pkcs12"
|
||||
)
|
||||
|
||||
func pkcs12keyPass(pass, input string) string {
|
||||
privateKey, _, _, err := gopkcs12.DecodeChain([]byte(input), pass)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
|
||||
marshalPrivateKey, err := x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := pem.Encode(&buf, &pem.Block{
|
||||
Type: pemTypeKey,
|
||||
Bytes: marshalPrivateKey,
|
||||
}); err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func parsePrivateKey(block []byte) any {
|
||||
if k, err := x509.ParsePKCS1PrivateKey(block); err == nil {
|
||||
return k
|
||||
}
|
||||
if k, err := x509.ParsePKCS8PrivateKey(block); err == nil {
|
||||
return k
|
||||
}
|
||||
if k, err := x509.ParseECPrivateKey(block); err == nil {
|
||||
return k
|
||||
}
|
||||
panic("Error: unable to parse private key")
|
||||
}
|
||||
|
||||
func pkcs12key(input string) string {
|
||||
return pkcs12keyPass("", input)
|
||||
}
|
||||
|
||||
func pkcs12certPass(pass, input string) string {
|
||||
_, certificate, caCerts, err := gopkcs12.DecodeChain([]byte(input), pass)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
|
||||
var pemData []byte
|
||||
var buf bytes.Buffer
|
||||
if err := pem.Encode(&buf, &pem.Block{
|
||||
Type: pemTypeCertificate,
|
||||
Bytes: certificate.Raw,
|
||||
}); err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
|
||||
pemData = append(pemData, buf.Bytes()...)
|
||||
|
||||
for _, ca := range caCerts {
|
||||
var buf bytes.Buffer
|
||||
if err := pem.Encode(&buf, &pem.Block{
|
||||
Type: pemTypeCertificate,
|
||||
Bytes: ca.Raw,
|
||||
}); err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
pemData = append(pemData, buf.Bytes()...)
|
||||
}
|
||||
|
||||
// try to order certificate chain. If it fails we return
|
||||
// the unordered raw pem data.
|
||||
// This fails if multiple leaf or disjunct certs are provided.
|
||||
ordered := fetchCertChains(pemData)
|
||||
|
||||
return string(ordered)
|
||||
}
|
||||
|
||||
func pkcs12cert(input string) string {
|
||||
return pkcs12certPass("", input)
|
||||
}
|
||||
|
||||
func pemToPkcs12(cert, key string) string {
|
||||
return pemToPkcs12Pass(cert, key, "")
|
||||
}
|
||||
|
||||
func pemToPkcs12Pass(cert, key, pass string) string {
|
||||
certPem, _ := pem.Decode([]byte(cert))
|
||||
|
||||
parsedCert, err := x509.ParseCertificate(certPem.Bytes)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
|
||||
return certsToPkcs12(parsedCert, key, nil, pass)
|
||||
}
|
||||
|
||||
func fullPemToPkcs12(cert, key string) string {
|
||||
return fullPemToPkcs12Pass(cert, key, "")
|
||||
}
|
||||
|
||||
func fullPemToPkcs12Pass(cert, key, pass string) string {
|
||||
certPem, rest := pem.Decode([]byte(cert))
|
||||
|
||||
parsedCert, err := x509.ParseCertificate(certPem.Bytes)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
|
||||
caCerts := make([]*x509.Certificate, 0)
|
||||
for len(rest) > 0 {
|
||||
caPem, restBytes := pem.Decode(rest)
|
||||
rest = restBytes
|
||||
|
||||
caCert, err := x509.ParseCertificate(caPem.Bytes)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
|
||||
caCerts = append(caCerts, caCert)
|
||||
}
|
||||
|
||||
return certsToPkcs12(parsedCert, key, caCerts, pass)
|
||||
}
|
||||
|
||||
func certsToPkcs12(cert *x509.Certificate, key string, caCerts []*x509.Certificate, password string) string {
|
||||
keyPem, _ := pem.Decode([]byte(key))
|
||||
parsedKey := parsePrivateKey(keyPem.Bytes)
|
||||
|
||||
pfx, err := gopkcs12.Modern.Encode(parsedKey, cert, caCerts, password)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
|
||||
return base64.StdEncoding.EncodeToString(pfx)
|
||||
}
|
||||
67
k8-operator/internal/template/template.go
Normal file
67
k8-operator/internal/template/template.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
tpl "text/template"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
)
|
||||
|
||||
var customInfisicalSecretTemplateFunctions = tpl.FuncMap{
|
||||
"pkcs12key": pkcs12key,
|
||||
"pkcs12keyPass": pkcs12keyPass,
|
||||
"pkcs12cert": pkcs12cert,
|
||||
"pkcs12certPass": pkcs12certPass,
|
||||
|
||||
"pemToPkcs12": pemToPkcs12,
|
||||
"pemToPkcs12Pass": pemToPkcs12Pass,
|
||||
"fullPemToPkcs12": fullPemToPkcs12,
|
||||
"fullPemToPkcs12Pass": fullPemToPkcs12Pass,
|
||||
|
||||
"filterPEM": filterPEM,
|
||||
"filterCertChain": filterCertChain,
|
||||
|
||||
"jwkPublicKeyPem": jwkPublicKeyPem,
|
||||
"jwkPrivateKeyPem": jwkPrivateKeyPem,
|
||||
|
||||
"toYaml": toYAML,
|
||||
"fromYaml": fromYAML,
|
||||
|
||||
"decodeBase64ToBytes": decodeBase64ToBytes,
|
||||
"encodeBase64": encodeBase64,
|
||||
}
|
||||
|
||||
const (
|
||||
errParse = "unable to parse template at key %s: %s"
|
||||
errExecute = "unable to execute template at key %s: %s"
|
||||
errDecodePKCS12WithPass = "unable to decode pkcs12 with password: %s"
|
||||
errDecodeCertWithPass = "unable to decode pkcs12 certificate with password: %s"
|
||||
errParsePrivKey = "unable to parse private key type"
|
||||
errUnmarshalJSON = "unable to unmarshal json: %s"
|
||||
errMarshalJSON = "unable to marshal json: %s"
|
||||
|
||||
pemTypeCertificate = "CERTIFICATE"
|
||||
pemTypeKey = "PRIVATE KEY"
|
||||
)
|
||||
|
||||
func InitializeTemplateFunctions() {
|
||||
templates := customInfisicalSecretTemplateFunctions
|
||||
|
||||
sprigFuncs := sprig.TxtFuncMap()
|
||||
// removed for security reasons
|
||||
delete(sprigFuncs, "env")
|
||||
delete(sprigFuncs, "expandenv")
|
||||
|
||||
for k, v := range sprigFuncs {
|
||||
// make sure we aren't overwriting any of our own functions
|
||||
_, exists := templates[k]
|
||||
if !exists {
|
||||
templates[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
customInfisicalSecretTemplateFunctions = templates
|
||||
}
|
||||
|
||||
func GetTemplateFunctions() tpl.FuncMap {
|
||||
return customInfisicalSecretTemplateFunctions
|
||||
}
|
||||
30
k8-operator/internal/template/yaml.go
Normal file
30
k8-operator/internal/template/yaml.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package template
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func toYAML(v any) string {
|
||||
data, err := yaml.Marshal(v)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
|
||||
}
|
||||
return strings.TrimSuffix(string(data), "\n")
|
||||
}
|
||||
|
||||
// fromYAML converts a YAML document into a map[string]any.
|
||||
//
|
||||
// This is not a general-purpose YAML parser, and will not parse all valid
|
||||
// YAML documents.
|
||||
func fromYAML(str string) map[string]any {
|
||||
mapData := map[string]any{}
|
||||
|
||||
if err := yaml.Unmarshal([]byte(str), &mapData); err != nil {
|
||||
panic(fmt.Sprintf("Error: %v", err))
|
||||
}
|
||||
return mapData
|
||||
}
|
||||
630
k8-operator/internal/util/auth.go
Normal file
630
k8-operator/internal/util/auth.go
Normal file
@@ -0,0 +1,630 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"errors"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
||||
authenticationv1 "k8s.io/api/authentication/v1"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
infisicalSdk "github.com/infisical/go-sdk"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
func GetServiceAccountToken(k8sClient client.Client, namespace string, serviceAccountName string, autoCreateServiceAccountToken bool, serviceAccountTokenAudiences []string, isNamespaceScoped bool) (string, error) {
|
||||
|
||||
if autoCreateServiceAccountToken {
|
||||
restClient, err := GetRestClientFromClient()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get REST client: %w", err)
|
||||
}
|
||||
|
||||
tokenRequest := &authenticationv1.TokenRequest{
|
||||
Spec: authenticationv1.TokenRequestSpec{
|
||||
ExpirationSeconds: ptr.Int64(600), // 10 minutes. the token only needs to be valid for when we do the initial k8s login.
|
||||
},
|
||||
}
|
||||
|
||||
if len(serviceAccountTokenAudiences) > 0 {
|
||||
// Conditionally add the audiences if they are specified.
|
||||
// Failing to do this causes a default audience to be used, which is not what we want if the user doesn't specify any.
|
||||
tokenRequest.Spec.Audiences = serviceAccountTokenAudiences
|
||||
}
|
||||
|
||||
result := &authenticationv1.TokenRequest{}
|
||||
err = restClient.
|
||||
Post().
|
||||
Namespace(namespace).
|
||||
Resource("serviceaccounts").
|
||||
Name(serviceAccountName).
|
||||
SubResource("token").
|
||||
Body(tokenRequest).
|
||||
Do(context.Background()).
|
||||
Into(result)
|
||||
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create token: %w", err)
|
||||
}
|
||||
|
||||
return result.Status.Token, nil
|
||||
}
|
||||
|
||||
serviceAccount := &corev1.ServiceAccount{}
|
||||
err := k8sClient.Get(context.TODO(), client.ObjectKey{Name: serviceAccountName, Namespace: namespace}, serviceAccount)
|
||||
if err != nil {
|
||||
if IsNamespaceScopedError(err, isNamespaceScoped) {
|
||||
return "", fmt.Errorf("unable to fetch service account. Your Operator is namespace scoped, and cannot read secrets outside of its namespace. Please ensure the service account is in the same namespace as the operator. [err=%v]", err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
if len(serviceAccount.Secrets) == 0 {
|
||||
return "", fmt.Errorf("no secrets found for service account %s", serviceAccountName)
|
||||
}
|
||||
|
||||
secretName := serviceAccount.Secrets[0].Name
|
||||
|
||||
secret := &corev1.Secret{}
|
||||
err = k8sClient.Get(context.TODO(), client.ObjectKey{Name: secretName, Namespace: namespace}, secret)
|
||||
if err != nil {
|
||||
if IsNamespaceScopedError(err, isNamespaceScoped) {
|
||||
return "", fmt.Errorf("unable to fetch service account token secret. Your Operator is namespace scoped, and cannot read secrets outside of its namespace. Please ensure the service account token secret is in the same namespace as the operator. [err=%v]", err)
|
||||
}
|
||||
return "", err
|
||||
}
|
||||
|
||||
token := secret.Data["token"]
|
||||
|
||||
return string(token), nil
|
||||
}
|
||||
|
||||
type AuthStrategyType string
|
||||
|
||||
var AuthStrategy = struct {
|
||||
SERVICE_TOKEN AuthStrategyType
|
||||
SERVICE_ACCOUNT AuthStrategyType
|
||||
UNIVERSAL_MACHINE_IDENTITY AuthStrategyType
|
||||
KUBERNETES_MACHINE_IDENTITY AuthStrategyType
|
||||
AWS_IAM_MACHINE_IDENTITY AuthStrategyType
|
||||
AZURE_MACHINE_IDENTITY AuthStrategyType
|
||||
GCP_ID_TOKEN_MACHINE_IDENTITY AuthStrategyType
|
||||
GCP_IAM_MACHINE_IDENTITY AuthStrategyType
|
||||
LDAP_MACHINE_IDENTITY AuthStrategyType
|
||||
}{
|
||||
SERVICE_TOKEN: "SERVICE_TOKEN",
|
||||
SERVICE_ACCOUNT: "SERVICE_ACCOUNT",
|
||||
UNIVERSAL_MACHINE_IDENTITY: "UNIVERSAL_MACHINE_IDENTITY",
|
||||
KUBERNETES_MACHINE_IDENTITY: "KUBERNETES_AUTH_MACHINE_IDENTITY",
|
||||
AWS_IAM_MACHINE_IDENTITY: "AWS_IAM_MACHINE_IDENTITY",
|
||||
AZURE_MACHINE_IDENTITY: "AZURE_MACHINE_IDENTITY",
|
||||
GCP_ID_TOKEN_MACHINE_IDENTITY: "GCP_ID_TOKEN_MACHINE_IDENTITY",
|
||||
GCP_IAM_MACHINE_IDENTITY: "GCP_IAM_MACHINE_IDENTITY",
|
||||
LDAP_MACHINE_IDENTITY: "LDAP_MACHINE_IDENTITY",
|
||||
}
|
||||
|
||||
type SecretCrdType string
|
||||
|
||||
var SecretCrd = struct {
|
||||
INFISICAL_SECRET SecretCrdType
|
||||
INFISICAL_PUSH_SECRET SecretCrdType
|
||||
INFISICAL_DYNAMIC_SECRET SecretCrdType
|
||||
}{
|
||||
INFISICAL_SECRET: "INFISICAL_SECRET",
|
||||
INFISICAL_PUSH_SECRET: "INFISICAL_PUSH_SECRET",
|
||||
INFISICAL_DYNAMIC_SECRET: "INFISICAL_DYNAMIC_SECRET",
|
||||
}
|
||||
|
||||
type SecretAuthInput struct {
|
||||
Secret interface{}
|
||||
Type SecretCrdType
|
||||
}
|
||||
|
||||
type AuthenticationDetails struct {
|
||||
AuthStrategy AuthStrategyType
|
||||
MachineIdentityScope v1alpha1.MachineIdentityScopeInWorkspace // This will only be set if a machine identity auth method is used (e.g. UniversalAuth or KubernetesAuth, etc.)
|
||||
IsMachineIdentityAuth bool
|
||||
SecretType SecretCrdType
|
||||
}
|
||||
|
||||
var ErrAuthNotApplicable = errors.New("authentication not applicable")
|
||||
|
||||
func HandleUniversalAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, isNamespaceScoped bool) (AuthenticationDetails, error) {
|
||||
|
||||
var universalAuthSpec v1alpha1.UniversalAuthDetails
|
||||
|
||||
switch secretCrd.Type {
|
||||
case SecretCrd.INFISICAL_SECRET:
|
||||
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
|
||||
}
|
||||
universalAuthSpec = infisicalSecret.Spec.Authentication.UniversalAuth
|
||||
case SecretCrd.INFISICAL_PUSH_SECRET:
|
||||
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
|
||||
}
|
||||
|
||||
universalAuthSpec = v1alpha1.UniversalAuthDetails{
|
||||
CredentialsRef: infisicalPushSecret.Spec.Authentication.UniversalAuth.CredentialsRef,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
|
||||
case SecretCrd.INFISICAL_DYNAMIC_SECRET:
|
||||
infisicalDynamicSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalDynamicSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalDynamicSecret")
|
||||
}
|
||||
|
||||
universalAuthSpec = v1alpha1.UniversalAuthDetails{
|
||||
CredentialsRef: infisicalDynamicSecret.Spec.Authentication.UniversalAuth.CredentialsRef,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
}
|
||||
|
||||
if universalAuthSpec.CredentialsRef.SecretName == "" || universalAuthSpec.CredentialsRef.SecretNamespace == "" {
|
||||
return AuthenticationDetails{}, ErrAuthNotApplicable
|
||||
}
|
||||
|
||||
universalAuthKubeSecret, err := GetInfisicalUniversalAuthFromKubeSecret(ctx, reconcilerClient, v1alpha1.KubeSecretReference{
|
||||
SecretNamespace: universalAuthSpec.CredentialsRef.SecretNamespace,
|
||||
SecretName: universalAuthSpec.CredentialsRef.SecretName,
|
||||
}, isNamespaceScoped)
|
||||
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get machine identity creds from kube secret [err=%s]", err)
|
||||
}
|
||||
|
||||
if universalAuthKubeSecret.ClientId == "" && universalAuthKubeSecret.ClientSecret == "" {
|
||||
return AuthenticationDetails{}, ErrAuthNotApplicable
|
||||
}
|
||||
|
||||
_, err = infisicalClient.Auth().UniversalAuthLogin(universalAuthKubeSecret.ClientId, universalAuthKubeSecret.ClientSecret)
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("unable to login with machine identity credentials [err=%s]", err)
|
||||
}
|
||||
|
||||
return AuthenticationDetails{
|
||||
AuthStrategy: AuthStrategy.UNIVERSAL_MACHINE_IDENTITY,
|
||||
MachineIdentityScope: universalAuthSpec.SecretsScope,
|
||||
IsMachineIdentityAuth: true,
|
||||
SecretType: secretCrd.Type,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func HandleLdapAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, isNamespaceScoped bool) (AuthenticationDetails, error) {
|
||||
|
||||
var ldapAuthSpec v1alpha1.LdapAuthDetails
|
||||
|
||||
switch secretCrd.Type {
|
||||
case SecretCrd.INFISICAL_SECRET:
|
||||
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
|
||||
}
|
||||
ldapAuthSpec = infisicalSecret.Spec.Authentication.LdapAuth
|
||||
case SecretCrd.INFISICAL_PUSH_SECRET:
|
||||
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
|
||||
}
|
||||
|
||||
ldapAuthSpec = v1alpha1.LdapAuthDetails{
|
||||
CredentialsRef: infisicalPushSecret.Spec.Authentication.LdapAuth.CredentialsRef,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
IdentityID: infisicalPushSecret.Spec.Authentication.LdapAuth.IdentityID,
|
||||
}
|
||||
|
||||
case SecretCrd.INFISICAL_DYNAMIC_SECRET:
|
||||
infisicalDynamicSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalDynamicSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalDynamicSecret")
|
||||
}
|
||||
|
||||
ldapAuthSpec = v1alpha1.LdapAuthDetails{
|
||||
CredentialsRef: infisicalDynamicSecret.Spec.Authentication.LdapAuth.CredentialsRef,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
IdentityID: infisicalDynamicSecret.Spec.Authentication.LdapAuth.IdentityID,
|
||||
}
|
||||
}
|
||||
|
||||
if ldapAuthSpec.CredentialsRef.SecretName == "" || ldapAuthSpec.CredentialsRef.SecretNamespace == "" {
|
||||
return AuthenticationDetails{}, ErrAuthNotApplicable
|
||||
}
|
||||
|
||||
ldapAuthKubeSecret, err := GetInfisicalLdapAuthFromKubeSecret(ctx, reconcilerClient, v1alpha1.KubeSecretReference{
|
||||
SecretNamespace: ldapAuthSpec.CredentialsRef.SecretNamespace,
|
||||
SecretName: ldapAuthSpec.CredentialsRef.SecretName,
|
||||
}, isNamespaceScoped)
|
||||
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get machine identity creds from kube secret [err=%s]", err)
|
||||
}
|
||||
|
||||
if ldapAuthKubeSecret.Username == "" && ldapAuthKubeSecret.Password == "" {
|
||||
return AuthenticationDetails{}, ErrAuthNotApplicable
|
||||
}
|
||||
|
||||
_, err = infisicalClient.Auth().LdapAuthLogin(ldapAuthSpec.IdentityID, ldapAuthKubeSecret.Username, ldapAuthKubeSecret.Password)
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("unable to login with machine identity credentials [err=%s]", err)
|
||||
}
|
||||
|
||||
return AuthenticationDetails{
|
||||
AuthStrategy: AuthStrategy.LDAP_MACHINE_IDENTITY,
|
||||
MachineIdentityScope: ldapAuthSpec.SecretsScope,
|
||||
IsMachineIdentityAuth: true,
|
||||
SecretType: secretCrd.Type,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func HandleKubernetesAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, isNamespaceScoped bool) (AuthenticationDetails, error) {
|
||||
var kubernetesAuthSpec v1alpha1.KubernetesAuthDetails
|
||||
|
||||
switch secretCrd.Type {
|
||||
case SecretCrd.INFISICAL_SECRET:
|
||||
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
|
||||
}
|
||||
kubernetesAuthSpec = infisicalSecret.Spec.Authentication.KubernetesAuth
|
||||
case SecretCrd.INFISICAL_PUSH_SECRET:
|
||||
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
|
||||
}
|
||||
kubernetesAuthSpec = v1alpha1.KubernetesAuthDetails{
|
||||
IdentityID: infisicalPushSecret.Spec.Authentication.KubernetesAuth.IdentityID,
|
||||
ServiceAccountRef: v1alpha1.KubernetesServiceAccountRef{
|
||||
Namespace: infisicalPushSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Namespace,
|
||||
Name: infisicalPushSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Name,
|
||||
},
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
AutoCreateServiceAccountToken: infisicalPushSecret.Spec.Authentication.KubernetesAuth.AutoCreateServiceAccountToken,
|
||||
ServiceAccountTokenAudiences: infisicalPushSecret.Spec.Authentication.KubernetesAuth.ServiceAccountTokenAudiences,
|
||||
}
|
||||
|
||||
case SecretCrd.INFISICAL_DYNAMIC_SECRET:
|
||||
infisicalDynamicSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalDynamicSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalDynamicSecret")
|
||||
}
|
||||
|
||||
kubernetesAuthSpec = v1alpha1.KubernetesAuthDetails{
|
||||
IdentityID: infisicalDynamicSecret.Spec.Authentication.KubernetesAuth.IdentityID,
|
||||
ServiceAccountRef: v1alpha1.KubernetesServiceAccountRef{
|
||||
Namespace: infisicalDynamicSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Namespace,
|
||||
Name: infisicalDynamicSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Name,
|
||||
},
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
AutoCreateServiceAccountToken: infisicalDynamicSecret.Spec.Authentication.KubernetesAuth.AutoCreateServiceAccountToken,
|
||||
ServiceAccountTokenAudiences: infisicalDynamicSecret.Spec.Authentication.KubernetesAuth.ServiceAccountTokenAudiences,
|
||||
}
|
||||
}
|
||||
|
||||
if kubernetesAuthSpec.IdentityID == "" {
|
||||
return AuthenticationDetails{}, ErrAuthNotApplicable
|
||||
}
|
||||
|
||||
serviceAccountToken, err := GetServiceAccountToken(
|
||||
reconcilerClient,
|
||||
kubernetesAuthSpec.ServiceAccountRef.Namespace,
|
||||
kubernetesAuthSpec.ServiceAccountRef.Name,
|
||||
kubernetesAuthSpec.AutoCreateServiceAccountToken,
|
||||
kubernetesAuthSpec.ServiceAccountTokenAudiences,
|
||||
isNamespaceScoped,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("unable to get service account token [err=%s]", err)
|
||||
}
|
||||
|
||||
_, err = infisicalClient.Auth().KubernetesRawServiceAccountTokenLogin(kubernetesAuthSpec.IdentityID, serviceAccountToken)
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("unable to login with Kubernetes native auth [err=%s]", err)
|
||||
}
|
||||
|
||||
return AuthenticationDetails{
|
||||
AuthStrategy: AuthStrategy.KUBERNETES_MACHINE_IDENTITY,
|
||||
MachineIdentityScope: kubernetesAuthSpec.SecretsScope,
|
||||
IsMachineIdentityAuth: true,
|
||||
SecretType: secretCrd.Type,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func HandleAwsIamAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, _ bool) (AuthenticationDetails, error) {
|
||||
awsIamAuthSpec := v1alpha1.AWSIamAuthDetails{}
|
||||
|
||||
switch secretCrd.Type {
|
||||
case SecretCrd.INFISICAL_SECRET:
|
||||
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
|
||||
}
|
||||
|
||||
awsIamAuthSpec = infisicalSecret.Spec.Authentication.AwsIamAuth
|
||||
case SecretCrd.INFISICAL_PUSH_SECRET:
|
||||
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
|
||||
}
|
||||
|
||||
awsIamAuthSpec = v1alpha1.AWSIamAuthDetails{
|
||||
IdentityID: infisicalPushSecret.Spec.Authentication.AwsIamAuth.IdentityID,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
|
||||
case SecretCrd.INFISICAL_DYNAMIC_SECRET:
|
||||
infisicalDynamicSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalDynamicSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalDynamicSecret")
|
||||
}
|
||||
|
||||
awsIamAuthSpec = v1alpha1.AWSIamAuthDetails{
|
||||
IdentityID: infisicalDynamicSecret.Spec.Authentication.AwsIamAuth.IdentityID,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
}
|
||||
|
||||
if awsIamAuthSpec.IdentityID == "" {
|
||||
return AuthenticationDetails{}, ErrAuthNotApplicable
|
||||
}
|
||||
|
||||
_, err := infisicalClient.Auth().AwsIamAuthLogin(awsIamAuthSpec.IdentityID)
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("unable to login with AWS IAM auth [err=%s]", err)
|
||||
}
|
||||
|
||||
return AuthenticationDetails{
|
||||
AuthStrategy: AuthStrategy.AWS_IAM_MACHINE_IDENTITY,
|
||||
MachineIdentityScope: awsIamAuthSpec.SecretsScope,
|
||||
IsMachineIdentityAuth: true,
|
||||
SecretType: secretCrd.Type,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func HandleAzureAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, _ bool) (AuthenticationDetails, error) {
|
||||
azureAuthSpec := v1alpha1.AzureAuthDetails{}
|
||||
|
||||
switch secretCrd.Type {
|
||||
case SecretCrd.INFISICAL_SECRET:
|
||||
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
|
||||
}
|
||||
|
||||
azureAuthSpec = infisicalSecret.Spec.Authentication.AzureAuth
|
||||
|
||||
case SecretCrd.INFISICAL_PUSH_SECRET:
|
||||
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
|
||||
}
|
||||
|
||||
azureAuthSpec = v1alpha1.AzureAuthDetails{
|
||||
IdentityID: infisicalPushSecret.Spec.Authentication.AzureAuth.IdentityID,
|
||||
Resource: infisicalPushSecret.Spec.Authentication.AzureAuth.Resource,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
|
||||
case SecretCrd.INFISICAL_DYNAMIC_SECRET:
|
||||
infisicalDynamicSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalDynamicSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalDynamicSecret")
|
||||
}
|
||||
|
||||
azureAuthSpec = v1alpha1.AzureAuthDetails{
|
||||
IdentityID: infisicalDynamicSecret.Spec.Authentication.AzureAuth.IdentityID,
|
||||
Resource: infisicalDynamicSecret.Spec.Authentication.AzureAuth.Resource,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
}
|
||||
|
||||
if azureAuthSpec.IdentityID == "" {
|
||||
return AuthenticationDetails{}, ErrAuthNotApplicable
|
||||
}
|
||||
|
||||
_, err := infisicalClient.Auth().AzureAuthLogin(azureAuthSpec.IdentityID, azureAuthSpec.Resource) // If resource is empty(""), it will default to "https://management.azure.com/" in the SDK.
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("unable to login with Azure auth [err=%s]", err)
|
||||
}
|
||||
|
||||
return AuthenticationDetails{
|
||||
AuthStrategy: AuthStrategy.AZURE_MACHINE_IDENTITY,
|
||||
MachineIdentityScope: azureAuthSpec.SecretsScope,
|
||||
IsMachineIdentityAuth: true,
|
||||
SecretType: secretCrd.Type,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func HandleGcpIdTokenAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, _ bool) (AuthenticationDetails, error) {
|
||||
gcpIdTokenSpec := v1alpha1.GCPIdTokenAuthDetails{}
|
||||
|
||||
switch secretCrd.Type {
|
||||
case SecretCrd.INFISICAL_SECRET:
|
||||
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
|
||||
}
|
||||
|
||||
gcpIdTokenSpec = infisicalSecret.Spec.Authentication.GcpIdTokenAuth
|
||||
case SecretCrd.INFISICAL_PUSH_SECRET:
|
||||
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
|
||||
}
|
||||
|
||||
gcpIdTokenSpec = v1alpha1.GCPIdTokenAuthDetails{
|
||||
IdentityID: infisicalPushSecret.Spec.Authentication.GcpIdTokenAuth.IdentityID,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
|
||||
case SecretCrd.INFISICAL_DYNAMIC_SECRET:
|
||||
infisicalDynamicSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalDynamicSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalDynamicSecret")
|
||||
}
|
||||
|
||||
gcpIdTokenSpec = v1alpha1.GCPIdTokenAuthDetails{
|
||||
IdentityID: infisicalDynamicSecret.Spec.Authentication.GcpIdTokenAuth.IdentityID,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
}
|
||||
|
||||
if gcpIdTokenSpec.IdentityID == "" {
|
||||
return AuthenticationDetails{}, ErrAuthNotApplicable
|
||||
}
|
||||
|
||||
_, err := infisicalClient.Auth().GcpIdTokenAuthLogin(gcpIdTokenSpec.IdentityID)
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("unable to login with GCP Id Token auth [err=%s]", err)
|
||||
}
|
||||
|
||||
return AuthenticationDetails{
|
||||
AuthStrategy: AuthStrategy.GCP_ID_TOKEN_MACHINE_IDENTITY,
|
||||
MachineIdentityScope: gcpIdTokenSpec.SecretsScope,
|
||||
IsMachineIdentityAuth: true,
|
||||
SecretType: secretCrd.Type,
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func HandleGcpIamAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, _ bool) (AuthenticationDetails, error) {
|
||||
gcpIamSpec := v1alpha1.GcpIamAuthDetails{}
|
||||
|
||||
switch secretCrd.Type {
|
||||
case SecretCrd.INFISICAL_SECRET:
|
||||
infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
|
||||
}
|
||||
|
||||
gcpIamSpec = infisicalSecret.Spec.Authentication.GcpIamAuth
|
||||
case SecretCrd.INFISICAL_PUSH_SECRET:
|
||||
infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret")
|
||||
}
|
||||
|
||||
gcpIamSpec = v1alpha1.GcpIamAuthDetails{
|
||||
IdentityID: infisicalPushSecret.Spec.Authentication.GcpIamAuth.IdentityID,
|
||||
ServiceAccountKeyFilePath: infisicalPushSecret.Spec.Authentication.GcpIamAuth.ServiceAccountKeyFilePath,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
|
||||
case SecretCrd.INFISICAL_DYNAMIC_SECRET:
|
||||
infisicalDynamicSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalDynamicSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalDynamicSecret")
|
||||
}
|
||||
|
||||
gcpIamSpec = v1alpha1.GcpIamAuthDetails{
|
||||
IdentityID: infisicalDynamicSecret.Spec.Authentication.GcpIamAuth.IdentityID,
|
||||
ServiceAccountKeyFilePath: infisicalDynamicSecret.Spec.Authentication.GcpIamAuth.ServiceAccountKeyFilePath,
|
||||
SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{},
|
||||
}
|
||||
}
|
||||
|
||||
if gcpIamSpec.IdentityID == "" && gcpIamSpec.ServiceAccountKeyFilePath == "" {
|
||||
return AuthenticationDetails{}, ErrAuthNotApplicable
|
||||
}
|
||||
|
||||
_, err := infisicalClient.Auth().GcpIamAuthLogin(gcpIamSpec.IdentityID, gcpIamSpec.ServiceAccountKeyFilePath)
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("unable to login with GCP IAM auth [err=%s]", err)
|
||||
}
|
||||
|
||||
return AuthenticationDetails{
|
||||
AuthStrategy: AuthStrategy.GCP_IAM_MACHINE_IDENTITY,
|
||||
MachineIdentityScope: gcpIamSpec.SecretsScope,
|
||||
IsMachineIdentityAuth: true,
|
||||
SecretType: secretCrd.Type,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func HandleAuthentication(ctx context.Context, secretInput SecretAuthInput, reconcilerClient client.Client, infisicalClient infisicalSdk.InfisicalClientInterface, isNamespaceScoped bool) (AuthenticationDetails, error) {
|
||||
|
||||
// We only support legacy auth for InfisicalSecret CRD
|
||||
if secretInput.Type == SecretCrd.INFISICAL_SECRET {
|
||||
infisicalSecret, ok := secretInput.Secret.(v1alpha1.InfisicalSecret)
|
||||
|
||||
if !ok {
|
||||
return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret")
|
||||
}
|
||||
|
||||
// ? Legacy support, service token auth
|
||||
infisicalToken, err := GetInfisicalTokenFromKubeSecret(ctx, reconcilerClient, infisicalSecret)
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err)
|
||||
}
|
||||
if infisicalToken != "" {
|
||||
infisicalClient.Auth().SetAccessToken(infisicalToken)
|
||||
return AuthenticationDetails{AuthStrategy: AuthStrategy.SERVICE_TOKEN}, nil
|
||||
}
|
||||
|
||||
// ? Legacy support, service account auth
|
||||
serviceAccountCreds, err := GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx, reconcilerClient, infisicalSecret)
|
||||
if err != nil {
|
||||
return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err)
|
||||
}
|
||||
|
||||
if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" {
|
||||
infisicalClient.Auth().SetAccessToken(serviceAccountCreds.AccessKey)
|
||||
return AuthenticationDetails{AuthStrategy: AuthStrategy.SERVICE_ACCOUNT}, nil
|
||||
}
|
||||
}
|
||||
|
||||
authStrategies := map[AuthStrategyType]func(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface, isNamespaceScoped bool) (AuthenticationDetails, error){
|
||||
AuthStrategy.UNIVERSAL_MACHINE_IDENTITY: HandleUniversalAuth,
|
||||
AuthStrategy.KUBERNETES_MACHINE_IDENTITY: HandleKubernetesAuth,
|
||||
AuthStrategy.AWS_IAM_MACHINE_IDENTITY: HandleAwsIamAuth,
|
||||
AuthStrategy.AZURE_MACHINE_IDENTITY: HandleAzureAuth,
|
||||
AuthStrategy.GCP_ID_TOKEN_MACHINE_IDENTITY: HandleGcpIdTokenAuth,
|
||||
AuthStrategy.GCP_IAM_MACHINE_IDENTITY: HandleGcpIamAuth,
|
||||
AuthStrategy.LDAP_MACHINE_IDENTITY: HandleLdapAuth,
|
||||
}
|
||||
|
||||
for authStrategy, authHandler := range authStrategies {
|
||||
authDetails, err := authHandler(ctx, reconcilerClient, secretInput, infisicalClient, isNamespaceScoped)
|
||||
|
||||
if err == nil {
|
||||
return authDetails, nil
|
||||
}
|
||||
|
||||
if !errors.Is(err, ErrAuthNotApplicable) {
|
||||
return AuthenticationDetails{}, fmt.Errorf("authentication failed for strategy [%s] [err=%w]", authStrategy, err)
|
||||
}
|
||||
}
|
||||
|
||||
return AuthenticationDetails{}, fmt.Errorf("no authentication method provided")
|
||||
|
||||
}
|
||||
59
k8-operator/internal/util/handler.go
Normal file
59
k8-operator/internal/util/handler.go
Normal file
@@ -0,0 +1,59 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math/rand"
|
||||
"time"
|
||||
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/util/workqueue"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
"sigs.k8s.io/controller-runtime/pkg/event"
|
||||
"sigs.k8s.io/controller-runtime/pkg/reconcile"
|
||||
)
|
||||
|
||||
// computeMaxJitterDuration returns a random duration between 0 and max.
|
||||
// This is useful for introducing jitter to event processing.
|
||||
func computeMaxJitterDuration(max time.Duration) (time.Duration, time.Duration) {
|
||||
if max <= 0 {
|
||||
return 0, 0
|
||||
}
|
||||
jitter := time.Duration(rand.Int63n(int64(max)))
|
||||
return max, jitter
|
||||
}
|
||||
|
||||
// EnqueueDelayedEventHandler enqueues reconcile requests with a random delay (jitter)
|
||||
// to spread the load and avoid thundering herd issues.
|
||||
type EnqueueDelayedEventHandler struct {
|
||||
Delay time.Duration
|
||||
}
|
||||
|
||||
func (e *EnqueueDelayedEventHandler) Create(_ context.Context, _ event.TypedCreateEvent[client.Object], _ workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
}
|
||||
|
||||
func (e *EnqueueDelayedEventHandler) Update(_ context.Context, _ event.TypedUpdateEvent[client.Object], _ workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
}
|
||||
|
||||
func (e *EnqueueDelayedEventHandler) Delete(_ context.Context, _ event.TypedDeleteEvent[client.Object], _ workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
}
|
||||
|
||||
func (e *EnqueueDelayedEventHandler) Generic(_ context.Context, evt event.TypedGenericEvent[client.Object], q workqueue.TypedRateLimitingInterface[reconcile.Request]) {
|
||||
if evt.Object == nil {
|
||||
return
|
||||
}
|
||||
|
||||
req := reconcile.Request{
|
||||
NamespacedName: types.NamespacedName{
|
||||
Namespace: evt.Object.GetNamespace(),
|
||||
Name: evt.Object.GetName(),
|
||||
},
|
||||
}
|
||||
|
||||
_, delay := computeMaxJitterDuration(e.Delay)
|
||||
|
||||
if delay > 0 {
|
||||
q.AddAfter(req, delay)
|
||||
} else {
|
||||
q.Add(req)
|
||||
}
|
||||
}
|
||||
60
k8-operator/internal/util/helpers.go
Normal file
60
k8-operator/internal/util/helpers.go
Normal file
@@ -0,0 +1,60 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ConvertIntervalToDuration(resyncInterval *string) (time.Duration, error) {
|
||||
|
||||
if resyncInterval == nil || *resyncInterval == "" {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
length := len(*resyncInterval)
|
||||
if length < 2 {
|
||||
return 0, fmt.Errorf("invalid format")
|
||||
}
|
||||
|
||||
unit := (*resyncInterval)[length-1:]
|
||||
numberPart := (*resyncInterval)[:length-1]
|
||||
|
||||
number, err := strconv.Atoi(numberPart)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
switch unit {
|
||||
case "s":
|
||||
if number < 5 {
|
||||
return 0, fmt.Errorf("resync interval must be at least 5 seconds")
|
||||
}
|
||||
return time.Duration(number) * time.Second, nil
|
||||
case "m":
|
||||
return time.Duration(number) * time.Minute, nil
|
||||
case "h":
|
||||
return time.Duration(number) * time.Hour, nil
|
||||
case "d":
|
||||
return time.Duration(number) * 24 * time.Hour, nil
|
||||
case "w":
|
||||
return time.Duration(number) * 7 * 24 * time.Hour, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("invalid time unit")
|
||||
}
|
||||
}
|
||||
|
||||
func AppendAPIEndpoint(address string) string {
|
||||
if strings.HasSuffix(address, "/api") {
|
||||
return address
|
||||
}
|
||||
if address[len(address)-1] == '/' {
|
||||
return address + "api"
|
||||
}
|
||||
return address + "/api"
|
||||
}
|
||||
|
||||
func IsNamespaceScopedError(err error, isNamespaceScoped bool) bool {
|
||||
return isNamespaceScoped && err != nil && strings.Contains(err.Error(), "unknown namespace for the cache")
|
||||
}
|
||||
183
k8-operator/internal/util/kubernetes.go
Normal file
183
k8-operator/internal/util/kubernetes.go
Normal file
@@ -0,0 +1,183 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/constants"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/model"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
k8Errors "k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/types"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/clientcmd"
|
||||
"sigs.k8s.io/controller-runtime/pkg/client"
|
||||
)
|
||||
|
||||
const INFISICAL_MACHINE_IDENTITY_CLIENT_ID = "clientId"
|
||||
const INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET = "clientSecret"
|
||||
|
||||
const INFISICAL_MACHINE_IDENTITY_LDAP_USERNAME = "username"
|
||||
const INFISICAL_MACHINE_IDENTITY_LDAP_PASSWORD = "password"
|
||||
|
||||
func GetKubeSecretByNamespacedName(ctx context.Context, reconcilerClient client.Client, namespacedName types.NamespacedName) (*corev1.Secret, error) {
|
||||
kubeSecret := &corev1.Secret{}
|
||||
err := reconcilerClient.Get(ctx, namespacedName, kubeSecret)
|
||||
if err != nil {
|
||||
kubeSecret = nil
|
||||
}
|
||||
|
||||
return kubeSecret, err
|
||||
}
|
||||
|
||||
func GetKubeConfigMapByNamespacedName(ctx context.Context, reconcilerClient client.Client, namespacedName types.NamespacedName) (*corev1.ConfigMap, error) {
|
||||
kubeConfigMap := &corev1.ConfigMap{}
|
||||
err := reconcilerClient.Get(ctx, namespacedName, kubeConfigMap)
|
||||
if err != nil {
|
||||
kubeConfigMap = nil
|
||||
}
|
||||
|
||||
return kubeConfigMap, err
|
||||
}
|
||||
|
||||
func GetInfisicalUniversalAuthFromKubeSecret(ctx context.Context, reconcilerClient client.Client, universalAuthRef v1alpha1.KubeSecretReference, isNamespaceScoped bool) (machineIdentityDetails model.UniversalAuthIdentityDetails, err error) {
|
||||
|
||||
universalAuthCredsFromKubeSecret, err := GetKubeSecretByNamespacedName(ctx, reconcilerClient, types.NamespacedName{
|
||||
Namespace: universalAuthRef.SecretNamespace,
|
||||
Name: universalAuthRef.SecretName,
|
||||
})
|
||||
|
||||
if k8Errors.IsNotFound(err) {
|
||||
return model.UniversalAuthIdentityDetails{}, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if IsNamespaceScopedError(err, isNamespaceScoped) {
|
||||
return model.UniversalAuthIdentityDetails{}, 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 model.UniversalAuthIdentityDetails{}, fmt.Errorf("something went wrong when fetching your machine identity credentials [err=%s]", err)
|
||||
}
|
||||
|
||||
clientIdFromSecret := universalAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_CLIENT_ID]
|
||||
clientSecretFromSecret := universalAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET]
|
||||
|
||||
return model.UniversalAuthIdentityDetails{ClientId: string(clientIdFromSecret), ClientSecret: string(clientSecretFromSecret)}, nil
|
||||
|
||||
}
|
||||
|
||||
func GetInfisicalLdapAuthFromKubeSecret(ctx context.Context, reconcilerClient client.Client, ldapAuthRef v1alpha1.KubeSecretReference, isNamespaceScoped bool) (machineIdentityDetails model.LdapIdentityDetails, err error) {
|
||||
|
||||
ldapAuthCredsFromKubeSecret, err := GetKubeSecretByNamespacedName(ctx, reconcilerClient, types.NamespacedName{
|
||||
Namespace: ldapAuthRef.SecretNamespace,
|
||||
Name: ldapAuthRef.SecretName,
|
||||
})
|
||||
|
||||
if k8Errors.IsNotFound(err) {
|
||||
return model.LdapIdentityDetails{}, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if IsNamespaceScopedError(err, isNamespaceScoped) {
|
||||
return model.LdapIdentityDetails{}, fmt.Errorf("unable to fetch Kubernetes secret. Your Operator is namespace scoped, and cannot read secrets outside of its namespace. Please ensure the secret is in the same namespace as the operator. [err=%v]", err)
|
||||
}
|
||||
return model.LdapIdentityDetails{}, fmt.Errorf("something went wrong when fetching your machine identity credentials [err=%s]", err)
|
||||
}
|
||||
|
||||
usernameFromSecret := ldapAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_LDAP_USERNAME]
|
||||
passwordFromSecret := ldapAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_LDAP_PASSWORD]
|
||||
|
||||
return model.LdapIdentityDetails{Username: string(usernameFromSecret), Password: string(passwordFromSecret)}, nil
|
||||
|
||||
}
|
||||
|
||||
func getKubeClusterConfig() (*rest.Config, error) {
|
||||
config, err := rest.InClusterConfig()
|
||||
if err != nil {
|
||||
|
||||
loadingRules := clientcmd.NewDefaultClientConfigLoadingRules()
|
||||
configOverrides := &clientcmd.ConfigOverrides{}
|
||||
kubeConfig := clientcmd.NewNonInteractiveDeferredLoadingClientConfig(loadingRules, configOverrides)
|
||||
return kubeConfig.ClientConfig()
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
func GetRestClientFromClient() (rest.Interface, error) {
|
||||
|
||||
config, err := getKubeClusterConfig()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clientset, err := kubernetes.NewForConfig(config)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return clientset.CoreV1().RESTClient(), nil
|
||||
|
||||
}
|
||||
|
||||
func GetInfisicalTokenFromKubeSecret(ctx context.Context, reconcilerClient client.Client, 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 := GetKubeSecretByNamespacedName(ctx, reconcilerClient, types.NamespacedName{
|
||||
Namespace: secretNamespace,
|
||||
Name: secretName,
|
||||
})
|
||||
|
||||
if k8Errors.IsNotFound(err) || (secretNamespace == "" && secretName == "") {
|
||||
return "", 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.TokenSecretReference.SecretName, infisicalSecret.Spec.TokenSecretReference.SecretNamespace, err)
|
||||
}
|
||||
|
||||
infisicalServiceToken := tokenSecret.Data[constants.INFISICAL_TOKEN_SECRET_KEY_NAME]
|
||||
|
||||
return strings.Replace(string(infisicalServiceToken), " ", "", -1), nil
|
||||
}
|
||||
|
||||
func GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx context.Context, reconcilerClient client.Client, infisicalSecret v1alpha1.InfisicalSecret) (serviceAccountDetails model.ServiceAccountDetails, err error) {
|
||||
|
||||
secretNamespace := infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretNamespace
|
||||
secretName := infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretName
|
||||
|
||||
serviceAccountCredsFromKubeSecret, err := GetKubeSecretByNamespacedName(ctx, reconcilerClient, types.NamespacedName{
|
||||
Namespace: secretNamespace,
|
||||
Name: secretName,
|
||||
})
|
||||
|
||||
if k8Errors.IsNotFound(err) || (secretNamespace == "" && secretName == "") {
|
||||
return model.ServiceAccountDetails{}, nil
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
15
k8-operator/internal/util/models.go
Normal file
15
k8-operator/internal/util/models.go
Normal file
@@ -0,0 +1,15 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/packages/util/sse"
|
||||
infisicalSdk "github.com/infisical/go-sdk"
|
||||
)
|
||||
|
||||
type ResourceVariables struct {
|
||||
InfisicalClient infisicalSdk.InfisicalClientInterface
|
||||
CancelCtx context.CancelFunc
|
||||
AuthDetails AuthenticationDetails
|
||||
ServerSentEvents *sse.ConnectionRegistry
|
||||
}
|
||||
186
k8-operator/internal/util/secrets.go
Normal file
186
k8-operator/internal/util/secrets.go
Normal file
@@ -0,0 +1,186 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/api"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/model"
|
||||
"github.com/go-resty/resty/v2"
|
||||
infisical "github.com/infisical/go-sdk"
|
||||
)
|
||||
|
||||
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 GetPlainTextSecretsViaMachineIdentity(infisicalClient infisical.InfisicalClientInterface, secretScope v1alpha1.MachineIdentityScopeInWorkspace) ([]model.SingleEnvironmentVariable, error) {
|
||||
|
||||
secrets, err := infisicalClient.Secrets().List(infisical.ListSecretsOptions{
|
||||
ProjectSlug: secretScope.ProjectSlug,
|
||||
Environment: secretScope.EnvSlug,
|
||||
Recursive: secretScope.Recursive,
|
||||
SecretPath: secretScope.SecretsPath,
|
||||
IncludeImports: true,
|
||||
ExpandSecretReferences: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to get secrets. [err=%v]", err)
|
||||
}
|
||||
|
||||
var environmentVariables []model.SingleEnvironmentVariable
|
||||
|
||||
for _, secret := range secrets {
|
||||
|
||||
environmentVariables = append(environmentVariables, model.SingleEnvironmentVariable{
|
||||
Key: secret.SecretKey,
|
||||
Value: secret.SecretValue,
|
||||
Type: secret.Type,
|
||||
ID: secret.ID,
|
||||
SecretPath: secret.SecretPath,
|
||||
})
|
||||
}
|
||||
|
||||
return environmentVariables, nil
|
||||
}
|
||||
|
||||
func GetPlainTextSecretsViaServiceToken(infisicalClient infisical.InfisicalClientInterface, fullServiceToken string, envSlug string, secretPath string, recursive bool) ([]model.SingleEnvironmentVariable, error) {
|
||||
serviceTokenParts := strings.SplitN(fullServiceToken, ".", 4)
|
||||
if len(serviceTokenParts) < 4 {
|
||||
return nil, 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, fmt.Errorf("unable to get service token details. [err=%v]", err)
|
||||
}
|
||||
|
||||
secrets, err := infisicalClient.Secrets().List(infisical.ListSecretsOptions{
|
||||
ProjectID: serviceTokenDetails.Workspace,
|
||||
Environment: envSlug,
|
||||
Recursive: recursive,
|
||||
SecretPath: secretPath,
|
||||
IncludeImports: true,
|
||||
ExpandSecretReferences: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var environmentVariables []model.SingleEnvironmentVariable
|
||||
|
||||
for _, secret := range secrets {
|
||||
|
||||
environmentVariables = append(environmentVariables, model.SingleEnvironmentVariable{
|
||||
Key: secret.SecretKey,
|
||||
Value: secret.SecretValue,
|
||||
Type: secret.Type,
|
||||
ID: secret.ID,
|
||||
SecretPath: secret.SecretPath,
|
||||
})
|
||||
}
|
||||
|
||||
return environmentVariables, nil
|
||||
|
||||
}
|
||||
|
||||
// Fetches plaintext secrets from an API endpoint using a service account.
|
||||
// The function fetches the service account details and keys, decrypts the workspace key, fetches the encrypted secrets for the specified project and environment, and decrypts the secrets using the decrypted workspace key.
|
||||
// Returns the plaintext secrets, encrypted secrets response, and any errors that occurred during the process.
|
||||
func GetPlainTextSecretsViaServiceAccount(infisicalClient infisical.InfisicalClientInterface, serviceAccountCreds model.ServiceAccountDetails, projectId string, environmentName string) ([]model.SingleEnvironmentVariable, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient.SetAuthToken(serviceAccountCreds.AccessKey).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
serviceAccountDetails, err := api.CallGetServiceTokenAccountDetailsV2(httpClient)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account details. [err=%v]", err)
|
||||
}
|
||||
|
||||
serviceAccountKeys, err := api.CallGetServiceAccountKeysV2(httpClient, api.GetServiceAccountKeysRequest{ServiceAccountId: serviceAccountDetails.ServiceAccount.ID})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("GetPlainTextSecretsViaServiceAccount: unable to get service account key details. [err=%v]", err)
|
||||
}
|
||||
|
||||
// find key for requested project
|
||||
var workspaceServiceAccountKey api.ServiceAccountKey
|
||||
for _, serviceAccountKey := range serviceAccountKeys.ServiceAccountKeys {
|
||||
if serviceAccountKey.Workspace == projectId {
|
||||
workspaceServiceAccountKey = serviceAccountKey
|
||||
}
|
||||
}
|
||||
|
||||
if workspaceServiceAccountKey.ID == "" || workspaceServiceAccountKey.EncryptedKey == "" || workspaceServiceAccountKey.Nonce == "" || serviceAccountCreds.PublicKey == "" || serviceAccountCreds.PrivateKey == "" {
|
||||
return nil, fmt.Errorf("unable to find key for [projectId=%s] [err=%v]. Ensure that the given service account has access to given projectId", projectId, err)
|
||||
}
|
||||
|
||||
secrets, err := infisicalClient.Secrets().List(infisical.ListSecretsOptions{
|
||||
ProjectID: projectId,
|
||||
Environment: environmentName,
|
||||
Recursive: false,
|
||||
SecretPath: "/",
|
||||
IncludeImports: true,
|
||||
ExpandSecretReferences: true,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var environmentVariables []model.SingleEnvironmentVariable
|
||||
|
||||
for _, secret := range secrets {
|
||||
environmentVariables = append(environmentVariables, model.SingleEnvironmentVariable{
|
||||
Key: secret.SecretKey,
|
||||
Value: secret.SecretValue,
|
||||
Type: secret.Type,
|
||||
ID: secret.ID,
|
||||
SecretPath: secret.SecretPath,
|
||||
})
|
||||
}
|
||||
|
||||
return environmentVariables, nil
|
||||
}
|
||||
116
k8-operator/internal/util/sse/client.go
Normal file
116
k8-operator/internal/util/sse/client.go
Normal file
@@ -0,0 +1,116 @@
|
||||
package sse
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type SSEEvent struct {
|
||||
ID string
|
||||
Event string
|
||||
Data string
|
||||
}
|
||||
|
||||
// SSEClient handles SSE connections
|
||||
type SSEClient struct {
|
||||
URL string
|
||||
Client *http.Client
|
||||
LastHealthCheck time.Time
|
||||
mu *sync.Mutex // for safe concurrent access to LastHealthCheck
|
||||
}
|
||||
|
||||
// NewClient creates a new SSE client
|
||||
func NewClient() SSEClient {
|
||||
return SSEClient{
|
||||
mu: &sync.Mutex{},
|
||||
Client: &http.Client{
|
||||
Timeout: 0, // No timeout for streaming
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Connect establishes SSE connection and returns a channel of events
|
||||
func (c *SSEClient) Connect(req *http.Request) (<-chan SSEEvent, <-chan error, error) {
|
||||
// Set required headers for SSE
|
||||
req.Header.Set("Cache-Control", "no-cache")
|
||||
req.Header.Set("Connection", "keep-alive")
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
resp, err := c.Client.Do(req)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
resp.Body.Close()
|
||||
return nil, nil, fmt.Errorf("unexpected status code: %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
eventChan := make(chan SSEEvent)
|
||||
errorChan := make(chan error)
|
||||
|
||||
go c.stream(resp.Body, eventChan, errorChan)
|
||||
|
||||
return eventChan, errorChan, nil
|
||||
}
|
||||
|
||||
func (c *SSEClient) stream(body io.ReadCloser, eventChan chan<- SSEEvent, errorChan chan<- error) {
|
||||
defer body.Close()
|
||||
defer close(eventChan)
|
||||
defer close(errorChan)
|
||||
|
||||
scanner := bufio.NewScanner(body)
|
||||
var event SSEEvent
|
||||
|
||||
for scanner.Scan() {
|
||||
line := strings.TrimSpace(scanner.Text())
|
||||
|
||||
// End of event
|
||||
if line == "" {
|
||||
if event.Data != "" || event.Event != "" {
|
||||
if strings.TrimSpace(event.Data) == "1" {
|
||||
c.mu.Lock()
|
||||
c.LastHealthCheck = time.Now()
|
||||
c.mu.Unlock()
|
||||
} else if event.Event != "ping" {
|
||||
eventChan <- event
|
||||
}
|
||||
|
||||
event = SSEEvent{} // Reset for next event
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case strings.HasPrefix(line, "data:"):
|
||||
data := strings.TrimSpace(strings.TrimPrefix(line, "data:"))
|
||||
if event.Data != "" {
|
||||
event.Data += "\n"
|
||||
}
|
||||
event.Data += data
|
||||
|
||||
case strings.HasPrefix(line, "event:"):
|
||||
event.Event = strings.TrimSpace(strings.TrimPrefix(line, "event:"))
|
||||
|
||||
case strings.HasPrefix(line, "id:"):
|
||||
event.ID = strings.TrimSpace(strings.TrimPrefix(line, "id:"))
|
||||
|
||||
case strings.HasPrefix(line, "retry:"):
|
||||
// Optional: parse and apply retry interval here
|
||||
|
||||
case strings.HasPrefix(line, ":"):
|
||||
// Comment line — ignored
|
||||
default:
|
||||
// Unknown line format — can log/debug if needed
|
||||
}
|
||||
}
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
errorChan <- err
|
||||
}
|
||||
}
|
||||
165
k8-operator/internal/util/sse/sse.go
Normal file
165
k8-operator/internal/util/sse/sse.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package sse
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type ConnectionMeta struct {
|
||||
EventChan <-chan SSEEvent
|
||||
ErrorChan <-chan error
|
||||
LastPingAt time.Time
|
||||
Cancel context.CancelFunc
|
||||
}
|
||||
|
||||
type ConnectionRegistry struct {
|
||||
Ctx context.Context
|
||||
meta *ConnectionMeta
|
||||
client SSEClient
|
||||
mu sync.RWMutex
|
||||
|
||||
monitorCancel context.CancelFunc
|
||||
monitorCtx context.Context
|
||||
}
|
||||
|
||||
func NewConnectionRegistry(ctx context.Context) *ConnectionRegistry {
|
||||
monitorCtx, monitorCancel := context.WithCancel(ctx)
|
||||
return &ConnectionRegistry{
|
||||
Ctx: ctx,
|
||||
client: NewClient(),
|
||||
monitorCtx: monitorCtx,
|
||||
monitorCancel: monitorCancel,
|
||||
}
|
||||
}
|
||||
|
||||
// create creates a new connection
|
||||
func (r *ConnectionRegistry) create(req *http.Request) (*ConnectionMeta, error) {
|
||||
// Create new connection using provided request
|
||||
eventChan, errorChan, err := r.client.Connect(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect: %w", err)
|
||||
}
|
||||
|
||||
meta := &ConnectionMeta{
|
||||
EventChan: eventChan,
|
||||
ErrorChan: errorChan,
|
||||
LastPingAt: time.Now(),
|
||||
}
|
||||
|
||||
r.meta = meta
|
||||
|
||||
// Start cleanup monitor for this connection (NON-BLOCKING)
|
||||
go r.monitor(meta)
|
||||
|
||||
println("Creating new connection\n")
|
||||
return meta, nil
|
||||
}
|
||||
|
||||
// Get retrieves the existing connection
|
||||
func (r *ConnectionRegistry) Get() (*ConnectionMeta, bool) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.meta, r.meta != nil
|
||||
}
|
||||
|
||||
// Close closes the connection
|
||||
func (r *ConnectionRegistry) Close() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.meta != nil {
|
||||
if r.meta.Cancel != nil {
|
||||
r.meta.Cancel()
|
||||
}
|
||||
r.meta = nil
|
||||
}
|
||||
|
||||
// Cancel the monitor
|
||||
if r.monitorCancel != nil {
|
||||
r.monitorCancel()
|
||||
}
|
||||
}
|
||||
|
||||
// IsConnected returns whether there's an active connection
|
||||
func (r *ConnectionRegistry) IsConnected() bool {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
return r.meta != nil
|
||||
}
|
||||
|
||||
// UpdateLastPing updates the last ping time
|
||||
func (r *ConnectionRegistry) UpdateLastPing() {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
if r.meta != nil {
|
||||
r.meta.LastPingAt = time.Now()
|
||||
}
|
||||
}
|
||||
|
||||
// monitor watches for connection closure and cleans up
|
||||
func (r *ConnectionRegistry) monitor(meta *ConnectionMeta) {
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-r.monitorCtx.Done():
|
||||
// Context cancelled, exit monitor
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
r.mu.RLock()
|
||||
currentMeta := r.meta
|
||||
r.mu.RUnlock()
|
||||
|
||||
// Check if this monitor is still relevant
|
||||
if currentMeta != meta {
|
||||
// This connection has been replaced, exit monitor
|
||||
return
|
||||
}
|
||||
|
||||
if currentMeta != nil && time.Since(currentMeta.LastPingAt) > 2*time.Minute {
|
||||
fmt.Println("Last ping was more than 2 minutes ago, closing connection")
|
||||
r.mu.Lock()
|
||||
if r.meta == meta { // Double-check under lock
|
||||
if r.meta.Cancel != nil {
|
||||
r.meta.Cancel()
|
||||
}
|
||||
r.meta = nil
|
||||
}
|
||||
r.mu.Unlock()
|
||||
return // Exit monitor after cleanup
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Subscribe provides a convenient way to get events from the connection
|
||||
func (r *ConnectionRegistry) Subscribe(build func() (*http.Request, error)) (<-chan SSEEvent, <-chan error, error) {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
|
||||
// Get existing connection if available
|
||||
if r.meta != nil {
|
||||
return r.meta.EventChan, r.meta.ErrorChan, nil
|
||||
}
|
||||
|
||||
req, err := build()
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
// Create new connection if none exists
|
||||
meta, err := r.create(req)
|
||||
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return meta.EventChan, meta.ErrorChan, nil
|
||||
}
|
||||
40
k8-operator/internal/util/time.go
Normal file
40
k8-operator/internal/util/time.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
func ConvertResyncIntervalToDuration(resyncInterval string) (time.Duration, error) {
|
||||
length := len(resyncInterval)
|
||||
if length < 2 {
|
||||
return 0, fmt.Errorf("invalid format")
|
||||
}
|
||||
|
||||
unit := resyncInterval[length-1:]
|
||||
numberPart := resyncInterval[:length-1]
|
||||
|
||||
number, err := strconv.Atoi(numberPart)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
switch unit {
|
||||
case "s":
|
||||
if number < 5 {
|
||||
return 0, fmt.Errorf("resync interval must be at least 5 seconds")
|
||||
}
|
||||
return time.Duration(number) * time.Second, nil
|
||||
case "m":
|
||||
return time.Duration(number) * time.Minute, nil
|
||||
case "h":
|
||||
return time.Duration(number) * time.Hour, nil
|
||||
case "d":
|
||||
return time.Duration(number) * 24 * time.Hour, nil
|
||||
case "w":
|
||||
return time.Duration(number) * 7 * 24 * time.Hour, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("invalid time unit")
|
||||
}
|
||||
}
|
||||
44
k8-operator/internal/util/workspace.go
Normal file
44
k8-operator/internal/util/workspace.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package util
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/Infisical/infisical/k8-operator/internal/api"
|
||||
"github.com/Infisical/infisical/k8-operator/internal/model"
|
||||
"github.com/go-resty/resty/v2"
|
||||
)
|
||||
|
||||
func GetProjectByID(accessToken string, projectId string) (model.Project, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient.
|
||||
SetAuthScheme("Bearer").
|
||||
SetAuthToken(accessToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
projectDetails, err := api.CallGetProjectByID(httpClient, api.GetProjectByIDRequest{
|
||||
ProjectID: projectId,
|
||||
})
|
||||
if err != nil {
|
||||
return model.Project{}, fmt.Errorf("unable to get project by slug. [err=%v]", err)
|
||||
}
|
||||
|
||||
return projectDetails.Project, nil
|
||||
}
|
||||
|
||||
func GetProjectBySlug(accessToken string, projectSlug string) (model.Project, error) {
|
||||
httpClient := resty.New()
|
||||
httpClient.
|
||||
SetAuthScheme("Bearer").
|
||||
SetAuthToken(accessToken).
|
||||
SetHeader("Accept", "application/json")
|
||||
|
||||
project, err := api.CallGetProjectByIDv2(httpClient, api.GetProjectByIDRequest{
|
||||
ProjectID: projectSlug,
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return model.Project{}, fmt.Errorf("unable to get project by slug. [err=%v]", err)
|
||||
}
|
||||
|
||||
return project, nil
|
||||
}
|
||||
Reference in New Issue
Block a user