From 8c0d345d2398b67aa450f03b764ba3a583322750 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 7 Dec 2024 00:13:18 +0400 Subject: [PATCH] feat(k8-operator): dynamic secrets --- k8-operator/PROJECT | 9 + k8-operator/api/v1alpha1/common.go | 105 +++++ .../v1alpha1/infisicaldynamicsecret_types.go | 99 ++++ .../api/v1alpha1/infisicalpushsecret_types.go | 63 +-- .../api/v1alpha1/infisicalsecret_types.go | 59 +-- .../api/v1alpha1/zz_generated.deepcopy.go | 380 +++++++++------ ...infisical.com_infisicaldynamicsecrets.yaml | 222 +++++++++ k8-operator/config/crd/kustomization.yaml | 1 + .../infisicaldynamicsecret_editor_role.yaml | 27 ++ .../infisicaldynamicsecret_viewer_role.yaml | 23 + k8-operator/config/rbac/role.yaml | 26 + .../samples/crd/pushsecret/pushSecret.yaml | 2 +- .../infisicaldynamicsecret_controller.go | 207 ++++++++ .../infisicaldynamicsecret_helper.go | 446 ++++++++++++++++++ .../infisicalpushsecret_controller.go | 6 +- .../infisicalsecret_controller.go | 6 +- k8-operator/go.mod | 4 +- k8-operator/go.sum | 4 + .../infisicaldynamicsecret_controller.go | 63 +++ k8-operator/main.go | 10 + k8-operator/packages/api/api.go | 21 + k8-operator/packages/api/models.go | 14 +- k8-operator/packages/constants/constants.go | 13 +- .../controllerhelpers/controllerhelpers.go} | 64 ++- k8-operator/packages/controllerutil/util.go | 45 -- k8-operator/packages/model/model.go | 12 + k8-operator/packages/util/auth.go | 88 +++- .../packages/util/{time.go => helpers.go} | 23 +- k8-operator/packages/util/workspace.go | 27 ++ 29 files changed, 1742 insertions(+), 327 deletions(-) create mode 100644 k8-operator/api/v1alpha1/common.go create mode 100644 k8-operator/api/v1alpha1/infisicaldynamicsecret_types.go create mode 100644 k8-operator/config/crd/bases/secrets.infisical.com_infisicaldynamicsecrets.yaml create mode 100644 k8-operator/config/rbac/infisicaldynamicsecret_editor_role.yaml create mode 100644 k8-operator/config/rbac/infisicaldynamicsecret_viewer_role.yaml create mode 100644 k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go create mode 100644 k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go create mode 100644 k8-operator/internal/controller/infisicaldynamicsecret_controller.go rename k8-operator/{controllers/infisicalsecret/auto_redeployment.go => packages/controllerhelpers/controllerhelpers.go} (59%) delete mode 100644 k8-operator/packages/controllerutil/util.go rename k8-operator/packages/util/{time.go => helpers.go} (58%) create mode 100644 k8-operator/packages/util/workspace.go diff --git a/k8-operator/PROJECT b/k8-operator/PROJECT index e57b8bee5d..59ebed6f60 100644 --- a/k8-operator/PROJECT +++ b/k8-operator/PROJECT @@ -26,4 +26,13 @@ resources: kind: InfisicalPushSecretSecret path: github.com/Infisical/infisical/k8-operator/api/v1alpha1 version: v1alpha1 + - api: + crdVersion: v1 + namespaced: true + controller: true + domain: infisical.com + group: secrets + kind: InfisicalDynamicSecret + path: github.com/Infisical/infisical/k8-operator/api/v1alpha1 + version: v1alpha1 version: "3" diff --git a/k8-operator/api/v1alpha1/common.go b/k8-operator/api/v1alpha1/common.go new file mode 100644 index 0000000000..387bc7c9ea --- /dev/null +++ b/k8-operator/api/v1alpha1/common.go @@ -0,0 +1,105 @@ +package v1alpha1 + +type GenericInfisicalAuthentication struct { + // +kubebuilder:validation:Optional + UniversalAuth GenericUniversalAuth `json:"universalAuth,omitempty"` + // +kubebuilder:validation:Optional + KubernetesAuth GenericKubernetesAuth `json:"kubernetesAuth,omitempty"` + // +kubebuilder:validation:Optional + AwsIamAuth GenericAwsIamAuth `json:"awsIamAuth,omitempty"` + // +kubebuilder:validation:Optional + AzureAuth GenericAzureAuth `json:"azureAuth,omitempty"` + // +kubebuilder:validation:Optional + GcpIdTokenAuth GenericGcpIdTokenAuth `json:"gcpIdTokenAuth,omitempty"` + // +kubebuilder:validation:Optional + GcpIamAuth GenericGcpIamAuth `json:"gcpIamAuth,omitempty"` +} + +type GenericUniversalAuth struct { + // +kubebuilder:validation:Required + CredentialsRef KubeSecretReference `json:"credentialsRef"` +} + +type GenericAwsIamAuth struct { + // +kubebuilder:validation:Required + IdentityID string `json:"identityId"` +} + +type GenericAzureAuth struct { + // +kubebuilder:validation:Required + IdentityID string `json:"identityId"` + // +kubebuilder:validation:Optional + Resource string `json:"resource,omitempty"` +} + +type GenericGcpIdTokenAuth struct { + // +kubebuilder:validation:Required + IdentityID string `json:"identityId"` +} + +type GenericGcpIamAuth struct { + // +kubebuilder:validation:Required + IdentityID string `json:"identityId"` + // +kubebuilder:validation:Required + ServiceAccountKeyFilePath string `json:"serviceAccountKeyFilePath"` +} + +type GenericKubernetesAuth struct { + // +kubebuilder:validation:Required + IdentityID string `json:"identityId"` + // +kubebuilder:validation:Required + ServiceAccountRef KubernetesServiceAccountRef `json:"serviceAccountRef"` +} + +type TLSConfig struct { + // Reference to secret containing CA cert + // +kubebuilder:validation:Optional + CaRef CaReference `json:"caRef,omitempty"` +} + +type CaReference struct { + // The name of the Kubernetes Secret + // +kubebuilder:validation:Required + SecretName string `json:"secretName"` + + // The namespace where the Kubernetes Secret is located + // +kubebuilder:validation:Required + SecretNamespace string `json:"secretNamespace"` + + // +kubebuilder:validation:Required + // The name of the secret property with the CA certificate value + SecretKey string `json:"key"` +} + +type KubeSecretReference struct { + // The name of the Kubernetes Secret + // +kubebuilder:validation:Required + SecretName string `json:"secretName"` + + // The name space where the Kubernetes Secret is located + // +kubebuilder:validation:Required + SecretNamespace string `json:"secretNamespace"` +} + +type ManagedKubeSecretConfig struct { + // The name of the Kubernetes Secret + // +kubebuilder:validation:Required + SecretName string `json:"secretName"` + + // The name space where the Kubernetes Secret is located + // +kubebuilder:validation:Required + SecretNamespace string `json:"secretNamespace"` + + // The Kubernetes Secret type (experimental feature). More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types + // +kubebuilder:validation:Optional + // +kubebuilder:default:=Opaque + SecretType string `json:"secretType"` + + // The Kubernetes Secret creation policy. + // Enum with values: 'Owner', 'Orphan'. + // Owner creates the secret and sets .metadata.ownerReferences of the InfisicalSecret CRD that created it. + // Orphan will not set the secret owner. This will result in the secret being orphaned and not deleted when the resource is deleted. + // +kubebuilder:validation:Optional + // +kubebuilder:default:=Orphan + CreationPolicy string `json:"creationPolicy"` +} diff --git a/k8-operator/api/v1alpha1/infisicaldynamicsecret_types.go b/k8-operator/api/v1alpha1/infisicaldynamicsecret_types.go new file mode 100644 index 0000000000..142cb671fb --- /dev/null +++ b/k8-operator/api/v1alpha1/infisicaldynamicsecret_types.go @@ -0,0 +1,99 @@ +/* +Copyright 2022. + +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 v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type InfisicalDynamicSecretLease struct { + ID string `json:"id"` + Version int64 `json:"version"` + CreationTimestamp metav1.Time `json:"creationTimestamp"` + ExpiresAt metav1.Time `json:"expiresAt"` +} + +type DynamicSecretDetails struct { + // +kubebuilder:validation:Required + // +kubebuilder:validation:Immutable + SecretName string `json:"secretName"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:Immutable + SecretPath string `json:"secretsPath"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:Immutable + EnvironmentSlug string `json:"environmentSlug"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:Immutable + ProjectID string `json:"projectId"` +} + +// InfisicalDynamicSecretSpec defines the desired state of InfisicalDynamicSecret. +type InfisicalDynamicSecretSpec struct { + // +kubebuilder:validation:Required + ManagedSecretReference ManagedKubeSecretConfig `json:"managedSecretReference"` // The destination to store the lease in. + + // +kubebuilder:validation:Required + Authentication GenericInfisicalAuthentication `json:"authentication"` // The authentication to use for authenticating with Infisical. + + // +kubebuilder:validation:Required + DynamicSecret DynamicSecretDetails `json:"dynamicSecret"` // The dynamic secret to create the lease for. Required. + + LeaseRevocationPolicy string `json:"leaseRevocationPolicy"` // Revoke will revoke the lease when the resource is deleted. Optional, will default to no revocation. + LeaseTTL string `json:"leaseTTL"` // The TTL of the lease in seconds. Optional, will default to the dynamic secret default TTL. + + // +kubebuilder:validation:Optional + HostAPI string `json:"hostAPI"` + + // +kubebuilder:validation:Optional + TLS TLSConfig `json:"tls"` +} + +// InfisicalDynamicSecretStatus defines the observed state of InfisicalDynamicSecret. +type InfisicalDynamicSecretStatus struct { + Lease *InfisicalDynamicSecretLease `json:"lease,omitempty"` + + DynamicSecretID string `json:"dynamicSecretId,omitempty"` + + // The MaxTTL can be null, if it's null, there's no max TTL and we should never have to renew. + MaxTTL string `json:"maxTTL,omitempty"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status + +// InfisicalDynamicSecret is the Schema for the infisicaldynamicsecrets API. +type InfisicalDynamicSecret struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec InfisicalDynamicSecretSpec `json:"spec,omitempty"` + Status InfisicalDynamicSecretStatus `json:"status,omitempty"` +} + +// +kubebuilder:object:root=true + +// InfisicalDynamicSecretList contains a list of InfisicalDynamicSecret. +type InfisicalDynamicSecretList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []InfisicalDynamicSecret `json:"items"` +} + +func init() { + SchemeBuilder.Register(&InfisicalDynamicSecret{}, &InfisicalDynamicSecretList{}) +} diff --git a/k8-operator/api/v1alpha1/infisicalpushsecret_types.go b/k8-operator/api/v1alpha1/infisicalpushsecret_types.go index a25e7ae9c7..9a1040868c 100644 --- a/k8-operator/api/v1alpha1/infisicalpushsecret_types.go +++ b/k8-operator/api/v1alpha1/infisicalpushsecret_types.go @@ -16,65 +16,6 @@ type InfisicalPushSecretDestination struct { ProjectID string `json:"projectId"` } -type PushSecretTlsConfig struct { - // Reference to secret containing CA cert - // +kubebuilder:validation:Optional - CaRef CaReference `json:"caRef,omitempty"` -} - -// PushSecretUniversalAuth defines universal authentication -type PushSecretUniversalAuth struct { - // +kubebuilder:validation:Required - CredentialsRef KubeSecretReference `json:"credentialsRef"` -} - -type PushSecretAwsIamAuth struct { - // +kubebuilder:validation:Required - IdentityID string `json:"identityId"` -} - -type PushSecretAzureAuth struct { - // +kubebuilder:validation:Required - IdentityID string `json:"identityId"` - // +kubebuilder:validation:Optional - Resource string `json:"resource,omitempty"` -} - -type PushSecretGcpIdTokenAuth struct { - // +kubebuilder:validation:Required - IdentityID string `json:"identityId"` -} - -type PushSecretGcpIamAuth struct { - // +kubebuilder:validation:Required - IdentityID string `json:"identityId"` - // +kubebuilder:validation:Required - ServiceAccountKeyFilePath string `json:"serviceAccountKeyFilePath"` -} - -// Rest of your types should be defined similarly... -type PushSecretKubernetesAuth struct { - // +kubebuilder:validation:Required - IdentityID string `json:"identityId"` - // +kubebuilder:validation:Required - ServiceAccountRef KubernetesServiceAccountRef `json:"serviceAccountRef"` -} - -type PushSecretAuthentication struct { - // +kubebuilder:validation:Optional - UniversalAuth PushSecretUniversalAuth `json:"universalAuth,omitempty"` - // +kubebuilder:validation:Optional - KubernetesAuth PushSecretKubernetesAuth `json:"kubernetesAuth,omitempty"` - // +kubebuilder:validation:Optional - AwsIamAuth PushSecretAwsIamAuth `json:"awsIamAuth,omitempty"` - // +kubebuilder:validation:Optional - AzureAuth PushSecretAzureAuth `json:"azureAuth,omitempty"` - // +kubebuilder:validation:Optional - GcpIdTokenAuth PushSecretGcpIdTokenAuth `json:"gcpIdTokenAuth,omitempty"` - // +kubebuilder:validation:Optional - GcpIamAuth PushSecretGcpIamAuth `json:"gcpIamAuth,omitempty"` -} - type SecretPush struct { // +kubebuilder:validation:Required Secret KubeSecretReference `json:"secret"` @@ -93,7 +34,7 @@ type InfisicalPushSecretSpec struct { Destination InfisicalPushSecretDestination `json:"destination"` // +kubebuilder:validation:Optional - Authentication PushSecretAuthentication `json:"authentication"` + Authentication GenericInfisicalAuthentication `json:"authentication"` // +kubebuilder:validation:Required Push SecretPush `json:"push"` @@ -105,7 +46,7 @@ type InfisicalPushSecretSpec struct { HostAPI string `json:"hostAPI"` // +kubebuilder:validation:Optional - TLS PushSecretTlsConfig `json:"tls"` + TLS TLSConfig `json:"tls"` } // InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret diff --git a/k8-operator/api/v1alpha1/infisicalsecret_types.go b/k8-operator/api/v1alpha1/infisicalsecret_types.go index 1af2faf202..cf22d64999 100644 --- a/k8-operator/api/v1alpha1/infisicalsecret_types.go +++ b/k8-operator/api/v1alpha1/infisicalsecret_types.go @@ -116,43 +116,6 @@ type MachineIdentityScopeInWorkspace struct { Recursive bool `json:"recursive"` } -type KubeSecretReference struct { - // The name of the Kubernetes Secret - // +kubebuilder:validation:Required - SecretName string `json:"secretName"` - - // The name space where the Kubernetes Secret is located - // +kubebuilder:validation:Required - SecretNamespace string `json:"secretNamespace"` -} - -type MangedKubeSecretConfig struct { - // The name of the Kubernetes Secret - // +kubebuilder:validation:Required - SecretName string `json:"secretName"` - - // The name space where the Kubernetes Secret is located - // +kubebuilder:validation:Required - SecretNamespace string `json:"secretNamespace"` - - // The Kubernetes Secret type (experimental feature). More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types - // +kubebuilder:validation:Optional - // +kubebuilder:default:=Opaque - SecretType string `json:"secretType"` - - // The Kubernetes Secret creation policy. - // Enum with values: 'Owner', 'Orphan'. - // Owner creates the secret and sets .metadata.ownerReferences of the InfisicalSecret CRD that created it. - // Orphan will not set the secret owner. This will result in the secret being orphaned and not deleted when the resource is deleted. - // +kubebuilder:validation:Optional - // +kubebuilder:default:=Orphan - CreationPolicy string `json:"creationPolicy"` - - // The template to transform the secret data - // +kubebuilder:validation:Optional - Template *InfisicalSecretTemplate `json:"template,omitempty"` -} - type InfisicalSecretTemplate struct { // This injects all retrieved secrets into the top level of your template. // Secrets defined in the template will take precedence over the injected ones. @@ -163,26 +126,6 @@ type InfisicalSecretTemplate struct { Data map[string]string `json:"data,omitempty"` } -type CaReference struct { - // The name of the Kubernetes Secret - // +kubebuilder:validation:Required - SecretName string `json:"secretName"` - - // The namespace where the Kubernetes Secret is located - // +kubebuilder:validation:Required - SecretNamespace string `json:"secretNamespace"` - - // +kubebuilder:validation:Required - // The name of the secret property with the CA certificate value - SecretKey string `json:"key"` -} - -type TLSConfig struct { - // Reference to secret containing CA cert - // +kubebuilder:validation:Optional - CaRef CaReference `json:"caRef,omitempty"` -} - // InfisicalSecretSpec defines the desired state of InfisicalSecret type InfisicalSecretSpec struct { // +kubebuilder:validation:Optional @@ -192,7 +135,7 @@ type InfisicalSecretSpec struct { Authentication Authentication `json:"authentication"` // +kubebuilder:validation:Required - ManagedSecretReference MangedKubeSecretConfig `json:"managedSecretReference"` + ManagedSecretReference ManagedKubeSecretConfig `json:"managedSecretReference"` // +kubebuilder:default:=60 ResyncInterval int `json:"resyncInterval"` diff --git a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go index fa54d41302..ddc5be2067 100644 --- a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -96,6 +96,21 @@ func (in *CaReference) DeepCopy() *CaReference { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DynamicSecretDetails) DeepCopyInto(out *DynamicSecretDetails) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DynamicSecretDetails. +func (in *DynamicSecretDetails) DeepCopy() *DynamicSecretDetails { + if in == nil { + return nil + } + out := new(DynamicSecretDetails) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GCPIdTokenAuthDetails) DeepCopyInto(out *GCPIdTokenAuthDetails) { *out = *in @@ -128,6 +143,234 @@ func (in *GcpIamAuthDetails) DeepCopy() *GcpIamAuthDetails { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GenericAwsIamAuth) DeepCopyInto(out *GenericAwsIamAuth) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericAwsIamAuth. +func (in *GenericAwsIamAuth) DeepCopy() *GenericAwsIamAuth { + if in == nil { + return nil + } + out := new(GenericAwsIamAuth) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GenericAzureAuth) DeepCopyInto(out *GenericAzureAuth) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericAzureAuth. +func (in *GenericAzureAuth) DeepCopy() *GenericAzureAuth { + if in == nil { + return nil + } + out := new(GenericAzureAuth) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GenericGcpIamAuth) DeepCopyInto(out *GenericGcpIamAuth) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericGcpIamAuth. +func (in *GenericGcpIamAuth) DeepCopy() *GenericGcpIamAuth { + if in == nil { + return nil + } + out := new(GenericGcpIamAuth) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GenericGcpIdTokenAuth) DeepCopyInto(out *GenericGcpIdTokenAuth) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericGcpIdTokenAuth. +func (in *GenericGcpIdTokenAuth) DeepCopy() *GenericGcpIdTokenAuth { + if in == nil { + return nil + } + out := new(GenericGcpIdTokenAuth) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GenericInfisicalAuthentication) DeepCopyInto(out *GenericInfisicalAuthentication) { + *out = *in + out.UniversalAuth = in.UniversalAuth + out.KubernetesAuth = in.KubernetesAuth + out.AwsIamAuth = in.AwsIamAuth + out.AzureAuth = in.AzureAuth + out.GcpIdTokenAuth = in.GcpIdTokenAuth + out.GcpIamAuth = in.GcpIamAuth +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericInfisicalAuthentication. +func (in *GenericInfisicalAuthentication) DeepCopy() *GenericInfisicalAuthentication { + if in == nil { + return nil + } + out := new(GenericInfisicalAuthentication) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GenericKubernetesAuth) DeepCopyInto(out *GenericKubernetesAuth) { + *out = *in + out.ServiceAccountRef = in.ServiceAccountRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericKubernetesAuth. +func (in *GenericKubernetesAuth) DeepCopy() *GenericKubernetesAuth { + if in == nil { + return nil + } + out := new(GenericKubernetesAuth) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GenericUniversalAuth) DeepCopyInto(out *GenericUniversalAuth) { + *out = *in + out.CredentialsRef = in.CredentialsRef +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GenericUniversalAuth. +func (in *GenericUniversalAuth) DeepCopy() *GenericUniversalAuth { + if in == nil { + return nil + } + out := new(GenericUniversalAuth) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfisicalDynamicSecret) DeepCopyInto(out *InfisicalDynamicSecret) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + out.Spec = in.Spec + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalDynamicSecret. +func (in *InfisicalDynamicSecret) DeepCopy() *InfisicalDynamicSecret { + if in == nil { + return nil + } + out := new(InfisicalDynamicSecret) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *InfisicalDynamicSecret) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfisicalDynamicSecretLease) DeepCopyInto(out *InfisicalDynamicSecretLease) { + *out = *in + in.CreationTimestamp.DeepCopyInto(&out.CreationTimestamp) + in.ExpiresAt.DeepCopyInto(&out.ExpiresAt) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalDynamicSecretLease. +func (in *InfisicalDynamicSecretLease) DeepCopy() *InfisicalDynamicSecretLease { + if in == nil { + return nil + } + out := new(InfisicalDynamicSecretLease) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfisicalDynamicSecretList) DeepCopyInto(out *InfisicalDynamicSecretList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]InfisicalDynamicSecret, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalDynamicSecretList. +func (in *InfisicalDynamicSecretList) DeepCopy() *InfisicalDynamicSecretList { + if in == nil { + return nil + } + out := new(InfisicalDynamicSecretList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *InfisicalDynamicSecretList) DeepCopyObject() runtime.Object { + if c := in.DeepCopy(); c != nil { + return c + } + return nil +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfisicalDynamicSecretSpec) DeepCopyInto(out *InfisicalDynamicSecretSpec) { + *out = *in + out.ManagedSecretReference = in.ManagedSecretReference + out.Authentication = in.Authentication + out.DynamicSecret = in.DynamicSecret + out.TLS = in.TLS +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalDynamicSecretSpec. +func (in *InfisicalDynamicSecretSpec) DeepCopy() *InfisicalDynamicSecretSpec { + if in == nil { + return nil + } + out := new(InfisicalDynamicSecretSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfisicalDynamicSecretStatus) DeepCopyInto(out *InfisicalDynamicSecretStatus) { + *out = *in + if in.Lease != nil { + in, out := &in.Lease, &out.Lease + *out = new(InfisicalDynamicSecretLease) + (*in).DeepCopyInto(*out) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalDynamicSecretStatus. +func (in *InfisicalDynamicSecretStatus) DeepCopy() *InfisicalDynamicSecretStatus { + if in == nil { + return nil + } + out := new(InfisicalDynamicSecretStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InfisicalPushSecret) DeepCopyInto(out *InfisicalPushSecret) { *out = *in @@ -435,7 +678,7 @@ func (in *MachineIdentityScopeInWorkspace) DeepCopy() *MachineIdentityScopeInWor } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *MangedKubeSecretConfig) DeepCopyInto(out *MangedKubeSecretConfig) { +func (in *ManagedKubeSecretConfig) DeepCopyInto(out *ManagedKubeSecretConfig) { *out = *in if in.Template != nil { in, out := &in.Template, &out.Template @@ -444,141 +687,12 @@ func (in *MangedKubeSecretConfig) DeepCopyInto(out *MangedKubeSecretConfig) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new MangedKubeSecretConfig. -func (in *MangedKubeSecretConfig) DeepCopy() *MangedKubeSecretConfig { - if in == nil { - return nil - } - out := new(MangedKubeSecretConfig) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PushSecretAuthentication) DeepCopyInto(out *PushSecretAuthentication) { - *out = *in - out.UniversalAuth = in.UniversalAuth - out.KubernetesAuth = in.KubernetesAuth - out.AwsIamAuth = in.AwsIamAuth - out.AzureAuth = in.AzureAuth - out.GcpIdTokenAuth = in.GcpIdTokenAuth - out.GcpIamAuth = in.GcpIamAuth -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretAuthentication. -func (in *PushSecretAuthentication) DeepCopy() *PushSecretAuthentication { - if in == nil { - return nil - } - out := new(PushSecretAuthentication) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PushSecretAwsIamAuth) DeepCopyInto(out *PushSecretAwsIamAuth) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretAwsIamAuth. -func (in *PushSecretAwsIamAuth) DeepCopy() *PushSecretAwsIamAuth { - if in == nil { - return nil - } - out := new(PushSecretAwsIamAuth) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PushSecretAzureAuth) DeepCopyInto(out *PushSecretAzureAuth) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretAzureAuth. -func (in *PushSecretAzureAuth) DeepCopy() *PushSecretAzureAuth { - if in == nil { - return nil - } - out := new(PushSecretAzureAuth) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PushSecretGcpIamAuth) DeepCopyInto(out *PushSecretGcpIamAuth) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretGcpIamAuth. -func (in *PushSecretGcpIamAuth) DeepCopy() *PushSecretGcpIamAuth { - if in == nil { - return nil - } - out := new(PushSecretGcpIamAuth) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PushSecretGcpIdTokenAuth) DeepCopyInto(out *PushSecretGcpIdTokenAuth) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretGcpIdTokenAuth. -func (in *PushSecretGcpIdTokenAuth) DeepCopy() *PushSecretGcpIdTokenAuth { - if in == nil { - return nil - } - out := new(PushSecretGcpIdTokenAuth) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PushSecretKubernetesAuth) DeepCopyInto(out *PushSecretKubernetesAuth) { - *out = *in - out.ServiceAccountRef = in.ServiceAccountRef -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretKubernetesAuth. -func (in *PushSecretKubernetesAuth) DeepCopy() *PushSecretKubernetesAuth { - if in == nil { - return nil - } - out := new(PushSecretKubernetesAuth) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PushSecretTlsConfig) DeepCopyInto(out *PushSecretTlsConfig) { - *out = *in - out.CaRef = in.CaRef -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretTlsConfig. -func (in *PushSecretTlsConfig) DeepCopy() *PushSecretTlsConfig { - if in == nil { - return nil - } - out := new(PushSecretTlsConfig) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *PushSecretUniversalAuth) DeepCopyInto(out *PushSecretUniversalAuth) { - *out = *in - out.CredentialsRef = in.CredentialsRef -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PushSecretUniversalAuth. -func (in *PushSecretUniversalAuth) DeepCopy() *PushSecretUniversalAuth { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ManagedKubeSecretConfig. +func (in *ManagedKubeSecretConfig) DeepCopy() *ManagedKubeSecretConfig { if in == nil { return nil } - out := new(PushSecretUniversalAuth) + out := new(ManagedKubeSecretConfig) in.DeepCopyInto(out) return out } diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_infisicaldynamicsecrets.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_infisicaldynamicsecrets.yaml new file mode 100644 index 0000000000..53fff8e4e5 --- /dev/null +++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicaldynamicsecrets.yaml @@ -0,0 +1,222 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + creationTimestamp: null + name: infisicaldynamicsecrets.secrets.infisical.com +spec: + group: secrets.infisical.com + names: + kind: InfisicalDynamicSecret + listKind: InfisicalDynamicSecretList + plural: infisicaldynamicsecrets + singular: infisicaldynamicsecret + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: InfisicalDynamicSecret is the Schema for the infisicaldynamicsecrets + API. + properties: + apiVersion: + description: 'APIVersion defines the versioned schema of this representation + of an object. Servers should convert recognized schemas to the latest + internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' + type: string + kind: + description: 'Kind is a string value representing the REST resource this + object represents. Servers may infer this from the endpoint the client + submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' + type: string + metadata: + type: object + spec: + description: InfisicalDynamicSecretSpec defines the desired state of InfisicalDynamicSecret. + properties: + authentication: + properties: + awsIamAuth: + properties: + identityId: + type: string + required: + - identityId + type: object + azureAuth: + properties: + identityId: + type: string + resource: + type: string + required: + - identityId + type: object + gcpIamAuth: + properties: + identityId: + type: string + serviceAccountKeyFilePath: + type: string + required: + - identityId + - serviceAccountKeyFilePath + type: object + gcpIdTokenAuth: + properties: + identityId: + type: string + required: + - identityId + type: object + kubernetesAuth: + properties: + identityId: + type: string + serviceAccountRef: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + required: + - identityId + - serviceAccountRef + type: object + universalAuth: + properties: + credentialsRef: + properties: + secretName: + description: The name of the Kubernetes Secret + type: string + secretNamespace: + description: The name space where the Kubernetes Secret + is located + type: string + required: + - secretName + - secretNamespace + type: object + required: + - credentialsRef + type: object + type: object + dynamicSecret: + properties: + environmentSlug: + type: string + projectId: + type: string + secretName: + type: string + secretsPath: + type: string + required: + - environmentSlug + - projectId + - secretName + - secretsPath + type: object + hostAPI: + type: string + leaseRevocationPolicy: + type: string + leaseTTL: + type: string + managedSecretReference: + properties: + creationPolicy: + default: Orphan + description: 'The Kubernetes Secret creation policy. Enum with + values: ''Owner'', ''Orphan''. Owner creates the secret and + sets .metadata.ownerReferences of the InfisicalSecret CRD that + created it. Orphan will not set the secret owner. This will + result in the secret being orphaned and not deleted when the + resource is deleted.' + type: string + secretName: + description: The name of the Kubernetes Secret + type: string + secretNamespace: + description: The name space where the Kubernetes Secret is located + type: string + secretType: + default: Opaque + description: 'The Kubernetes Secret type (experimental feature). + More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types' + type: string + required: + - secretName + - secretNamespace + type: object + tls: + properties: + caRef: + description: Reference to secret containing CA cert + properties: + key: + description: The name of the secret property with the CA certificate + value + type: string + secretName: + description: The name of the Kubernetes Secret + type: string + secretNamespace: + description: The namespace where the Kubernetes Secret is + located + type: string + required: + - key + - secretName + - secretNamespace + type: object + type: object + required: + - authentication + - dynamicSecret + - leaseRevocationPolicy + - leaseTTL + - managedSecretReference + type: object + status: + description: InfisicalDynamicSecretStatus defines the observed state of + InfisicalDynamicSecret. + properties: + dynamicSecretId: + type: string + lease: + properties: + creationTimestamp: + format: date-time + type: string + expiresAt: + format: date-time + type: string + id: + type: string + version: + format: int64 + type: integer + required: + - creationTimestamp + - expiresAt + - id + - version + type: object + maxTTL: + description: The MaxTTL can be null, if it's null, there's no max + TTL and we should never have to renew. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/k8-operator/config/crd/kustomization.yaml b/k8-operator/config/crd/kustomization.yaml index bb7b81e6b0..ea6db574af 100644 --- a/k8-operator/config/crd/kustomization.yaml +++ b/k8-operator/config/crd/kustomization.yaml @@ -4,6 +4,7 @@ resources: - bases/secrets.infisical.com_infisicalsecrets.yaml - bases/secrets.infisical.com_infisicalpushsecrets.yaml + - bases/secrets.infisical.com_infisicaldynamicsecrets.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: diff --git a/k8-operator/config/rbac/infisicaldynamicsecret_editor_role.yaml b/k8-operator/config/rbac/infisicaldynamicsecret_editor_role.yaml new file mode 100644 index 0000000000..9d68cdc755 --- /dev/null +++ b/k8-operator/config/rbac/infisicaldynamicsecret_editor_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to edit infisicaldynamicsecrets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: k8-operator + app.kubernetes.io/managed-by: kustomize + name: infisicaldynamicsecret-editor-role +rules: +- apiGroups: + - secrets.infisical.com + resources: + - infisicaldynamicsecrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - secrets.infisical.com + resources: + - infisicaldynamicsecrets/status + verbs: + - get diff --git a/k8-operator/config/rbac/infisicaldynamicsecret_viewer_role.yaml b/k8-operator/config/rbac/infisicaldynamicsecret_viewer_role.yaml new file mode 100644 index 0000000000..b80f51fa61 --- /dev/null +++ b/k8-operator/config/rbac/infisicaldynamicsecret_viewer_role.yaml @@ -0,0 +1,23 @@ +# permissions for end users to view infisicaldynamicsecrets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: k8-operator + app.kubernetes.io/managed-by: kustomize + name: infisicaldynamicsecret-viewer-role +rules: +- apiGroups: + - secrets.infisical.com + resources: + - infisicaldynamicsecrets + verbs: + - get + - list + - watch +- apiGroups: + - secrets.infisical.com + resources: + - infisicaldynamicsecrets/status + verbs: + - get diff --git a/k8-operator/config/rbac/role.yaml b/k8-operator/config/rbac/role.yaml index 00fab4a896..f237a324b8 100644 --- a/k8-operator/config/rbac/role.yaml +++ b/k8-operator/config/rbac/role.yaml @@ -44,6 +44,32 @@ rules: - list - update - watch +- apiGroups: + - secrets.infisical.com + resources: + - infisicaldynamicsecrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - secrets.infisical.com + resources: + - infisicaldynamicsecrets/finalizers + verbs: + - update +- apiGroups: + - secrets.infisical.com + resources: + - infisicaldynamicsecrets/status + verbs: + - get + - patch + - update - apiGroups: - secrets.infisical.com resources: diff --git a/k8-operator/config/samples/crd/pushsecret/pushSecret.yaml b/k8-operator/config/samples/crd/pushsecret/pushSecret.yaml index ab54f1384c..f1738fc474 100644 --- a/k8-operator/config/samples/crd/pushsecret/pushSecret.yaml +++ b/k8-operator/config/samples/crd/pushsecret/pushSecret.yaml @@ -1,7 +1,7 @@ apiVersion: secrets.infisical.com/v1alpha1 kind: InfisicalPushSecret metadata: - name: infisical-push-secret-demo + name: infisical-api-secret-sample-push spec: resyncInterval: 1m hostAPI: https://app.infisical.com/api diff --git a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go new file mode 100644 index 0000000000..8e1738eae1 --- /dev/null +++ b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go @@ -0,0 +1,207 @@ +package controllers + +import ( + "context" + "fmt" + "time" + + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/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/predicate" + + secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1" + "github.com/Infisical/infisical/k8-operator/packages/api" + "github.com/Infisical/infisical/k8-operator/packages/constants" + controllerhelpers "github.com/Infisical/infisical/k8-operator/packages/controllerhelpers" + "github.com/Infisical/infisical/k8-operator/packages/util" + "github.com/go-logr/logr" +) + +// InfisicalDynamicSecretReconciler reconciles a InfisicalDynamicSecret object +type InfisicalDynamicSecretReconciler struct { + client.Client + Scheme *runtime.Scheme + + BaseLogger logr.Logger +} + +// +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 + +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) +} + +func (r *InfisicalDynamicSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + + logger := r.GetLogger(req) + + 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 + } + + err := r.HandleLeaseRevocation(ctx, logger, infisicalDynamicSecretCRD) + + 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) + 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 + } + + if infisicalDynamicSecretCRD.Spec.HostAPI == "" { + api.API_HOST_URL = infisicalConfig["hostAPI"] + } else { + api.API_HOST_URL = util.AppendAPIEndpoint(infisicalDynamicSecretCRD.Spec.HostAPI) + } + + if infisicalDynamicSecretCRD.Spec.TLS.CaRef.SecretName != "" { + api.API_CA_CERTIFICATE, err = r.getInfisicalCaCertificateFromKubeSecret(ctx, infisicalDynamicSecretCRD) + if err != nil { + logger.Error(err, fmt.Sprintf("unable to fetch CA certificate. Will requeue after [requeueTime=%v]", requeueTime)) + return ctrl.Result{ + RequeueAfter: requeueTime, + }, nil + } + + logger.Info("Using custom CA certificate...") + } else { + api.API_CA_CERTIFICATE = "" + } + + nextReconcile, err := r.ReconcileInfisicalDynamicSecret(ctx, logger, infisicalDynamicSecretCRD) + // r.SetSuccessfullyReconciledConditions(ctx, &infisicalDynamicSecretCRD, err) + + if err == nil && nextReconcile.Seconds() >= 5 { + requeueTime = nextReconcile + } + + if err != nil { + logger.Error(err, fmt.Sprintf("unable to reconcile Infisical Push Secret. Will requeue after [requeueTime=%v]", requeueTime)) + return ctrl.Result{ + RequeueAfter: requeueTime, + }, nil + } + + _, err = controllerhelpers.ReconcileDeploymentsWithManagedSecrets(ctx, r.Client, logger, infisicalDynamicSecretCRD.Spec.ManagedSecretReference) + + 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 +} + +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{}, builder.WithPredicates( + specChangeOrDelete, + )). + Complete(r) +} diff --git a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go new file mode 100644 index 0000000000..c5e2703c9c --- /dev/null +++ b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go @@ -0,0 +1,446 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + "github.com/Infisical/infisical/k8-operator/api/v1alpha1" + "github.com/Infisical/infisical/k8-operator/packages/api" + "github.com/Infisical/infisical/k8-operator/packages/constants" + "github.com/Infisical/infisical/k8-operator/packages/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" + ctrl "sigs.k8s.io/controller-runtime" +) + +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) handleAuthentication(ctx context.Context, infisicalSecret v1alpha1.InfisicalDynamicSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (util.AuthenticationDetails, error) { + authStrategies := map[util.AuthStrategyType]func(ctx context.Context, reconcilerClient client.Client, secretCrd util.SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (util.AuthenticationDetails, error){ + util.AuthStrategy.UNIVERSAL_MACHINE_IDENTITY: util.HandleUniversalAuth, + util.AuthStrategy.KUBERNETES_MACHINE_IDENTITY: util.HandleKubernetesAuth, + util.AuthStrategy.AWS_IAM_MACHINE_IDENTITY: util.HandleAwsIamAuth, + util.AuthStrategy.AZURE_MACHINE_IDENTITY: util.HandleAzureAuth, + util.AuthStrategy.GCP_ID_TOKEN_MACHINE_IDENTITY: util.HandleGcpIdTokenAuth, + util.AuthStrategy.GCP_IAM_MACHINE_IDENTITY: util.HandleGcpIamAuth, + } + + for authStrategy, authHandler := range authStrategies { + authDetails, err := authHandler(ctx, r.Client, util.SecretAuthInput{ + Secret: infisicalSecret, + Type: util.SecretCrd.INFISICAL_DYNAMIC_SECRET, + }, infisicalClient) + + if err == nil { + return authDetails, nil + } + + if !errors.Is(err, util.ErrAuthNotApplicable) { + return util.AuthenticationDetails{}, fmt.Errorf("authentication failed for strategy [%s] [err=%w]", authStrategy, err) + } + } + + return util.AuthenticationDetails{}, fmt.Errorf("no authentication method provided") + +} + +func (r *InfisicalDynamicSecretReconciler) getInfisicalCaCertificateFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalDynamicSecret) (caCertificate string, err error) { + + caCertificateFromKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.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 { + 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 (r *InfisicalDynamicSecretReconciler) getResourceVariables(infisicalDynamicSecret v1alpha1.InfisicalDynamicSecret) util.ResourceVariables { + + var resourceVariables util.ResourceVariables + + if _, ok := infisicalDynamicSecretsResourceVariablesMap[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, + }) + + infisicalDynamicSecretsResourceVariablesMap[string(infisicalDynamicSecret.UID)] = util.ResourceVariables{ + InfisicalClient: client, + CancelCtx: cancel, + AuthDetails: util.AuthenticationDetails{}, + } + + resourceVariables = infisicalDynamicSecretsResourceVariablesMap[string(infisicalDynamicSecret.UID)] + + } else { + resourceVariables = infisicalDynamicSecretsResourceVariablesMap[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) + } + + 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("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) { + infisicalDynamicSecretsResourceVariablesMap[string(infisicalDynamicSecret.UID)] = resourceVariables +} + +func (r *InfisicalDynamicSecretReconciler) HandleLeaseRevocation(ctx context.Context, logger logr.Logger, infisicalDynamicSecret v1alpha1.InfisicalDynamicSecret) error { + if infisicalDynamicSecret.Spec.LeaseRevocationPolicy != string(constants.DYNAMIC_SECRET_LEASE_REVOCATION_POLICY_ENABLED) { + return nil + } + + resourceVariables := r.getResourceVariables(infisicalDynamicSecret) + infisicalClient := resourceVariables.InfisicalClient + + logger.Info("Authenticating for lease revocation") + authDetails, err := r.handleAuthentication(ctx, infisicalDynamicSecret, infisicalClient) + + 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, + }) + + 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 { + 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) (time.Duration, error) { + + resourceVariables := r.getResourceVariables(infisicalDynamicSecret) + 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 = r.handleAuthentication(ctx, infisicalDynamicSecret, infisicalClient) + + if err != nil { + return nextReconcile, fmt.Errorf("unable to authenticate [err=%s]", err) + } + + r.updateResourceVariables(infisicalDynamicSecret, util.ResourceVariables{ + InfisicalClient: infisicalClient, + CancelCtx: cancelCtx, + AuthDetails: authDetails, + }) + } + + destination, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{ + Name: infisicalDynamicSecret.Spec.ManagedSecretReference.SecretName, + Namespace: infisicalDynamicSecret.Spec.ManagedSecretReference.SecretNamespace, + }) + + if err != nil && !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) + } + + if err != nil { + if k8Errors.IsNotFound(err) { + return nextReconcile, fmt.Errorf("destination secret not found") + } + + return nextReconcile, fmt.Errorf("unable to fetch destination secret") + } + + if infisicalDynamicSecret.Status.Lease == nil { + r.CreateDynamicSecretLease(ctx, logger, infisicalClient, &infisicalDynamicSecret, destination) + } else { + now := time.Now() + leaseExpiresAt := infisicalDynamicSecret.Status.Lease.ExpiresAt.Time + + // Calculate from creation to expiration + originalLeaseDuration := leaseExpiresAt.Sub(infisicalDynamicSecret.Status.Lease.CreationTimestamp.Time) + + // 30% of the original duration (if the TTL has 30% or less of its time left, renew) + renewalThreshold := originalLeaseDuration * 30 / 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 { + 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)) + } + + return nextReconcile, nil +} diff --git a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go index af5428ee7b..d288c693a8 100644 --- a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go +++ b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go @@ -22,7 +22,7 @@ import ( secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1" "github.com/Infisical/infisical/k8-operator/packages/api" "github.com/Infisical/infisical/k8-operator/packages/constants" - controllerhelpers "github.com/Infisical/infisical/k8-operator/packages/controllerutil" + controllerhelpers "github.com/Infisical/infisical/k8-operator/packages/controllerhelpers" "github.com/Infisical/infisical/k8-operator/packages/util" "github.com/go-logr/logr" ) @@ -105,7 +105,7 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl. if infisicalPushSecretCRD.Spec.ResyncInterval != "" { - duration, err := util.ConvertResyncIntervalToDuration(infisicalPushSecretCRD.Spec.ResyncInterval) + duration, err := util.ConvertIntervalToDuration(infisicalPushSecretCRD.Spec.ResyncInterval) if err != nil { logger.Error(err, fmt.Sprintf("unable to convert resync interval to duration. Will requeue after [requeueTime=%v]", requeueTime)) @@ -141,7 +141,7 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl. if infisicalPushSecretCRD.Spec.HostAPI == "" { api.API_HOST_URL = infisicalConfig["hostAPI"] } else { - api.API_HOST_URL = infisicalPushSecretCRD.Spec.HostAPI + api.API_HOST_URL = util.AppendAPIEndpoint(infisicalPushSecretCRD.Spec.HostAPI) } if infisicalPushSecretCRD.Spec.TLS.CaRef.SecretName != "" { diff --git a/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go b/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go index 9e4656c55b..cadbd05ce5 100644 --- a/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go +++ b/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go @@ -15,7 +15,7 @@ import ( secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1" "github.com/Infisical/infisical/k8-operator/packages/api" - controllerhelpers "github.com/Infisical/infisical/k8-operator/packages/controllerutil" + controllerhelpers "github.com/Infisical/infisical/k8-operator/packages/controllerhelpers" "github.com/Infisical/infisical/k8-operator/packages/util" "github.com/go-logr/logr" ) @@ -110,7 +110,7 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ if infisicalSecretCRD.Spec.HostAPI == "" { api.API_HOST_URL = infisicalConfig["hostAPI"] } else { - api.API_HOST_URL = infisicalSecretCRD.Spec.HostAPI + api.API_HOST_URL = util.AppendAPIEndpoint(infisicalSecretCRD.Spec.HostAPI) } if infisicalSecretCRD.Spec.TLS.CaRef.SecretName != "" { @@ -138,7 +138,7 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ }, nil } - numDeployments, err := r.ReconcileDeploymentsWithManagedSecrets(ctx, logger, infisicalSecretCRD) + numDeployments, err := controllerhelpers.ReconcileDeploymentsWithManagedSecrets(ctx, r.Client, logger, infisicalSecretCRD.Spec.ManagedSecretReference) r.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)) diff --git a/k8-operator/go.mod b/k8-operator/go.mod index 5c7d268f22..0731666fab 100644 --- a/k8-operator/go.mod +++ b/k8-operator/go.mod @@ -3,7 +3,7 @@ module github.com/Infisical/infisical/k8-operator go 1.21 require ( - github.com/infisical/go-sdk v0.4.1 + github.com/infisical/go-sdk v0.4.4 github.com/onsi/ginkgo/v2 v2.6.0 github.com/onsi/gomega v1.24.1 k8s.io/apimachinery v0.26.1 @@ -54,7 +54,7 @@ require ( github.com/emicklei/go-restful/v3 v3.9.0 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-logr/logr v1.4.2 // indirect + github.com/go-logr/logr v1.4.2 github.com/go-logr/zapr v1.2.3 // indirect github.com/go-openapi/jsonpointer v0.19.5 // indirect github.com/go-openapi/jsonreference v0.20.0 // indirect diff --git a/k8-operator/go.sum b/k8-operator/go.sum index c78515f743..f6b61945a9 100644 --- a/k8-operator/go.sum +++ b/k8-operator/go.sum @@ -219,6 +219,10 @@ github.com/imdario/mergo v0.3.12 h1:b6R2BslTbIEToALKP7LxUvijTsNI9TAe80pLWN2g/HU= github.com/imdario/mergo v0.3.12/go.mod h1:jmQim1M+e3UYxmgPu/WyfjB3N3VflVyUjjjwH0dnCYA= github.com/infisical/go-sdk v0.4.1 h1:ZeLyc2+2TeIaw9odjxR3ipQqYzVSMOnd8/RaqyUNvBg= github.com/infisical/go-sdk v0.4.1/go.mod h1:6fWzAwTPIoKU49mQ2Oxu+aFnJu9n7k2JcNrZjzhHM2M= +github.com/infisical/go-sdk v0.4.3 h1:O5ZJ2eCBAZDE9PIAfBPq9Utb2CgQKrhmj9R0oFTRu4U= +github.com/infisical/go-sdk v0.4.3/go.mod h1:6fWzAwTPIoKU49mQ2Oxu+aFnJu9n7k2JcNrZjzhHM2M= +github.com/infisical/go-sdk v0.4.4 h1:Z4CBzxfhiY6ikjRimOEeyEEnb3QT/BKw3OzNFH7Pe+U= +github.com/infisical/go-sdk v0.4.4/go.mod h1:6fWzAwTPIoKU49mQ2Oxu+aFnJu9n7k2JcNrZjzhHM2M= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= diff --git a/k8-operator/internal/controller/infisicaldynamicsecret_controller.go b/k8-operator/internal/controller/infisicaldynamicsecret_controller.go new file mode 100644 index 0000000000..8d75eef692 --- /dev/null +++ b/k8-operator/internal/controller/infisicaldynamicsecret_controller.go @@ -0,0 +1,63 @@ +/* +Copyright 2022. + +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" + + "k8s.io/apimachinery/pkg/runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1" +) + +// InfisicalDynamicSecretReconciler reconciles a InfisicalDynamicSecret object +type InfisicalDynamicSecretReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +// +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 + +// 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 + + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *InfisicalDynamicSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&secretsv1alpha1.InfisicalDynamicSecret{}). + Named("infisicaldynamicsecret"). + Complete(r) +} diff --git a/k8-operator/main.go b/k8-operator/main.go index a0e11c0ca1..4afbf6e560 100644 --- a/k8-operator/main.go +++ b/k8-operator/main.go @@ -16,6 +16,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1" + infisicalDynamicSecretController "github.com/Infisical/infisical/k8-operator/controllers/infisicaldynamicsecret" infisicalPushSecretController "github.com/Infisical/infisical/k8-operator/controllers/infisicalpushsecret" infisicalSecretController "github.com/Infisical/infisical/k8-operator/controllers/infisicalsecret" //+kubebuilder:scaffold:imports @@ -92,6 +93,15 @@ func main() { os.Exit(1) } + if err = (&infisicalDynamicSecretController.InfisicalDynamicSecretReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + BaseLogger: ctrl.Log, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "InfisicalDynamicSecret") + os.Exit(1) + } + //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/k8-operator/packages/api/api.go b/k8-operator/packages/api/api.go index de12b4bc55..36edfa5c1d 100644 --- a/k8-operator/packages/api/api.go +++ b/k8-operator/packages/api/api.go @@ -125,3 +125,24 @@ func CallGetServiceAccountKeysV2(httpClient *resty.Client, request GetServiceAcc 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 + +} diff --git a/k8-operator/packages/api/models.go b/k8-operator/packages/api/models.go index b2316117b1..01f835397a 100644 --- a/k8-operator/packages/api/models.go +++ b/k8-operator/packages/api/models.go @@ -1,6 +1,10 @@ package api -import "time" +import ( + "time" + + "github.com/Infisical/infisical/k8-operator/packages/model" +) type GetEncryptedWorkspaceKeyRequest struct { WorkspaceId string `json:"workspaceId"` @@ -194,3 +198,11 @@ type ServiceAccountKey struct { type GetServiceAccountKeysResponse struct { ServiceAccountKeys []ServiceAccountKey `json:"serviceAccountKeys"` } + +type GetProjectByIDRequest struct { + ProjectID string +} + +type GetProjectByIDResponse struct { + Project model.Project `json:"workspace"` +} diff --git a/k8-operator/packages/constants/constants.go b/k8-operator/packages/constants/constants.go index ff28e408d1..75f15606a2 100644 --- a/k8-operator/packages/constants/constants.go +++ b/k8-operator/packages/constants/constants.go @@ -1,5 +1,7 @@ package constants +import "errors" + const SERVICE_ACCOUNT_ACCESS_KEY = "serviceAccountAccessKey" const SERVICE_ACCOUNT_PUBLIC_KEY = "serviceAccountPublicKey" const SERVICE_ACCOUNT_PRIVATE_KEY = "serviceAccountPrivateKey" @@ -13,7 +15,8 @@ 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 = "infisical.secrets.infisical.com/finalizer" +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 @@ -22,3 +25,11 @@ const ( PUSH_SECRET_REPLACE_POLICY_ENABLED PushSecretReplacePolicy = "Replace" PUSH_SECRET_DELETE_POLICY_ENABLED PushSecretDeletionPolicy = "Delete" ) + +type DynamicSecretLeaseRevocationPolicy string + +const ( + DYNAMIC_SECRET_LEASE_REVOCATION_POLICY_ENABLED DynamicSecretLeaseRevocationPolicy = "Revoke" +) + +var ErrInvalidLease = errors.New("invalid dynamic secret lease") diff --git a/k8-operator/controllers/infisicalsecret/auto_redeployment.go b/k8-operator/packages/controllerhelpers/controllerhelpers.go similarity index 59% rename from k8-operator/controllers/infisicalsecret/auto_redeployment.go rename to k8-operator/packages/controllerhelpers/controllerhelpers.go index 599e126b50..a036675ba2 100644 --- a/k8-operator/controllers/infisicalsecret/auto_redeployment.go +++ b/k8-operator/packages/controllerhelpers/controllerhelpers.go @@ -1,4 +1,4 @@ -package controllers +package controllerhelpers import ( "context" @@ -10,27 +10,30 @@ import ( "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 (r *InfisicalSecretReconciler) ReconcileDeploymentsWithManagedSecrets(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret) (int, error) { +func ReconcileDeploymentsWithManagedSecrets(ctx context.Context, client controllerClient.Client, logger logr.Logger, managedSecret v1alpha1.ManagedKubeSecretConfig) (int, error) { listOfDeployments := &v1.DeploymentList{} - err := r.Client.List(ctx, listOfDeployments, &client.ListOptions{Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace}) + + 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]", infisicalSecret.Spec.ManagedSecretReference.SecretNamespace, err) + return 0, fmt.Errorf("unable to get deployments in the [namespace=%v] [err=%v]", managedSecret.SecretNamespace, err) } managedKubeSecretNameAndNamespace := types.NamespacedName{ - Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace, - Name: infisicalSecret.Spec.ManagedSecretReference.SecretName, + Namespace: managedSecret.SecretNamespace, + Name: managedSecret.SecretName, } managedKubeSecret := &corev1.Secret{} - err = r.Client.Get(ctx, managedKubeSecretNameAndNamespace, managedKubeSecret) + err = client.Get(ctx, managedKubeSecretNameAndNamespace, managedKubeSecret) if err != nil { return 0, fmt.Errorf("unable to fetch Kubernetes secret to update deployment: %v", err) } @@ -39,12 +42,12 @@ func (r *InfisicalSecretReconciler) ReconcileDeploymentsWithManagedSecrets(ctx c // 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" && r.IsDeploymentUsingManagedSecret(deployment, infisicalSecret) { + if deployment.Annotations[AUTO_RELOAD_DEPLOYMENT_ANNOTATION] == "true" && IsDeploymentUsingManagedSecret(deployment, managedSecret) { // Start a goroutine to reconcile the deployment wg.Add(1) - go func(d v1.Deployment, s corev1.Secret) { + go func(deployment v1.Deployment, managedSecret corev1.Secret) { defer wg.Done() - if err := r.ReconcileDeployment(ctx, logger, d, s); err != nil { + 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) @@ -57,8 +60,8 @@ func (r *InfisicalSecretReconciler) ReconcileDeploymentsWithManagedSecrets(ctx c } // Check if the deployment uses managed secrets -func (r *InfisicalSecretReconciler) IsDeploymentUsingManagedSecret(deployment v1.Deployment, infisicalSecret v1alpha1.InfisicalSecret) bool { - managedSecretName := infisicalSecret.Spec.ManagedSecretReference.SecretName +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 { @@ -82,7 +85,7 @@ func (r *InfisicalSecretReconciler) IsDeploymentUsingManagedSecret(deployment v1 // This function ensures that a deployment is in sync with a Kubernetes secret by comparing their versions. // If the version of the secret is different from the version annotation on the deployment, the annotation is updated to trigger a restart of the deployment. -func (r *InfisicalSecretReconciler) ReconcileDeployment(ctx context.Context, logger logr.Logger, deployment v1.Deployment, secret corev1.Secret) error { +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] @@ -101,8 +104,41 @@ func (r *InfisicalSecretReconciler) ReconcileDeployment(ctx context.Context, log deployment.Annotations[annotationKey] = annotationValue deployment.Spec.Template.Annotations[annotationKey] = annotationValue - if err := r.Client.Update(ctx, &deployment); err != nil { + if err := client.Update(ctx, &deployment); err != nil { return fmt.Errorf("failed to update deployment annotation: %v", err) } return nil } + +func GetInfisicalConfigMap(ctx context.Context, client client.Client) (configMap map[string]string, errToReturn error) { + // default key values + defaultConfigMapData := make(map[string]string) + defaultConfigMapData["hostAPI"] = constants.INFISICAL_DOMAIN + + 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 + } +} diff --git a/k8-operator/packages/controllerutil/util.go b/k8-operator/packages/controllerutil/util.go deleted file mode 100644 index 8c610e2e53..0000000000 --- a/k8-operator/packages/controllerutil/util.go +++ /dev/null @@ -1,45 +0,0 @@ -package controllerhelpers - -import ( - "context" - "fmt" - - "github.com/Infisical/infisical/k8-operator/packages/constants" - 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" -) - -func GetInfisicalConfigMap(ctx context.Context, client client.Client) (configMap map[string]string, errToReturn error) { - // default key values - defaultConfigMapData := make(map[string]string) - defaultConfigMapData["hostAPI"] = constants.INFISICAL_DOMAIN - - 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 - } -} diff --git a/k8-operator/packages/model/model.go b/k8-operator/packages/model/model.go index e3328061cc..2dbc6d2595 100644 --- a/k8-operator/packages/model/model.go +++ b/k8-operator/packages/model/model.go @@ -28,3 +28,15 @@ 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"` + } +} diff --git a/k8-operator/packages/util/auth.go b/k8-operator/packages/util/auth.go index d011742775..f4f9348f1b 100644 --- a/k8-operator/packages/util/auth.go +++ b/k8-operator/packages/util/auth.go @@ -63,11 +63,13 @@ var AuthStrategy = struct { type SecretCrdType string var SecretCrd = struct { - INFISICAL_SECRET SecretCrdType - INFISICAL_PUSH_SECRET SecretCrdType + INFISICAL_SECRET SecretCrdType + INFISICAL_PUSH_SECRET SecretCrdType + INFISICAL_DYNAMIC_SECRET SecretCrdType }{ - INFISICAL_SECRET: "INFISICAL_SECRET", - INFISICAL_PUSH_SECRET: "INFISICAL_PUSH_SECRET", + INFISICAL_SECRET: "INFISICAL_SECRET", + INFISICAL_PUSH_SECRET: "INFISICAL_PUSH_SECRET", + INFISICAL_DYNAMIC_SECRET: "INFISICAL_DYNAMIC_SECRET", } type SecretAuthInput struct { @@ -107,6 +109,18 @@ func HandleUniversalAuth(ctx context.Context, reconcilerClient client.Client, se 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{}, + } } universalAuthKubeSecret, err := GetInfisicalUniversalAuthFromKubeSecret(ctx, reconcilerClient, v1alpha1.KubeSecretReference{ @@ -160,6 +174,22 @@ func HandleKubernetesAuth(ctx context.Context, reconcilerClient client.Client, s }, 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") + } + + 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{}, + } } if kubernetesAuthSpec.IdentityID == "" { @@ -208,6 +238,18 @@ func HandleAwsIamAuth(ctx context.Context, reconcilerClient client.Client, secre 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 == "" { @@ -253,6 +295,19 @@ func HandleAzureAuth(ctx context.Context, reconcilerClient client.Client, secret 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 == "" { @@ -296,6 +351,18 @@ func HandleGcpIdTokenAuth(ctx context.Context, reconcilerClient client.Client, s 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 == "" { @@ -340,6 +407,19 @@ func HandleGcpIamAuth(ctx context.Context, reconcilerClient client.Client, secre 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 == "" { diff --git a/k8-operator/packages/util/time.go b/k8-operator/packages/util/helpers.go similarity index 58% rename from k8-operator/packages/util/time.go rename to k8-operator/packages/util/helpers.go index 0b78a16a68..02621dcfdf 100644 --- a/k8-operator/packages/util/time.go +++ b/k8-operator/packages/util/helpers.go @@ -3,10 +3,11 @@ package util import ( "fmt" "strconv" + "strings" "time" ) -func ConvertResyncIntervalToDuration(resyncInterval string) (time.Duration, error) { +func ConvertIntervalToDuration(resyncInterval string) (time.Duration, error) { length := len(resyncInterval) if length < 2 { return 0, fmt.Errorf("invalid format") @@ -38,3 +39,23 @@ func ConvertResyncIntervalToDuration(resyncInterval string) (time.Duration, erro return 0, fmt.Errorf("invalid time unit") } } + +func ConvertIntervalToTime(resyncInterval string) (time.Time, error) { + duration, err := ConvertIntervalToDuration(resyncInterval) + if err != nil { + return time.Time{}, err + } + + // Add duration to current time + return time.Now().Add(duration), nil +} + +func AppendAPIEndpoint(address string) string { + if strings.HasSuffix(address, "/api") { + return address + } + if address[len(address)-1] == '/' { + return address + "api" + } + return address + "/api" +} diff --git a/k8-operator/packages/util/workspace.go b/k8-operator/packages/util/workspace.go new file mode 100644 index 0000000000..ad3694fcf1 --- /dev/null +++ b/k8-operator/packages/util/workspace.go @@ -0,0 +1,27 @@ +package util + +import ( + "fmt" + + "github.com/Infisical/infisical/k8-operator/packages/api" + "github.com/Infisical/infisical/k8-operator/packages/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 +}