mirror of
https://github.com/hanzoai/kms-operator.git
synced 2026-10-30 18:32:57 +00:00
Modules declare go 1.27.1 and the build images follow. go fix applied the modernizers: promoted-field keys in struct literals, maps.Copy, any and range over int. omitzero is not applied, since it changes what the JSON says. Co-authored-by: Hanzo Dev <[email protected]>
391 lines
14 KiB
Go
391 lines
14 KiB
Go
// Package controllers/kmspushsecret_helper.go: write-side reconciler.
|
|
//
|
|
// Pushes a Kubernetes Secret + (optional) generator output to the
|
|
// canonical luxfi/kms surface as one secret per key. The canonical
|
|
// surface has no list/discovery endpoint and no opaque secret ID —
|
|
// every value is addressed by exact (org, env, path, name).
|
|
//
|
|
// Reconciler model:
|
|
//
|
|
// - status.ManagedSecrets[name] = name (the names this CR has
|
|
// pushed; values are the same — the field is a Set).
|
|
// - For each desired key: GET. On 404, POST (create). On 200, PATCH
|
|
// with If-Match version CAS only if the new value differs.
|
|
// - Drift: any name in status.ManagedSecrets that is no longer in
|
|
// the desired set is DELETEd from KMS, and dropped from the map.
|
|
//
|
|
// All input/output passes the kmsapi control-byte filter; oversize
|
|
// scopes (org/env/path/name) are rejected before any URL is built.
|
|
package controllers
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"strings"
|
|
tpl "text/template"
|
|
|
|
"github.com/go-logr/logr"
|
|
corev1 "k8s.io/api/core/v1"
|
|
k8Errors "k8s.io/apimachinery/pkg/api/errors"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
|
|
secretsv1 "github.com/hanzoai/kms-operator/api/v1"
|
|
"github.com/hanzoai/kms-operator/packages/api"
|
|
"github.com/hanzoai/kms-operator/packages/constants"
|
|
generatorUtil "github.com/hanzoai/kms-operator/packages/generator"
|
|
"github.com/hanzoai/kms-operator/packages/kmsapi"
|
|
"github.com/hanzoai/kms-operator/packages/model"
|
|
"github.com/hanzoai/kms-operator/packages/template"
|
|
"github.com/hanzoai/kms-operator/packages/util"
|
|
)
|
|
|
|
func (r *KMSPushSecretReconciler) handleAuthentication(
|
|
ctx context.Context,
|
|
kmsSecret secretsv1.KMSPushSecret,
|
|
host string,
|
|
kmsClient kmsapi.Transport,
|
|
) (util.AuthenticationDetails, error) {
|
|
|
|
authDetails, err := util.HandleUniversalAuth(ctx, r.Client, util.SecretAuthInput{
|
|
Secret: kmsSecret,
|
|
Type: util.SecretCrd.KMS_PUSH_SECRET,
|
|
}, host, kmsClient)
|
|
|
|
if err == nil {
|
|
return authDetails, nil
|
|
}
|
|
if errors.Is(err, util.ErrAuthNotApplicable) {
|
|
return util.AuthenticationDetails{}, errors.New(
|
|
"no authentication method provided — KMSPushSecret.spec.authentication.universalAuth.credentialsRef is required",
|
|
)
|
|
}
|
|
return util.AuthenticationDetails{}, err
|
|
}
|
|
|
|
func (r *KMSPushSecretReconciler) getKMSCaCertificateFromKubeSecret(
|
|
ctx context.Context,
|
|
kmsSecret secretsv1.KMSPushSecret,
|
|
) (string, error) {
|
|
caCertificateFromKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
|
|
Namespace: kmsSecret.Spec.TLS.CaRef.SecretNamespace,
|
|
Name: kmsSecret.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 {
|
|
return "", fmt.Errorf("something went wrong when fetching your CA certificate [err=%s]", err)
|
|
}
|
|
|
|
return string(caCertificateFromKubeSecret.Data[kmsSecret.Spec.TLS.CaRef.SecretKey]), nil
|
|
}
|
|
|
|
func (r *KMSPushSecretReconciler) getResourceVariables(kmsPushSecret secretsv1.KMSPushSecret) util.ResourceVariables {
|
|
if rv, ok := kmsPushSecretResourceVariablesMap[string(kmsPushSecret.UID)]; ok {
|
|
return rv
|
|
}
|
|
|
|
_, cancel := context.WithCancel(context.Background())
|
|
host := kmsPushSecret.Spec.HostAPI
|
|
if host == "" {
|
|
host = api.API_HOST_URL
|
|
}
|
|
cli := util.BuildTransport(host, api.API_CA_CERTIFICATE, api.USER_AGENT_NAME)
|
|
|
|
rv := util.ResourceVariables{
|
|
KMSClient: cli,
|
|
CancelCtx: cancel,
|
|
AuthDetails: util.AuthenticationDetails{},
|
|
}
|
|
kmsPushSecretResourceVariablesMap[string(kmsPushSecret.UID)] = rv
|
|
return rv
|
|
}
|
|
|
|
func (r *KMSPushSecretReconciler) updateResourceVariables(kmsPushSecret secretsv1.KMSPushSecret, resourceVariables util.ResourceVariables) {
|
|
kmsPushSecretResourceVariablesMap[string(kmsPushSecret.UID)] = resourceVariables
|
|
}
|
|
|
|
func (r *KMSPushSecretReconciler) processGenerators(ctx context.Context, kmsPushSecret secretsv1.KMSPushSecret) (map[string]string, error) {
|
|
|
|
processedSecrets := make(map[string]string)
|
|
|
|
if len(kmsPushSecret.Spec.Push.Generators) == 0 {
|
|
return processedSecrets, nil
|
|
}
|
|
|
|
for _, generator := range kmsPushSecret.Spec.Push.Generators {
|
|
generatorRef := generator.GeneratorRef
|
|
|
|
clusterGenerator := &secretsv1.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 == secretsv1.GeneratorKindPassword {
|
|
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 == secretsv1.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 *KMSPushSecretReconciler) processTemplatedSecrets(
|
|
kmsPushSecret secretsv1.KMSPushSecret,
|
|
kubePushSecret *corev1.Secret,
|
|
destination secretsv1.KMSPushSecretDestination,
|
|
) (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 kmsPushSecret.Spec.Push.Secret.Template == nil ||
|
|
(kmsPushSecret.Spec.Push.Secret.Template != nil && kmsPushSecret.Spec.Push.Secret.Template.IncludeAllSecrets) {
|
|
for key, value := range kubePushSecret.Data {
|
|
processedSecrets[key] = string(value)
|
|
}
|
|
}
|
|
|
|
if kmsPushSecret.Spec.Push.Secret.Template != nil &&
|
|
len(kmsPushSecret.Spec.Push.Secret.Template.Data) > 0 {
|
|
|
|
for templateKey, userTemplate := range kmsPushSecret.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
|
|
}
|
|
|
|
// ReconcileKMSPushSecret converges the KMS state with the desired set
|
|
// of secrets derived from spec.push.secret + spec.push.generators.
|
|
//
|
|
// Drift handling: any name in status.ManagedSecrets that is not in the
|
|
// fresh desired set is DELETEd from KMS so the stale value cannot be
|
|
// re-fetched by a downstream consumer. New keys are POSTed; existing
|
|
// keys are PATCHed only if the value changed (read-modify-write).
|
|
func (r *KMSPushSecretReconciler) ReconcileKMSPushSecret(
|
|
ctx context.Context,
|
|
logger logr.Logger,
|
|
kmsPushSecret secretsv1.KMSPushSecret,
|
|
) error {
|
|
|
|
resourceVariables := r.getResourceVariables(kmsPushSecret)
|
|
kmsClient := resourceVariables.KMSClient
|
|
cancelCtx := resourceVariables.CancelCtx
|
|
authDetails := resourceVariables.AuthDetails
|
|
host := api.API_HOST_URL
|
|
if h := strings.TrimSpace(kmsPushSecret.Spec.HostAPI); h != "" {
|
|
host = h
|
|
}
|
|
|
|
if authDetails.AuthStrategy == "" {
|
|
var err error
|
|
logger.V(1).Info("No authentication strategy found. Attempting to authenticate")
|
|
authDetails, err = r.handleAuthentication(ctx, kmsPushSecret, host, kmsClient)
|
|
r.SetAuthenticatedStatusCondition(ctx, &kmsPushSecret, err)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to authenticate [err=%s]", err)
|
|
}
|
|
r.updateResourceVariables(kmsPushSecret, util.ResourceVariables{
|
|
KMSClient: kmsClient,
|
|
CancelCtx: cancelCtx,
|
|
AuthDetails: authDetails,
|
|
})
|
|
}
|
|
|
|
processedSecrets := make(map[string]string)
|
|
if kmsPushSecret.Spec.Push.Secret != nil {
|
|
kubePushSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
|
|
Namespace: kmsPushSecret.Spec.Push.Secret.SecretNamespace,
|
|
Name: kmsPushSecret.Spec.Push.Secret.SecretName,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("unable to fetch kube secret [err=%s]", err)
|
|
}
|
|
|
|
processedSecrets, err = r.processTemplatedSecrets(kmsPushSecret, kubePushSecret, kmsPushSecret.Spec.Destination)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to process templated secrets [err=%s]", err)
|
|
}
|
|
}
|
|
|
|
generatorSecrets, err := r.processGenerators(ctx, kmsPushSecret)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to process generators [err=%s]", err)
|
|
}
|
|
maps.Copy(processedSecrets, generatorSecrets)
|
|
|
|
destination := kmsPushSecret.Spec.Destination
|
|
updatePolicy := kmsPushSecret.Spec.UpdatePolicy
|
|
canReplace := updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED)
|
|
|
|
var (
|
|
secretsFailedToCreate []string
|
|
secretsFailedToUpdate []string
|
|
secretsFailedToDelete []string
|
|
)
|
|
|
|
if kmsPushSecret.Status.ManagedSecrets == nil {
|
|
kmsPushSecret.Status.ManagedSecrets = make(map[string]string)
|
|
}
|
|
|
|
// 1. Create / update every desired key.
|
|
for key, value := range processedSecrets {
|
|
// Read the current value first so we know whether to POST or PATCH.
|
|
current, getErr := kmsClient.GetSecret(ctx, host, authDetails.BearerToken, destination.ProjectID, destination.EnvironmentSlug, destination.SecretsPath, key)
|
|
switch {
|
|
case errors.Is(getErr, kmsapi.ErrNotFound):
|
|
// Create.
|
|
if _, err := kmsClient.CreateSecret(ctx, host, authDetails.BearerToken, destination.ProjectID, destination.EnvironmentSlug, destination.SecretsPath, key, value); err != nil {
|
|
secretsFailedToCreate = append(secretsFailedToCreate, key)
|
|
logger.Info(fmt.Sprintf("unable to create secret [key=%s] [err=%s]", key, err))
|
|
continue
|
|
}
|
|
kmsPushSecret.Status.ManagedSecrets[key] = key
|
|
|
|
case getErr != nil:
|
|
secretsFailedToUpdate = append(secretsFailedToUpdate, key)
|
|
logger.Info(fmt.Sprintf("unable to read existing secret [key=%s] [err=%s]", key, getErr))
|
|
continue
|
|
|
|
default:
|
|
// Found. Update only if (a) we manage it, OR (b) replace
|
|
// policy is enabled. Skip silently otherwise — same
|
|
// semantics as the legacy reconciler.
|
|
_, weManageIt := kmsPushSecret.Status.ManagedSecrets[key]
|
|
mayUpdate := weManageIt || canReplace
|
|
if !mayUpdate {
|
|
continue
|
|
}
|
|
if current.Value == value {
|
|
// No-op write would still bump version unnecessarily.
|
|
kmsPushSecret.Status.ManagedSecrets[key] = key
|
|
continue
|
|
}
|
|
if _, err := kmsClient.UpdateSecret(ctx, host, authDetails.BearerToken, destination.ProjectID, destination.EnvironmentSlug, destination.SecretsPath, key, value, current.Version); err != nil {
|
|
secretsFailedToUpdate = append(secretsFailedToUpdate, key)
|
|
logger.Info(fmt.Sprintf("unable to update secret [key=%s] [err=%s]", key, err))
|
|
continue
|
|
}
|
|
kmsPushSecret.Status.ManagedSecrets[key] = key
|
|
}
|
|
}
|
|
|
|
// 2. Drift: drop any managed key that is no longer in the desired
|
|
// set. The canonical server returns 404 if it was already gone;
|
|
// treat that as success and prune the local roster.
|
|
for managedKey := range kmsPushSecret.Status.ManagedSecrets {
|
|
if _, stillDesired := processedSecrets[managedKey]; stillDesired {
|
|
continue
|
|
}
|
|
err := kmsClient.DeleteSecret(ctx, host, authDetails.BearerToken, destination.ProjectID, destination.EnvironmentSlug, destination.SecretsPath, managedKey)
|
|
if err != nil && !errors.Is(err, kmsapi.ErrNotFound) {
|
|
secretsFailedToDelete = append(secretsFailedToDelete, managedKey)
|
|
logger.Info(fmt.Sprintf("unable to delete secret [key=%s] [err=%s]", managedKey, err))
|
|
continue
|
|
}
|
|
delete(kmsPushSecret.Status.ManagedSecrets, managedKey)
|
|
}
|
|
|
|
r.SetFailedToCreateSecretsStatusCondition(ctx, &kmsPushSecret, joinIfAny("Failed to create secrets: ", secretsFailedToCreate))
|
|
r.SetFailedToUpdateSecretsStatusCondition(ctx, &kmsPushSecret, joinIfAny("Failed to update secrets: ", secretsFailedToUpdate))
|
|
r.SetFailedToDeleteSecretsStatusCondition(ctx, &kmsPushSecret, joinIfAny("Failed to delete secrets: ", secretsFailedToDelete))
|
|
// SetFailedToReplaceSecretsStatusCondition is retained for status-API
|
|
// compatibility but is no longer reachable on the canonical surface
|
|
// (no rename-by-id semantics exist).
|
|
r.SetFailedToReplaceSecretsStatusCondition(ctx, &kmsPushSecret, "")
|
|
|
|
if err := r.Client.Status().Update(ctx, &kmsPushSecret); err != nil {
|
|
return fmt.Errorf("unable to update status of KMSPushSecret [err=%s]", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *KMSPushSecretReconciler) DeleteManagedSecrets(
|
|
ctx context.Context,
|
|
logger logr.Logger,
|
|
kmsPushSecret secretsv1.KMSPushSecret,
|
|
) error {
|
|
if kmsPushSecret.Spec.DeletionPolicy != string(constants.PUSH_SECRET_DELETE_POLICY_ENABLED) {
|
|
return nil
|
|
}
|
|
|
|
resourceVariables := r.getResourceVariables(kmsPushSecret)
|
|
kmsClient := resourceVariables.KMSClient
|
|
cancelCtx := resourceVariables.CancelCtx
|
|
authDetails := resourceVariables.AuthDetails
|
|
host := api.API_HOST_URL
|
|
if h := strings.TrimSpace(kmsPushSecret.Spec.HostAPI); h != "" {
|
|
host = h
|
|
}
|
|
|
|
if authDetails.AuthStrategy == "" {
|
|
logger.V(1).Info("No authentication strategy found. Attempting to authenticate")
|
|
var err error
|
|
authDetails, err = r.handleAuthentication(ctx, kmsPushSecret, host, kmsClient)
|
|
r.SetAuthenticatedStatusCondition(ctx, &kmsPushSecret, err)
|
|
if err != nil {
|
|
return fmt.Errorf("unable to authenticate [err=%s]", err)
|
|
}
|
|
r.updateResourceVariables(kmsPushSecret, util.ResourceVariables{
|
|
KMSClient: kmsClient,
|
|
CancelCtx: cancelCtx,
|
|
AuthDetails: authDetails,
|
|
})
|
|
}
|
|
|
|
destination := kmsPushSecret.Spec.Destination
|
|
for managedKey := range kmsPushSecret.Status.ManagedSecrets {
|
|
err := kmsClient.DeleteSecret(ctx, host, authDetails.BearerToken, destination.ProjectID, destination.EnvironmentSlug, destination.SecretsPath, managedKey)
|
|
if err != nil && !errors.Is(err, kmsapi.ErrNotFound) {
|
|
logger.Info(fmt.Sprintf("unable to delete secret [key=%s] [err=%s]", managedKey, err))
|
|
continue
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func joinIfAny(prefix string, items []string) string {
|
|
if len(items) == 0 {
|
|
return ""
|
|
}
|
|
return prefix + "[" + strings.Join(items, ", ") + "]"
|
|
}
|