feat(k8-operator): dynamic secrets

This commit is contained in:
Daniel Hougaard
2024-12-07 00:13:18 +04:00
parent 0d35273857
commit 4552f0efa4
29 changed files with 1741 additions and 325 deletions

View File

@@ -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"

View File

@@ -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"`
}

View File

@@ -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{})
}

View File

@@ -16,64 +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"`
}
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"`
@@ -92,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"`
@@ -104,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

View File

@@ -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"`

View File

@@ -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 {
// 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(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 {
if in == nil {
return nil
}
out := new(PushSecretUniversalAuth)
out := new(ManagedKubeSecretConfig)
in.DeepCopyInto(out)
return out
}

View File

@@ -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: {}

View File

@@ -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:

View File

@@ -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

View File

@@ -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

View File

@@ -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:

View File

@@ -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

View File

@@ -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)
}

View File

@@ -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
}

View File

@@ -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 != "" {

View File

@@ -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))

View File

@@ -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

View File

@@ -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=

View File

@@ -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)
}

View File

@@ -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 {

View File

@@ -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
}

View File

@@ -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"`
}

View File

@@ -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"
@@ -14,6 +16,7 @@ const OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE = "infisical-operator-system"
const INFISICAL_DOMAIN = "https://app.infisical.com/api"
const INFISICAL_PUSH_SECRET_FINALIZER_NAME = "pushsecret.secrets.infisical.com/finalizer"
const INFISICAL_DYNAMIC_SECRET_FINALIZER_NAME = "dynamicsecret.secrets.infisical.com/finalizer"
type PushSecretReplacePolicy string
type PushSecretDeletionPolicy string
@@ -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")

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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"`
}
}

View File

@@ -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 == "" {

View File

@@ -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"
}

View File

@@ -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
}