From 922b24578092b0390c3ad732ab6b176e74da6a4f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 29 Nov 2024 03:05:50 +0400 Subject: [PATCH 01/32] feat(k8-operator): push secrets --- .../api/v1alpha1/infisicalpushsecret_types.go | 141 ++++++ .../api/v1alpha1/zz_generated.deepcopy.go | 267 ++++++++++ ...ts.infisical.com_infisicalpushsecrets.yaml | 265 ++++++++++ k8-operator/config/crd/kustomization.yaml | 5 +- k8-operator/config/rbac/role.yaml | 26 + .../samples/crd/pushsecret/pushSecret.yaml | 45 ++ .../samples/crd/pushsecret/sourceSecret.yaml | 9 + .../infisicalpushsecret/conditions.go | 155 ++++++ .../infisicalpushsecret_controller.go | 233 +++++++++ .../infisicalpushsecret_helper.go | 473 ++++++++++++++++++ .../auto_redeployment.go | 16 +- .../{ => infisicalsecret}/conditions.go | 10 +- .../infisicalsecret_controller.go | 76 +-- .../infisicalsecret_helper.go | 223 +++------ .../{ => infisicalsecret}/suite_test.go | 0 .../controllers/infisicalsecret_auth.go | 150 ------ .../install-secrets-operator.yaml | 209 ++++++++ k8-operator/main.go | 20 +- k8-operator/packages/api/api.go | 4 - k8-operator/packages/constants/constants.go | 24 + k8-operator/packages/controllerutil/util.go | 45 ++ k8-operator/packages/util/auth.go | 326 ++++++++++++ k8-operator/packages/util/kubernetes.go | 50 ++ k8-operator/packages/util/models.go | 13 + k8-operator/packages/util/time.go | 40 ++ 25 files changed, 2477 insertions(+), 348 deletions(-) create mode 100644 k8-operator/api/v1alpha1/infisicalpushsecret_types.go create mode 100644 k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml create mode 100644 k8-operator/config/samples/crd/pushsecret/pushSecret.yaml create mode 100644 k8-operator/config/samples/crd/pushsecret/sourceSecret.yaml create mode 100644 k8-operator/controllers/infisicalpushsecret/conditions.go create mode 100644 k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go create mode 100644 k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go rename k8-operator/controllers/{ => infisicalsecret}/auto_redeployment.go (81%) rename k8-operator/controllers/{ => infisicalsecret}/conditions.go (86%) rename k8-operator/controllers/{ => infisicalsecret}/infisicalsecret_controller.go (64%) rename k8-operator/controllers/{ => infisicalsecret}/infisicalsecret_helper.go (57%) rename k8-operator/controllers/{ => infisicalsecret}/suite_test.go (100%) delete mode 100644 k8-operator/controllers/infisicalsecret_auth.go create mode 100644 k8-operator/packages/constants/constants.go create mode 100644 k8-operator/packages/controllerutil/util.go create mode 100644 k8-operator/packages/util/kubernetes.go create mode 100644 k8-operator/packages/util/models.go create mode 100644 k8-operator/packages/util/time.go diff --git a/k8-operator/api/v1alpha1/infisicalpushsecret_types.go b/k8-operator/api/v1alpha1/infisicalpushsecret_types.go new file mode 100644 index 000000000..336534ed2 --- /dev/null +++ b/k8-operator/api/v1alpha1/infisicalpushsecret_types.go @@ -0,0 +1,141 @@ +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +type InfisicalPushSecretDestination struct { + // +kubebuilder:validation:Required + // +kubebuilder:validation:Immutable + SecretsPath string `json:"secretsPath"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:Immutable + EnvSlug string `json:"envSlug"` + // +kubebuilder:validation:Required + // +kubebuilder:validation:Immutable + 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"` +} + +// InfisicalPushSecretSpec defines the desired state of InfisicalPushSecret +type InfisicalPushSecretSpec struct { + // +kubebuilder:validation:Optional + UpdatePolicy string `json:"updatePolicy"` + + // +kubebuilder:validation:Optional + DeletionPolicy string `json:"deletionPolicy"` + + // +kubebuilder:validation:Required + // +kubebuilder:validation:Immutable + Destination InfisicalPushSecretDestination `json:"destination"` + + // +kubebuilder:validation:Optional + Authentication PushSecretAuthentication `json:"authentication"` + + // +kubebuilder:validation:Required + Push SecretPush `json:"push"` + + ResyncInterval string `json:"resyncInterval"` + + // Infisical host to pull secrets from + // +kubebuilder:validation:Optional + HostAPI string `json:"hostAPI"` + + // +kubebuilder:validation:Optional + TLS PushSecretTlsConfig `json:"tls"` +} + +// InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret +type InfisicalPushSecretStatus struct { + Conditions []metav1.Condition `json:"conditions"` + + // managed secrets is a map where the key is the ID, and the value is the secret key (string[id], string[key] ) + ManagedSecrets map[string]string `json:"managedSecrets"` +} + +// +kubebuilder:object:root=true +// +kubebuilder:subresource:status +// InfisicalPushSecret is the Schema for the infisicalpushsecrets API +type InfisicalPushSecret struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec InfisicalPushSecretSpec `json:"spec,omitempty"` + Status InfisicalPushSecretStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// InfisicalPushSecretList contains a list of InfisicalPushSecret +type InfisicalPushSecretList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []InfisicalPushSecret `json:"items"` +} + +func init() { + SchemeBuilder.Register(&InfisicalPushSecret{}, &InfisicalPushSecretList{}) +} diff --git a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go index 41e4d3f20..fa54d4130 100644 --- a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -128,6 +128,128 @@ 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 *InfisicalPushSecret) DeepCopyInto(out *InfisicalPushSecret) { + *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 InfisicalPushSecret. +func (in *InfisicalPushSecret) DeepCopy() *InfisicalPushSecret { + if in == nil { + return nil + } + out := new(InfisicalPushSecret) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *InfisicalPushSecret) 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 *InfisicalPushSecretDestination) DeepCopyInto(out *InfisicalPushSecretDestination) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalPushSecretDestination. +func (in *InfisicalPushSecretDestination) DeepCopy() *InfisicalPushSecretDestination { + if in == nil { + return nil + } + out := new(InfisicalPushSecretDestination) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfisicalPushSecretList) DeepCopyInto(out *InfisicalPushSecretList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]InfisicalPushSecret, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalPushSecretList. +func (in *InfisicalPushSecretList) DeepCopy() *InfisicalPushSecretList { + if in == nil { + return nil + } + out := new(InfisicalPushSecretList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *InfisicalPushSecretList) 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 *InfisicalPushSecretSpec) DeepCopyInto(out *InfisicalPushSecretSpec) { + *out = *in + out.Destination = in.Destination + out.Authentication = in.Authentication + out.Push = in.Push + out.TLS = in.TLS +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalPushSecretSpec. +func (in *InfisicalPushSecretSpec) DeepCopy() *InfisicalPushSecretSpec { + if in == nil { + return nil + } + out := new(InfisicalPushSecretSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *InfisicalPushSecretStatus) DeepCopyInto(out *InfisicalPushSecretStatus) { + *out = *in + if in.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.ManagedSecrets != nil { + in, out := &in.ManagedSecrets, &out.ManagedSecrets + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new InfisicalPushSecretStatus. +func (in *InfisicalPushSecretStatus) DeepCopy() *InfisicalPushSecretStatus { + if in == nil { + return nil + } + out := new(InfisicalPushSecretStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *InfisicalSecret) DeepCopyInto(out *InfisicalSecret) { *out = *in @@ -332,6 +454,151 @@ func (in *MangedKubeSecretConfig) DeepCopy() *MangedKubeSecretConfig { 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) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SecretPush) DeepCopyInto(out *SecretPush) { + *out = *in + out.Secret = in.Secret +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretPush. +func (in *SecretPush) DeepCopy() *SecretPush { + if in == nil { + return nil + } + out := new(SecretPush) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretScopeInWorkspace) DeepCopyInto(out *SecretScopeInWorkspace) { *out = *in diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml new file mode 100644 index 000000000..a4ee1e21f --- /dev/null +++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml @@ -0,0 +1,265 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + creationTimestamp: null + name: infisicalpushsecrets.secrets.infisical.com +spec: + group: secrets.infisical.com + names: + kind: InfisicalPushSecret + listKind: InfisicalPushSecretList + plural: infisicalpushsecrets + singular: infisicalpushsecret + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: InfisicalPushSecret is the Schema for the infisicalpushsecrets + 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: InfisicalPushSecretSpec defines the desired state of InfisicalPushSecret + 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: + description: Rest of your types should be defined similarly... + properties: + identityId: + type: string + serviceAccountRef: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + required: + - identityId + - serviceAccountRef + type: object + universalAuth: + description: PushSecretUniversalAuth defines universal authentication + 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 + deletionPolicy: + type: string + destination: + properties: + envSlug: + type: string + projectId: + type: string + secretsPath: + type: string + required: + - envSlug + - projectId + - secretsPath + type: object + hostAPI: + description: Infisical host to pull secrets from + type: string + push: + properties: + secret: + 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: + - secret + type: object + resyncInterval: + type: string + 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 + updatePolicy: + type: string + required: + - destination + - push + - resyncInterval + type: object + status: + description: InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + managedSecrets: + additionalProperties: + type: string + description: managed secrets is a map where the key is the ID, and + the value is the secret key (string[id], string[key] ) + type: object + required: + - conditions + - managedSecrets + 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 ab2a736e1..bb7b81e6b 100644 --- a/k8-operator/config/crd/kustomization.yaml +++ b/k8-operator/config/crd/kustomization.yaml @@ -2,7 +2,8 @@ # since it depends on service name and namespace that are out of this kustomize package. # It should be run by config/default resources: -- bases/secrets.infisical.com_infisicalsecrets.yaml + - bases/secrets.infisical.com_infisicalsecrets.yaml + - bases/secrets.infisical.com_infisicalpushsecrets.yaml #+kubebuilder:scaffold:crdkustomizeresource patchesStrategicMerge: @@ -18,4 +19,4 @@ patchesStrategicMerge: # the following config is for teaching kustomize how to do kustomization for CRDs. configurations: -- kustomizeconfig.yaml + - kustomizeconfig.yaml diff --git a/k8-operator/config/rbac/role.yaml b/k8-operator/config/rbac/role.yaml index 10c2af414..00fab4a89 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: + - infisicalpushsecrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - secrets.infisical.com + resources: + - infisicalpushsecrets/finalizers + verbs: + - update +- apiGroups: + - secrets.infisical.com + resources: + - infisicalpushsecrets/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 new file mode 100644 index 000000000..8df2d3da5 --- /dev/null +++ b/k8-operator/config/samples/crd/pushsecret/pushSecret.yaml @@ -0,0 +1,45 @@ +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalPushSecret +metadata: + name: infisical-push-secret-demo +spec: + resyncInterval: 1m + hostAPI: https://app.infisical.com/api + + # Optional, defaults to replacement. + updatePolicy: Replace # If set to replace, existing secrets inside Infisical will be replaced by the value of the PushSecret on sync. + + # Optional, defaults to no deletion. + deletionPolicy: Delete # If set to delete, the secret(s) inside Infisical managed by the operator, will be deleted if the InfisicalPushSecret CRD is deleted. + + destination: + projectId: + envSlug: + secretsPath: + + push: + secret: + secretName: push-secret-demo # Secret CRD + secretNamespace: default + + # Only have one authentication method defined or you are likely to run into authentication issues. + # Remove all except one authentication method. + authentication: + awsIamAuth: + identityId: + azureAuth: + identityId: + gcpIamAuth: + identityId: + serviceAccountKeyFilePath: + gcpIdTokenAuth: + identityId: + kubernetesAuth: + identityId: + serviceAccountRef: + name: + namespace: + universalAuth: + credentialsRef: + secretName: # universal-auth-credentials + secretNamespace: # default diff --git a/k8-operator/config/samples/crd/pushsecret/sourceSecret.yaml b/k8-operator/config/samples/crd/pushsecret/sourceSecret.yaml new file mode 100644 index 000000000..6a3703a95 --- /dev/null +++ b/k8-operator/config/samples/crd/pushsecret/sourceSecret.yaml @@ -0,0 +1,9 @@ +apiVersion: v1 +kind: Secret +metadata: + name: push-secret-demo + namespace: default +stringData: # can also be "data", but needs to be base64 encoded + API_KEY: some-api-key + DATABASE_URL: postgres://127.0.0.1:5432 + ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab diff --git a/k8-operator/controllers/infisicalpushsecret/conditions.go b/k8-operator/controllers/infisicalpushsecret/conditions.go new file mode 100644 index 000000000..bd1851eb2 --- /dev/null +++ b/k8-operator/controllers/infisicalpushsecret/conditions.go @@ -0,0 +1,155 @@ +package controllers + +import ( + "context" + + "github.com/Infisical/infisical/k8-operator/api/v1alpha1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (r *InfisicalPushSecretReconciler) SetSuccessfullyReconciledConditions(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, err error) error { + + if infisicalPushSecret.Status.Conditions == nil { + infisicalPushSecret.Status.Conditions = []metav1.Condition{} + } + + if err != nil { + meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/SuccessfullyReconciled", + Status: metav1.ConditionTrue, + Reason: "Error", + Message: "Reconcile failed, secrets were not pushed to Infisical. Check operator logs for more info", + }) + } else { + meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/SuccessfullyReconciled", + Status: metav1.ConditionFalse, + Reason: "OK", + Message: "Reconcile succeeded, secrets were pushed to Infisical", + }) + } + + return r.Client.Status().Update(ctx, infisicalPushSecret) + +} + +func (r *InfisicalPushSecretReconciler) SetFailedToReplaceSecretsConditions(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, failMessage string) error { + if infisicalPushSecret.Status.Conditions == nil { + infisicalPushSecret.Status.Conditions = []metav1.Condition{} + } + + if failMessage != "" { + meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/FailedToReplaceSecrets", + Status: metav1.ConditionTrue, + Reason: "Error", + Message: failMessage, + }) + } else { + meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/FailedToReplaceSecrets", + Status: metav1.ConditionFalse, + Reason: "OK", + Message: "No errors, no secrets failed to be replaced", + }) + } + + return r.Client.Status().Update(ctx, infisicalPushSecret) +} + +func (r *InfisicalPushSecretReconciler) SetFailedToCreateSecretsConditions(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, failMessage string) error { + if infisicalPushSecret.Status.Conditions == nil { + infisicalPushSecret.Status.Conditions = []metav1.Condition{} + } + + if failMessage != "" { + meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/FailedToCreateSecrets", + Status: metav1.ConditionTrue, + Reason: "Error", + Message: failMessage, + }) + } else { + meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/FailedToCreateSecrets", + Status: metav1.ConditionFalse, + Reason: "OK", + Message: "No errors, no secrets failed to be created", + }) + } + + return r.Client.Status().Update(ctx, infisicalPushSecret) +} + +func (r *InfisicalPushSecretReconciler) SetFailedToUpdateSecretsConditions(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, failMessage string) error { + if infisicalPushSecret.Status.Conditions == nil { + infisicalPushSecret.Status.Conditions = []metav1.Condition{} + } + + if failMessage != "" { + meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/FailedToUpdateSecrets", + Status: metav1.ConditionTrue, + Reason: "Error", + Message: failMessage, + }) + } else { + meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/FailedToUpdateSecrets", + Status: metav1.ConditionFalse, + Reason: "OK", + Message: "No errors, no secrets failed to be updated", + }) + } + + return r.Client.Status().Update(ctx, infisicalPushSecret) +} + +func (r *InfisicalPushSecretReconciler) SetFailedToDeleteSecretsConditions(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, failMessage string) error { + if infisicalPushSecret.Status.Conditions == nil { + infisicalPushSecret.Status.Conditions = []metav1.Condition{} + } + + if failMessage != "" { + meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/FailedToDeleteSecrets", + Status: metav1.ConditionTrue, + Reason: "Error", + Message: failMessage, + }) + } else { + meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/FailedToDeleteSecrets", + Status: metav1.ConditionFalse, + Reason: "OK", + Message: "No errors, no secrets failed to be deleted", + }) + } + + return r.Client.Status().Update(ctx, infisicalPushSecret) +} + +func (r *InfisicalPushSecretReconciler) SetAuthenticatedConditions(ctx context.Context, infisicalPushSecret *v1alpha1.InfisicalPushSecret, errorToConditionOn error) error { + if infisicalPushSecret.Status.Conditions == nil { + infisicalPushSecret.Status.Conditions = []metav1.Condition{} + } + + if errorToConditionOn != nil { + meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/Authenticated", + Status: metav1.ConditionFalse, + Reason: "Error", + Message: "Failed to authenticate with Infisical API. This can be caused by invalid service token or an invalid API host that is set. Check operator logs for more info", + }) + } else { + meta.SetStatusCondition(&infisicalPushSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/Authenticated", + Status: metav1.ConditionTrue, + Reason: "OK", + Message: "Successfully authenticated with Infisical API", + }) + } + + return r.Client.Status().Update(ctx, infisicalPushSecret) +} diff --git a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go new file mode 100644 index 000000000..f52f27881 --- /dev/null +++ b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go @@ -0,0 +1,233 @@ +package controllers + +import ( + "context" + "fmt" + "time" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/types" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/builder" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/event" + "sigs.k8s.io/controller-runtime/pkg/handler" + "sigs.k8s.io/controller-runtime/pkg/predicate" + "sigs.k8s.io/controller-runtime/pkg/reconcile" + "sigs.k8s.io/controller-runtime/pkg/source" + + 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" + "github.com/Infisical/infisical/k8-operator/packages/util" + "github.com/go-logr/logr" +) + +// InfisicalSecretReconciler reconciles a InfisicalSecret object +type InfisicalPushSecretReconciler struct { + client.Client + + BaseLogger logr.Logger + Scheme *runtime.Scheme +} + +var resourceVariablesMap map[string]util.ResourceVariables + +func (r *InfisicalPushSecretReconciler) GetLogger(req ctrl.Request) logr.Logger { + return r.BaseLogger.WithValues("infisicalpushsecret", req.NamespacedName) +} + +//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalpushsecrets,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalpushsecrets/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalpushsecrets/finalizers,verbs=update +//+kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;delete +//+kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;delete +//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;get;update +//+kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch + +// Reconcile is part of the main kubernetes reconciliation loop which aims to +// move the current state of the cluster closer to the desired state. +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.1/pkg/reconcile + +func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + + logger := r.GetLogger(req) + + var infisicalPushSecretCR secretsv1alpha1.InfisicalPushSecret + requeueTime := time.Minute // seconds + + if resourceVariablesMap == nil { + resourceVariablesMap = make(map[string]util.ResourceVariables) + } + + err := r.Get(ctx, req.NamespacedName, &infisicalPushSecretCR) + if err != nil { + if errors.IsNotFound(err) { + logger.Info("Infisical Push Secret CRD not found") + return ctrl.Result{ + Requeue: false, + }, nil + } else { + logger.Error(err, "Unable to fetch Infisical Secret CRD from cluster") + return ctrl.Result{ + RequeueAfter: requeueTime, + }, nil + } + } + + // Add finalizer if it doesn't exist + if !controllerutil.ContainsFinalizer(&infisicalPushSecretCR, constants.INFISICAL_PUSH_SECRET_FINALIZER_NAME) { + controllerutil.AddFinalizer(&infisicalPushSecretCR, constants.INFISICAL_PUSH_SECRET_FINALIZER_NAME) + if err := r.Update(ctx, &infisicalPushSecretCR); err != nil { + return ctrl.Result{}, err + } + } + + // Check if it's being deleted + if !infisicalPushSecretCR.DeletionTimestamp.IsZero() { + logger.Info("Handling deletion of InfisicalPushSecret") + if controllerutil.ContainsFinalizer(&infisicalPushSecretCR, constants.INFISICAL_PUSH_SECRET_FINALIZER_NAME) { + // We remove finalizers before running deletion logic to be completely safe from stuck resources + infisicalPushSecretCR.ObjectMeta.Finalizers = []string{} + if err := r.Update(ctx, &infisicalPushSecretCR); err != nil { + logger.Error(err, fmt.Sprintf("Error removing finalizers from InfisicalPushSecret %s", infisicalPushSecretCR.Name)) + return ctrl.Result{}, err + } + + if err := r.DeleteManagedSecrets(ctx, logger, infisicalPushSecretCR); err != nil { + return ctrl.Result{}, err // Even if this fails, we still want to delete the CRD + } + + } + return ctrl.Result{}, nil + } + + if infisicalPushSecretCR.Spec.ResyncInterval != "" { + + duration, err := util.ConvertResyncIntervalToDuration(infisicalPushSecretCR.Spec.ResyncInterval) + + if err != nil { + logger.Error(err, fmt.Sprintf("unable to convert resync interval to duration. Will requeue after [requeueTime=%v]", requeueTime)) + return ctrl.Result{ + RequeueAfter: requeueTime, + }, nil + } + + requeueTime = duration + + logger.Info(fmt.Sprintf("Manual re-sync interval set. Interval: %v", requeueTime)) + + } else { + logger.Info(fmt.Sprintf("Re-sync interval set. Interval: %v", requeueTime)) + } + + // Check if the resource is already marked for deletion + if infisicalPushSecretCR.GetDeletionTimestamp() != nil { + return ctrl.Result{ + Requeue: false, + }, 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 infisicalPushSecretCR.Spec.HostAPI == "" { + api.API_HOST_URL = infisicalConfig["hostAPI"] + } else { + api.API_HOST_URL = infisicalPushSecretCR.Spec.HostAPI + } + + if infisicalPushSecretCR.Spec.TLS.CaRef.SecretName != "" { + api.API_CA_CERTIFICATE, err = r.getInfisicalCaCertificateFromKubeSecret(ctx, infisicalPushSecretCR) + 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 + } + + fmt.Println("Using custom CA certificate...") + } else { + api.API_CA_CERTIFICATE = "" + } + + err = r.ReconcileInfisicalPushSecret(ctx, logger, infisicalPushSecretCR) + r.SetSuccessfullyReconciledConditions(ctx, &infisicalPushSecretCR, err) + + 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 + } + + // Sync again after the specified time + logger.Info(fmt.Sprintf("Operator will requeue after [%v]", requeueTime)) + return ctrl.Result{ + RequeueAfter: requeueTime, + }, nil +} + +func (r *InfisicalPushSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { + + // Custom predicate that allows both spec changes and deletions + specChangeOrDelete := predicate.Funcs{ + UpdateFunc: func(e event.UpdateEvent) bool { + // Only reconcile if spec/generation changed + return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() + }, + DeleteFunc: func(e event.DeleteEvent) bool { + // Always reconcile on deletion + 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.InfisicalPushSecret{}, builder.WithPredicates( + specChangeOrDelete, + )). + Watches( + &source.Kind{Type: &corev1.Secret{}}, + handler.EnqueueRequestsFromMapFunc(func(o client.Object) []reconcile.Request { + ctx := context.Background() + pushSecrets := &secretsv1alpha1.InfisicalPushSecretList{} + if err := r.List(ctx, pushSecrets); err != nil { + return []reconcile.Request{} + } + + requests := []reconcile.Request{} + for _, pushSecret := range pushSecrets.Items { + if pushSecret.Spec.Push.Secret.SecretName == o.GetName() && + pushSecret.Spec.Push.Secret.SecretNamespace == o.GetNamespace() { + requests = append(requests, reconcile.Request{ + NamespacedName: types.NamespacedName{ + Name: pushSecret.GetName(), + Namespace: pushSecret.GetNamespace(), + }, + }) + } + } + return requests + }), + ). + Complete(r) +} diff --git a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go new file mode 100644 index 000000000..a13452b69 --- /dev/null +++ b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go @@ -0,0 +1,473 @@ +package controllers + +import ( + "context" + "errors" + "fmt" + "strings" + + "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" + + infisicalSdk "github.com/infisical/go-sdk" + k8Errors "k8s.io/apimachinery/pkg/api/errors" +) + +func (r *InfisicalPushSecretReconciler) handleAuthentication(ctx context.Context, infisicalSecret v1alpha1.InfisicalPushSecret, 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_PUSH_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 *InfisicalPushSecretReconciler) getInfisicalCaCertificateFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalPushSecret) (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 *InfisicalPushSecretReconciler) getResourceVariables(infisicalPushSecret v1alpha1.InfisicalPushSecret) util.ResourceVariables { + + var resourceVariables util.ResourceVariables + + if _, ok := resourceVariablesMap[string(infisicalPushSecret.UID)]; !ok { + + ctx, cancel := context.WithCancel(context.Background()) + + client := infisicalSdk.NewInfisicalClient(ctx, infisicalSdk.Config{ + SiteUrl: api.API_HOST_URL, + CaCertificate: api.API_CA_CERTIFICATE, + UserAgent: api.USER_AGENT_NAME, + }) + + resourceVariablesMap[string(infisicalPushSecret.UID)] = util.ResourceVariables{ + InfisicalClient: client, + CancelCtx: cancel, + AuthDetails: util.AuthenticationDetails{}, + } + + resourceVariables = resourceVariablesMap[string(infisicalPushSecret.UID)] + + } else { + resourceVariables = resourceVariablesMap[string(infisicalPushSecret.UID)] + } + + return resourceVariables + +} + +func (r *InfisicalPushSecretReconciler) updateResourceVariables(infisicalPushSecret v1alpha1.InfisicalPushSecret, resourceVariables util.ResourceVariables) { + resourceVariablesMap[string(infisicalPushSecret.UID)] = resourceVariables +} + +func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context.Context, logger logr.Logger, infisicalPushSecret v1alpha1.InfisicalPushSecret) error { + + resourceVariables := r.getResourceVariables(infisicalPushSecret) + infisicalClient := resourceVariables.InfisicalClient + cancelCtx := resourceVariables.CancelCtx + authDetails := resourceVariables.AuthDetails + var err error + + if authDetails.AuthStrategy == "" { + logger.Info("No authentication strategy found. Attempting to authenticate") + authDetails, err = r.handleAuthentication(ctx, infisicalPushSecret, infisicalClient) + r.SetAuthenticatedConditions(ctx, &infisicalPushSecret, err) + + if err != nil { + return fmt.Errorf("unable to authenticate [err=%s]", err) + } + + r.updateResourceVariables(infisicalPushSecret, util.ResourceVariables{ + InfisicalClient: infisicalClient, + CancelCtx: cancelCtx, + AuthDetails: authDetails, + }) + } + + kubePushSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{ + Namespace: infisicalPushSecret.Spec.Push.Secret.SecretNamespace, + Name: infisicalPushSecret.Spec.Push.Secret.SecretName, + }) + + if err != nil { + return fmt.Errorf("unable to fetch kube secret [err=%s]", err) + } + + var kubeSecrets = make(map[string]string) + + for key, value := range kubePushSecret.Data { + kubeSecrets[key] = string(value) + } + + destination := infisicalPushSecret.Spec.Destination + existingSecrets, err := infisicalClient.Secrets().List(infisicalSdk.ListSecretsOptions{ + ProjectID: destination.ProjectID, + Environment: destination.EnvSlug, + SecretPath: destination.SecretsPath, + IncludeImports: false, + }) + + existingSecretsContainsKey := func(key string) bool { + for _, secret := range existingSecrets { + if secret.SecretKey == key { + return true + } + } + return false + } + + getExistingSecretByKey := func(key string) *infisicalSdk.Secret { + for _, secret := range existingSecrets { + if secret.SecretKey == key { + return &secret + } + } + return nil + } + + getExistingSecretById := func(id string) *infisicalSdk.Secret { + for _, secret := range existingSecrets { + if secret.ID == id { + return &secret + } + } + return nil + } + + if err != nil { + return fmt.Errorf("unable to list secrets [err=%s]", err) + } + + updatePolicy := infisicalPushSecret.Spec.UpdatePolicy + + var secretsFailedToCreate []string + var secretsFailedToUpdate []string + var secretsFailedToDelete []string + var secretsFailedToReplaceById []string + + // If the ManagedSecrets are nil, we know this is the first time the InfisicalPushSecret is being reconciled. + if infisicalPushSecret.Status.ManagedSecrets == nil { + + infisicalPushSecret.Status.ManagedSecrets = make(map[string]string) // (string[id], string[key] ) + + for secretKey, secretValue := range kubeSecrets { + if existingSecretsContainsKey(secretKey) { + if updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) { + updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{ + SecretKey: secretKey, + ProjectID: destination.ProjectID, + Environment: destination.EnvSlug, + SecretPath: destination.SecretsPath, + NewSecretValue: secretValue, + }) + + if err != nil { + secretsFailedToUpdate = append(secretsFailedToUpdate, secretKey) + logger.Info(fmt.Sprintf("unable to update secret [key=%s] [err=%s]", secretKey, err)) + continue + } + + infisicalPushSecret.Status.ManagedSecrets[updatedSecret.ID] = secretKey + } + } else { + createdSecret, err := infisicalClient.Secrets().Create(infisicalSdk.CreateSecretOptions{ + SecretKey: secretKey, + SecretValue: secretValue, + ProjectID: destination.ProjectID, + Environment: destination.EnvSlug, + SecretPath: destination.SecretsPath, + }) + + if err != nil { + secretsFailedToCreate = append(secretsFailedToCreate, secretKey) + logger.Info(fmt.Sprintf("unable to create secret [key=%s] [err=%s]", secretKey, err)) + continue + } + + infisicalPushSecret.Status.ManagedSecrets[createdSecret.ID] = secretKey + } + } + } else { + + // Loop over all the managed secrets, and find the corresponding existingSecret that has the same ID. If the key doesn't match, delete the secret, and re-create it with the correct key/value + for managedSecretId, managedSecretKey := range infisicalPushSecret.Status.ManagedSecrets { + + existingSecret := getExistingSecretById(managedSecretId) + + if existingSecret != nil { + + if existingSecret.SecretKey != managedSecretKey { + // Secret key has changed, lets delete the secret and re-create it with the correct key + + logger.Info(fmt.Sprintf("Secret with ID [id=%s] has changed key from [%s] to [%s]. Deleting and re-creating secret", managedSecretId, managedSecretKey, existingSecret.SecretKey)) + + deletedSecret, err := infisicalClient.Secrets().Delete(infisicalSdk.DeleteSecretOptions{ + SecretKey: existingSecret.SecretKey, + ProjectID: destination.ProjectID, + Environment: destination.EnvSlug, + SecretPath: destination.SecretsPath, + }) + + if err != nil { + secretsFailedToReplaceById = append(secretsFailedToReplaceById, managedSecretKey) + logger.Info(fmt.Sprintf("unable to delete secret [key=%s] [err=%s]", managedSecretKey, err)) + continue + } + + createdSecret, err := infisicalClient.Secrets().Create(infisicalSdk.CreateSecretOptions{ + SecretKey: managedSecretKey, + SecretValue: existingSecret.SecretValue, + ProjectID: destination.ProjectID, + Environment: destination.EnvSlug, + SecretPath: destination.SecretsPath, + }) + + if err != nil { + secretsFailedToReplaceById = append(secretsFailedToReplaceById, managedSecretKey) + logger.Info(fmt.Sprintf("unable to create secret [key=%s] [err=%s]", managedSecretKey, err)) + continue + } + + delete(infisicalPushSecret.Status.ManagedSecrets, deletedSecret.ID) + infisicalPushSecret.Status.ManagedSecrets[createdSecret.ID] = managedSecretKey + } + + } + } + + // We need to check if any of the secrets have been removed in the new kube secret + for _, managedSecretKey := range infisicalPushSecret.Status.ManagedSecrets { + + if _, ok := kubeSecrets[managedSecretKey]; !ok { + + // Secret has been removed, verify that the secret is managed by the operator + if getExistingSecretByKey(managedSecretKey) != nil { + logger.Info(fmt.Sprintf("Secret with key [key=%s] has been removed from the kube secret. Deleting secret from Infisical", managedSecretKey)) + + deletedSecret, err := infisicalClient.Secrets().Delete(infisicalSdk.DeleteSecretOptions{ + SecretKey: managedSecretKey, + ProjectID: destination.ProjectID, + Environment: destination.EnvSlug, + SecretPath: destination.SecretsPath, + }) + + if err != nil { + secretsFailedToDelete = append(secretsFailedToDelete, managedSecretKey) + logger.Info(fmt.Sprintf("unable to delete secret [key=%s] [err=%s]", managedSecretKey, err)) + continue + } + + delete(infisicalPushSecret.Status.ManagedSecrets, deletedSecret.ID) + } + } + } + + // We need to check if any new secrets have been added in the kube secret + for currentSecretKey := range kubeSecrets { + + if !existingSecretsContainsKey(currentSecretKey) { + + // Some secrets has been added, verify that the secret that has been added is not already managed by the operator + if _, ok := infisicalPushSecret.Status.ManagedSecrets[currentSecretKey]; !ok { + + // Secret was not managed by the operator, lets add it + logger.Info(fmt.Sprintf("Secret with key [key=%s] has been added to the kube secret. Creating secret in Infisical", currentSecretKey)) + + createdSecret, err := infisicalClient.Secrets().Create(infisicalSdk.CreateSecretOptions{ + SecretKey: currentSecretKey, + SecretValue: kubeSecrets[currentSecretKey], + ProjectID: destination.ProjectID, + Environment: destination.EnvSlug, + SecretPath: destination.SecretsPath, + }) + + if err != nil { + secretsFailedToCreate = append(secretsFailedToCreate, currentSecretKey) + logger.Info(fmt.Sprintf("unable to create secret [key=%s] [err=%s]", currentSecretKey, err)) + continue + } + + infisicalPushSecret.Status.ManagedSecrets[createdSecret.ID] = currentSecretKey + } + } else { + if updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) { + updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{ + SecretKey: currentSecretKey, + NewSecretValue: kubeSecrets[currentSecretKey], + ProjectID: destination.ProjectID, + Environment: destination.EnvSlug, + SecretPath: destination.SecretsPath, + }) + + if err != nil { + secretsFailedToUpdate = append(secretsFailedToUpdate, currentSecretKey) + logger.Info(fmt.Sprintf("unable to update secret [key=%s] [err=%s]", currentSecretKey, err)) + continue + } + + infisicalPushSecret.Status.ManagedSecrets[updatedSecret.ID] = currentSecretKey + } + } + } + + // Check if any of the existing secrets values have changed + for secretKey, secretValue := range kubeSecrets { + + existingSecret := getExistingSecretByKey(secretKey) + + if existingSecret != nil { + + _, managedByOperator := infisicalPushSecret.Status.ManagedSecrets[existingSecret.ID] + + if secretValue != existingSecret.SecretValue { + + if managedByOperator || updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) { + logger.Info(fmt.Sprintf("Secret with key [key=%s] has changed value. Updating secret in Infisical", secretKey)) + + updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{ + SecretKey: secretKey, + NewSecretValue: secretValue, + ProjectID: destination.ProjectID, + Environment: destination.EnvSlug, + SecretPath: destination.SecretsPath, + }) + + if err != nil { + secretsFailedToUpdate = append(secretsFailedToUpdate, secretKey) + logger.Info(fmt.Sprintf("unable to update secret [key=%s] [err=%s]", secretKey, err)) + continue + } + + infisicalPushSecret.Status.ManagedSecrets[updatedSecret.ID] = secretKey + } + } + } + } + } + + var errorMessage string + if len(secretsFailedToCreate) > 0 { + errorMessage = fmt.Sprintf("Failed to create secrets: [%s]", strings.Join(secretsFailedToCreate, ", ")) + } else { + errorMessage = "" + } + r.SetFailedToCreateSecretsConditions(ctx, &infisicalPushSecret, fmt.Sprintf("Failed to create secrets: [%s]", errorMessage)) + + if len(secretsFailedToUpdate) > 0 { + errorMessage = fmt.Sprintf("Failed to update secrets: [%s]", strings.Join(secretsFailedToUpdate, ", ")) + } else { + errorMessage = "" + } + r.SetFailedToUpdateSecretsConditions(ctx, &infisicalPushSecret, fmt.Sprintf("Failed to update secrets: [%s]", errorMessage)) + + if len(secretsFailedToDelete) > 0 { + errorMessage = fmt.Sprintf("Failed to delete secrets: [%s]", strings.Join(secretsFailedToDelete, ", ")) + } else { + errorMessage = "" + } + r.SetFailedToDeleteSecretsConditions(ctx, &infisicalPushSecret, errorMessage) + + if len(secretsFailedToReplaceById) > 0 { + errorMessage = fmt.Sprintf("Failed to replace secrets: [%s]", strings.Join(secretsFailedToReplaceById, ", ")) + } else { + errorMessage = "" + } + r.SetFailedToReplaceSecretsConditions(ctx, &infisicalPushSecret, errorMessage) + + // Update the status of the InfisicalPushSecret + if err := r.Client.Status().Update(ctx, &infisicalPushSecret); err != nil { + return fmt.Errorf("unable to update status of InfisicalPushSecret [err=%s]", err) + } + + return nil + +} + +func (r *InfisicalPushSecretReconciler) DeleteManagedSecrets(ctx context.Context, logger logr.Logger, infisicalPushSecret v1alpha1.InfisicalPushSecret) error { + if infisicalPushSecret.Spec.DeletionPolicy != string(constants.PUSH_SECRET_DELETE_POLICY_ENABLED) { + return nil + } + + resourceVariables := r.getResourceVariables(infisicalPushSecret) + infisicalClient := resourceVariables.InfisicalClient + + destination := infisicalPushSecret.Spec.Destination + existingSecrets, err := infisicalClient.Secrets().List(infisicalSdk.ListSecretsOptions{ + ProjectID: destination.ProjectID, + Environment: destination.EnvSlug, + SecretPath: destination.SecretsPath, + IncludeImports: false, + }) + + if err != nil { + return fmt.Errorf("unable to list secrets [err=%s]", err) + } + + existingSecretsMappedById := make(map[string]infisicalSdk.Secret) + for _, secret := range existingSecrets { + existingSecretsMappedById[secret.ID] = secret + } + + for managedSecretId, managedSecretKey := range infisicalPushSecret.Status.ManagedSecrets { + + if _, ok := existingSecretsMappedById[managedSecretId]; ok { + logger.Info(fmt.Sprintf("Deleting secret with key [key=%s]", managedSecretKey)) + + _, err := infisicalClient.Secrets().Delete(infisicalSdk.DeleteSecretOptions{ + SecretKey: managedSecretKey, + ProjectID: destination.ProjectID, + Environment: destination.EnvSlug, + SecretPath: destination.SecretsPath, + }) + + if err != nil { + logger.Info(fmt.Sprintf("unable to delete secret [key=%s] [err=%s]", managedSecretKey, err)) + continue + } + } + + } + + return nil +} diff --git a/k8-operator/controllers/auto_redeployment.go b/k8-operator/controllers/infisicalsecret/auto_redeployment.go similarity index 81% rename from k8-operator/controllers/auto_redeployment.go rename to k8-operator/controllers/infisicalsecret/auto_redeployment.go index cd5bf193b..599e126b5 100644 --- a/k8-operator/controllers/auto_redeployment.go +++ b/k8-operator/controllers/infisicalsecret/auto_redeployment.go @@ -6,6 +6,8 @@ import ( "sync" "github.com/Infisical/infisical/k8-operator/api/v1alpha1" + "github.com/Infisical/infisical/k8-operator/packages/constants" + "github.com/go-logr/logr" v1 "k8s.io/api/apps/v1" corev1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/types" @@ -15,7 +17,7 @@ import ( const DEPLOYMENT_SECRET_NAME_ANNOTATION_PREFIX = "secrets.infisical.com/managed-secret" const AUTO_RELOAD_DEPLOYMENT_ANNOTATION = "secrets.infisical.com/auto-reload" // needs to be set to true for a deployment to start auto redeploying -func (r *InfisicalSecretReconciler) ReconcileDeploymentsWithManagedSecrets(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (int, error) { +func (r *InfisicalSecretReconciler) ReconcileDeploymentsWithManagedSecrets(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret) (int, error) { listOfDeployments := &v1.DeploymentList{} err := r.Client.List(ctx, listOfDeployments, &client.ListOptions{Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace}) if err != nil { @@ -42,8 +44,8 @@ func (r *InfisicalSecretReconciler) ReconcileDeploymentsWithManagedSecrets(ctx c wg.Add(1) go func(d v1.Deployment, s corev1.Secret) { defer wg.Done() - if err := r.ReconcileDeployment(ctx, d, s); err != nil { - fmt.Printf("unable to reconcile deployment with [name=%v]. Will try next requeue", deployment.ObjectMeta.Name) + if err := r.ReconcileDeployment(ctx, logger, d, s); err != nil { + logger.Error(err, fmt.Sprintf("unable to reconcile deployment with [name=%v]. Will try next requeue", deployment.ObjectMeta.Name)) } }(deployment, *managedKubeSecret) } @@ -80,17 +82,17 @@ 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, deployment v1.Deployment, secret corev1.Secret) error { +func (r *InfisicalSecretReconciler) ReconcileDeployment(ctx context.Context, 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[SECRET_VERSION_ANNOTATION] + annotationValue := secret.Annotations[constants.SECRET_VERSION_ANNOTATION] if deployment.Annotations[annotationKey] == annotationValue && deployment.Spec.Template.Annotations[annotationKey] == annotationValue { - fmt.Printf("The [deploymentName=%v] is already using the most up to date managed secrets. No action required.\n", deployment.ObjectMeta.Name) + logger.Info(fmt.Sprintf("The [deploymentName=%v] is already using the most up to date managed secrets. No action required.", deployment.ObjectMeta.Name)) return nil } - fmt.Printf("deployment is using outdated managed secret. Starting re-deployment [deploymentName=%v]\n", deployment.ObjectMeta.Name) + logger.Info(fmt.Sprintf("Deployment is using outdated managed secret. Starting re-deployment [deploymentName=%v]", deployment.ObjectMeta.Name)) if deployment.Spec.Template.Annotations == nil { deployment.Spec.Template.Annotations = make(map[string]string) diff --git a/k8-operator/controllers/conditions.go b/k8-operator/controllers/infisicalsecret/conditions.go similarity index 86% rename from k8-operator/controllers/conditions.go rename to k8-operator/controllers/infisicalsecret/conditions.go index 312b1d699..7ac3a9e5f 100644 --- a/k8-operator/controllers/conditions.go +++ b/k8-operator/controllers/infisicalsecret/conditions.go @@ -5,6 +5,8 @@ import ( "fmt" "github.com/Infisical/infisical/k8-operator/api/v1alpha1" + "github.com/Infisical/infisical/k8-operator/packages/util" + "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) @@ -40,7 +42,7 @@ func (r *InfisicalSecretReconciler) SetReadyToSyncSecretsConditions(ctx context. return r.Client.Status().Update(ctx, infisicalSecret) } -func (r *InfisicalSecretReconciler) SetInfisicalTokenLoadCondition(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, authStrategy AuthStrategyType, errorToConditionOn error) { +func (r *InfisicalSecretReconciler) SetInfisicalTokenLoadCondition(ctx context.Context, logger logr.Logger, infisicalSecret *v1alpha1.InfisicalSecret, authStrategy util.AuthStrategyType, errorToConditionOn error) { if infisicalSecret.Status.Conditions == nil { infisicalSecret.Status.Conditions = []metav1.Condition{} } @@ -63,11 +65,11 @@ func (r *InfisicalSecretReconciler) SetInfisicalTokenLoadCondition(ctx context.C err := r.Client.Status().Update(ctx, infisicalSecret) if err != nil { - fmt.Println("Could not set condition for LoadedInfisicalToken") + logger.Error(err, "Could not set condition for LoadedInfisicalToken") } } -func (r *InfisicalSecretReconciler) SetInfisicalAutoRedeploymentReady(ctx context.Context, infisicalSecret *v1alpha1.InfisicalSecret, numDeployments int, errorToConditionOn error) { +func (r *InfisicalSecretReconciler) SetInfisicalAutoRedeploymentReady(ctx context.Context, logger logr.Logger, infisicalSecret *v1alpha1.InfisicalSecret, numDeployments int, errorToConditionOn error) { if infisicalSecret.Status.Conditions == nil { infisicalSecret.Status.Conditions = []metav1.Condition{} } @@ -90,6 +92,6 @@ func (r *InfisicalSecretReconciler) SetInfisicalAutoRedeploymentReady(ctx contex err := r.Client.Status().Update(ctx, infisicalSecret) if err != nil { - fmt.Println("Could not set condition for AutoRedeployReady") + logger.Error(err, "Could not set condition for AutoRedeployReady") } } diff --git a/k8-operator/controllers/infisicalsecret_controller.go b/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go similarity index 64% rename from k8-operator/controllers/infisicalsecret_controller.go rename to k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go index 90baf2396..8b5508858 100644 --- a/k8-operator/controllers/infisicalsecret_controller.go +++ b/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go @@ -15,15 +15,20 @@ import ( secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1" "github.com/Infisical/infisical/k8-operator/packages/api" - infisicalSdk "github.com/infisical/go-sdk" + controllerhelpers "github.com/Infisical/infisical/k8-operator/packages/controllerutil" + "github.com/Infisical/infisical/k8-operator/packages/util" + "github.com/go-logr/logr" ) // InfisicalSecretReconciler reconciles a InfisicalSecret object type InfisicalSecretReconciler struct { client.Client - Scheme *runtime.Scheme + BaseLogger logr.Logger + Scheme *runtime.Scheme } +var resourceVariablesMap map[string]util.ResourceVariables = make(map[string]util.ResourceVariables) + //+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/status,verbs=get;update;patch //+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/finalizers,verbs=update @@ -37,21 +42,26 @@ type InfisicalSecretReconciler struct { // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.1/pkg/reconcile -type ResourceVariables struct { - infisicalClient infisicalSdk.InfisicalClientInterface - cancelCtx context.CancelFunc - authDetails AuthenticationDetails -} - const FINALIZER_NAME = "secrets.finalizers.infisical.com" // Maps the infisicalSecretCR.UID to a infisicalSdk.InfisicalClientInterface and AuthenticationDetails. -var resourceVariablesMap = make(map[string]ResourceVariables) +// var resourceVariablesMap = make(map[string]ResourceVariables) + +func (r *InfisicalSecretReconciler) GetLogger(req ctrl.Request) logr.Logger { + return r.BaseLogger.WithValues("infisicalsecret", req.NamespacedName) +} func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + + logger := r.GetLogger(req) + var infisicalSecretCR secretsv1alpha1.InfisicalSecret requeueTime := time.Minute // seconds + if resourceVariablesMap == nil { + resourceVariablesMap = make(map[string]util.ResourceVariables) + } + err := r.Get(ctx, req.NamespacedName, &infisicalSecretCR) if err != nil { if errors.IsNotFound(err) { @@ -59,7 +69,7 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ Requeue: false, }, nil } else { - fmt.Printf("\nUnable to fetch Infisical Secret CRD from cluster because [err=%v]", err) + logger.Error(err, "unable to fetch Infisical Secret CRD from cluster") return ctrl.Result{ RequeueAfter: requeueTime, }, nil @@ -71,7 +81,7 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ if !infisicalSecretCR.ObjectMeta.DeletionTimestamp.IsZero() && len(infisicalSecretCR.ObjectMeta.Finalizers) > 0 { infisicalSecretCR.ObjectMeta.Finalizers = []string{} if err := r.Update(ctx, &infisicalSecretCR); err != nil { - fmt.Printf("Error removing finalizers from Infisical Secret %s: %v\n", infisicalSecretCR.Name, err) + logger.Error(err, fmt.Sprintf("Error removing finalizers from Infisical Secret %s", infisicalSecretCR.Name)) return ctrl.Result{}, err } // Our finalizers have been removed, so the reconciler can do nothing. @@ -80,9 +90,10 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ if infisicalSecretCR.Spec.ResyncInterval != 0 { requeueTime = time.Second * time.Duration(infisicalSecretCR.Spec.ResyncInterval) - fmt.Printf("\nManual re-sync interval set. Interval: %v\n", requeueTime) + logger.Info(fmt.Sprintf("Manual re-sync interval set. Interval: %v", requeueTime)) + } else { - fmt.Printf("\nRe-sync interval set. Interval: %v\n", requeueTime) + logger.Info(fmt.Sprintf("Re-sync interval set. Interval: %v", requeueTime)) } // Check if the resource is already marked for deletion @@ -93,9 +104,9 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ } // Get modified/default config - infisicalConfig, err := r.GetInfisicalConfigMap(ctx) + infisicalConfig, err := controllerhelpers.GetInfisicalConfigMap(ctx, r.Client) if err != nil { - fmt.Printf("unable to fetch infisical-config [err=%s]. Will requeue after [requeueTime=%v]\n", err, requeueTime) + logger.Error(err, fmt.Sprintf("unable to fetch infisical-config. Will requeue after [requeueTime=%v]", requeueTime)) return ctrl.Result{ RequeueAfter: requeueTime, }, nil @@ -108,40 +119,41 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ } if infisicalSecretCR.Spec.TLS.CaRef.SecretName != "" { - api.API_CA_CERTIFICATE, err = r.GetInfisicalCaCertificateFromKubeSecret(ctx, infisicalSecretCR) + api.API_CA_CERTIFICATE, err = r.getInfisicalCaCertificateFromKubeSecret(ctx, infisicalSecretCR) if err != nil { - fmt.Printf("unable to fetch CA certificate [err=%s]. Will requeue after [requeueTime=%v]\n", err, requeueTime) + logger.Error(err, fmt.Sprintf("unable to fetch CA certificate. Will requeue after [requeueTime=%v]", requeueTime)) return ctrl.Result{ RequeueAfter: requeueTime, }, nil } - fmt.Println("Using custom CA certificate...") + logger.Info("Using custom CA certificate...") } else { api.API_CA_CERTIFICATE = "" } - err = r.ReconcileInfisicalSecret(ctx, infisicalSecretCR) + err = r.ReconcileInfisicalSecret(ctx, logger, infisicalSecretCR) r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCR, err) if err != nil { - fmt.Printf("unable to reconcile Infisical Secret because [err=%v]. Will requeue after [requeueTime=%v]\n", err, requeueTime) + + logger.Error(err, fmt.Sprintf("unable to reconcile InfisicalSecret. Will requeue after [requeueTime=%v]", requeueTime)) return ctrl.Result{ RequeueAfter: requeueTime, }, nil } - numDeployments, err := r.ReconcileDeploymentsWithManagedSecrets(ctx, infisicalSecretCR) - r.SetInfisicalAutoRedeploymentReady(ctx, &infisicalSecretCR, numDeployments, err) + numDeployments, err := r.ReconcileDeploymentsWithManagedSecrets(ctx, logger, infisicalSecretCR) + r.SetInfisicalAutoRedeploymentReady(ctx, logger, &infisicalSecretCR, numDeployments, err) if err != nil { - fmt.Printf("unable to reconcile auto redeployment because [err=%v]", err) + 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 - fmt.Printf("Operator will requeue after [%v] \n", requeueTime) + logger.Info(fmt.Sprintf("Operator will requeue after [%v]", requeueTime)) return ctrl.Result{ RequeueAfter: requeueTime, }, nil @@ -151,16 +163,20 @@ func (r *InfisicalSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&secretsv1alpha1.InfisicalSecret{}, builder.WithPredicates(predicate.Funcs{ UpdateFunc: func(e event.UpdateEvent) bool { - if rv, ok := resourceVariablesMap[string(e.ObjectNew.GetUID())]; ok { - rv.cancelCtx() - delete(resourceVariablesMap, string(e.ObjectNew.GetUID())) + if resourceVariablesMap != nil { + if rv, ok := resourceVariablesMap[string(e.ObjectNew.GetUID())]; ok { + rv.CancelCtx() + delete(resourceVariablesMap, string(e.ObjectNew.GetUID())) + } } return true }, DeleteFunc: func(e event.DeleteEvent) bool { - if rv, ok := resourceVariablesMap[string(e.Object.GetUID())]; ok { - rv.cancelCtx() - delete(resourceVariablesMap, string(e.Object.GetUID())) + if resourceVariablesMap != nil { + if rv, ok := resourceVariablesMap[string(e.Object.GetUID())]; ok { + rv.CancelCtx() + delete(resourceVariablesMap, string(e.Object.GetUID())) + } } return true }, diff --git a/k8-operator/controllers/infisicalsecret_helper.go b/k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go similarity index 57% rename from k8-operator/controllers/infisicalsecret_helper.go rename to k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go index cdf2a4a26..7b9f8be93 100644 --- a/k8-operator/controllers/infisicalsecret_helper.go +++ b/k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go @@ -10,8 +10,10 @@ import ( "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/model" "github.com/Infisical/infisical/k8-operator/packages/util" + "github.com/go-logr/logr" "k8s.io/apimachinery/pkg/types" @@ -20,113 +22,61 @@ import ( k8Errors "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" ) -const SERVICE_ACCOUNT_ACCESS_KEY = "serviceAccountAccessKey" -const SERVICE_ACCOUNT_PUBLIC_KEY = "serviceAccountPublicKey" -const SERVICE_ACCOUNT_PRIVATE_KEY = "serviceAccountPrivateKey" - -const INFISICAL_MACHINE_IDENTITY_CLIENT_ID = "clientId" -const INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET = "clientSecret" - -const INFISICAL_TOKEN_SECRET_KEY_NAME = "infisicalToken" -const SECRET_VERSION_ANNOTATION = "secrets.infisical.com/version" // used to set the version of secrets via Etag -const OPERATOR_SETTINGS_CONFIGMAP_NAME = "infisical-config" -const OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE = "infisical-operator-system" -const INFISICAL_DOMAIN = "https://app.infisical.com/api" - -func (r *InfisicalSecretReconciler) HandleAuthentication(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { +func (r *InfisicalSecretReconciler) handleAuthentication(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (util.AuthenticationDetails, error) { // ? Legacy support, service token auth - infisicalToken, err := r.GetInfisicalTokenFromKubeSecret(ctx, infisicalSecret) + infisicalToken, err := r.getInfisicalTokenFromKubeSecret(ctx, infisicalSecret) if err != nil { - return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err) + return util.AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err) } if infisicalToken != "" { infisicalClient.Auth().SetAccessToken(infisicalToken) - return AuthenticationDetails{authStrategy: AuthStrategy.SERVICE_TOKEN}, nil + return util.AuthenticationDetails{AuthStrategy: util.AuthStrategy.SERVICE_TOKEN}, nil } // ? Legacy support, service account auth - serviceAccountCreds, err := r.GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret) + serviceAccountCreds, err := r.getInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret) if err != nil { - return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err) + return util.AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err) } if serviceAccountCreds.AccessKey != "" || serviceAccountCreds.PrivateKey != "" || serviceAccountCreds.PublicKey != "" { infisicalClient.Auth().SetAccessToken(serviceAccountCreds.AccessKey) - return AuthenticationDetails{authStrategy: AuthStrategy.SERVICE_ACCOUNT}, nil + return util.AuthenticationDetails{AuthStrategy: util.AuthStrategy.SERVICE_ACCOUNT}, nil } - authStrategies := map[AuthStrategyType]func(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error){ - AuthStrategy.UNIVERSAL_MACHINE_IDENTITY: r.handleUniversalAuth, - AuthStrategy.KUBERNETES_MACHINE_IDENTITY: r.handleKubernetesAuth, - AuthStrategy.AWS_IAM_MACHINE_IDENTITY: r.handleAwsIamAuth, - AuthStrategy.AZURE_MACHINE_IDENTITY: r.handleAzureAuth, - AuthStrategy.GCP_ID_TOKEN_MACHINE_IDENTITY: r.handleGcpIdTokenAuth, - AuthStrategy.GCP_IAM_MACHINE_IDENTITY: r.handleGcpIamAuth, + 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, infisicalSecret, infisicalClient) + authDetails, err := authHandler(ctx, r.Client, util.SecretAuthInput{ + Secret: infisicalSecret, + Type: util.SecretCrd.INFISICAL_SECRET, + }, infisicalClient) if err == nil { return authDetails, nil } - if !errors.Is(err, ErrAuthNotApplicable) { - return AuthenticationDetails{}, fmt.Errorf("authentication failed for strategy [%s] [err=%w]", authStrategy, err) + if !errors.Is(err, util.ErrAuthNotApplicable) { + return util.AuthenticationDetails{}, fmt.Errorf("authentication failed for strategy [%s] [err=%w]", authStrategy, err) } } - return AuthenticationDetails{}, fmt.Errorf("no authentication method provided") + return util.AuthenticationDetails{}, fmt.Errorf("no authentication method provided") } -func (r *InfisicalSecretReconciler) GetInfisicalConfigMap(ctx context.Context) (configMap map[string]string, errToReturn error) { - // default key values - defaultConfigMapData := make(map[string]string) - defaultConfigMapData["hostAPI"] = INFISICAL_DOMAIN - - kubeConfigMap := &corev1.ConfigMap{} - err := r.Client.Get(ctx, types.NamespacedName{ - Namespace: OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE, - Name: 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]", 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 - } -} - -func (r *InfisicalSecretReconciler) GetKubeSecretByNamespacedName(ctx context.Context, namespacedName types.NamespacedName) (*corev1.Secret, error) { - kubeSecret := &corev1.Secret{} - err := r.Client.Get(ctx, namespacedName, kubeSecret) - if err != nil { - kubeSecret = nil - } - - return kubeSecret, err -} - -func (r *InfisicalSecretReconciler) GetInfisicalTokenFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (string, error) { +func (r *InfisicalSecretReconciler) getInfisicalTokenFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (string, error) { // default to new secret ref structure secretName := infisicalSecret.Spec.Authentication.ServiceToken.ServiceTokenSecretReference.SecretName secretNamespace := infisicalSecret.Spec.Authentication.ServiceToken.ServiceTokenSecretReference.SecretNamespace @@ -139,7 +89,7 @@ func (r *InfisicalSecretReconciler) GetInfisicalTokenFromKubeSecret(ctx context. secretNamespace = infisicalSecret.Spec.TokenSecretReference.SecretNamespace } - tokenSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{ + tokenSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{ Namespace: secretNamespace, Name: secretName, }) @@ -152,36 +102,14 @@ func (r *InfisicalSecretReconciler) GetInfisicalTokenFromKubeSecret(ctx context. return "", fmt.Errorf("failed to read Infisical token secret from secret named [%s] in namespace [%s]: with error [%w]", infisicalSecret.Spec.TokenSecretReference.SecretName, infisicalSecret.Spec.TokenSecretReference.SecretNamespace, err) } - infisicalServiceToken := tokenSecret.Data[INFISICAL_TOKEN_SECRET_KEY_NAME] + infisicalServiceToken := tokenSecret.Data[constants.INFISICAL_TOKEN_SECRET_KEY_NAME] return strings.Replace(string(infisicalServiceToken), " ", "", -1), nil } -func (r *InfisicalSecretReconciler) GetInfisicalUniversalAuthFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (machineIdentityDetails model.MachineIdentityDetails, err error) { +func (r *InfisicalSecretReconciler) getInfisicalCaCertificateFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (caCertificate string, err error) { - universalAuthCredsFromKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{ - Namespace: infisicalSecret.Spec.Authentication.UniversalAuth.CredentialsRef.SecretNamespace, - Name: infisicalSecret.Spec.Authentication.UniversalAuth.CredentialsRef.SecretName, - }) - - if k8Errors.IsNotFound(err) { - return model.MachineIdentityDetails{}, nil - } - - if err != nil { - return model.MachineIdentityDetails{}, fmt.Errorf("something went wrong when fetching your machine identity credentials [err=%s]", err) - } - - clientIdFromSecret := universalAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_CLIENT_ID] - clientSecretFromSecret := universalAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET] - - return model.MachineIdentityDetails{ClientId: string(clientIdFromSecret), ClientSecret: string(clientSecretFromSecret)}, nil - -} - -func (r *InfisicalSecretReconciler) GetInfisicalCaCertificateFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (caCertificate string, err error) { - - caCertificateFromKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{ + caCertificateFromKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{ Namespace: infisicalSecret.Spec.TLS.CaRef.SecretNamespace, Name: infisicalSecret.Spec.TLS.CaRef.SecretName, }) @@ -197,13 +125,12 @@ func (r *InfisicalSecretReconciler) GetInfisicalCaCertificateFromKubeSecret(ctx caCertificateFromSecret := string(caCertificateFromKubeSecret.Data[infisicalSecret.Spec.TLS.CaRef.SecretKey]) return caCertificateFromSecret, nil - } // Fetches service account credentials from a Kubernetes secret specified in the infisicalSecret object, extracts the access key, public key, and private key from the secret, and returns them as a ServiceAccountCredentials object. // If any keys are missing or an error occurs, returns an empty object or an error object, respectively. -func (r *InfisicalSecretReconciler) GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (serviceAccountDetails model.ServiceAccountDetails, err error) { - serviceAccountCredsFromKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{ +func (r *InfisicalSecretReconciler) getInfisicalServiceAccountCredentialsFromKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) (serviceAccountDetails model.ServiceAccountDetails, err error) { + serviceAccountCredsFromKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{ Namespace: infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretNamespace, Name: infisicalSecret.Spec.Authentication.ServiceAccount.ServiceAccountSecretReference.SecretName, }) @@ -216,9 +143,9 @@ func (r *InfisicalSecretReconciler) GetInfisicalServiceAccountCredentialsFromKub return model.ServiceAccountDetails{}, fmt.Errorf("something went wrong when fetching your service account credentials [err=%s]", err) } - accessKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[SERVICE_ACCOUNT_ACCESS_KEY] - publicKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[SERVICE_ACCOUNT_PUBLIC_KEY] - privateKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[SERVICE_ACCOUNT_PRIVATE_KEY] + accessKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[constants.SERVICE_ACCOUNT_ACCESS_KEY] + publicKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[constants.SERVICE_ACCOUNT_PUBLIC_KEY] + privateKeyFromSecret := serviceAccountCredsFromKubeSecret.Data[constants.SERVICE_ACCOUNT_PRIVATE_KEY] if accessKeyFromSecret == nil || publicKeyFromSecret == nil || privateKeyFromSecret == nil { return model.ServiceAccountDetails{}, nil @@ -227,7 +154,7 @@ func (r *InfisicalSecretReconciler) GetInfisicalServiceAccountCredentialsFromKub return model.ServiceAccountDetails{AccessKey: string(accessKeyFromSecret), PrivateKey: string(privateKeyFromSecret), PublicKey: string(publicKeyFromSecret)}, nil } -func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error { +func (r *InfisicalSecretReconciler) createInfisicalManagedKubeSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error { plainProcessedSecrets := make(map[string][]byte) secretType := infisicalSecret.Spec.ManagedSecretReference.SecretType managedTemplateData := infisicalSecret.Spec.ManagedSecretReference.Template @@ -283,7 +210,7 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context } } - annotations[SECRET_VERSION_ANNOTATION] = ETag + annotations[constants.SECRET_VERSION_ANNOTATION] = ETag // create a new secret as specified by the managed secret spec of CRD newKubeSecretInstance := &corev1.Secret{ @@ -310,11 +237,11 @@ func (r *InfisicalSecretReconciler) CreateInfisicalManagedKubeSecret(ctx context return fmt.Errorf("unable to create the managed Kubernetes secret : %w", err) } - fmt.Printf("Successfully created a managed Kubernetes secret with your Infisical secrets. Type: %s\n", secretType) + logger.Info(fmt.Sprintf("Successfully created a managed Kubernetes secret with your Infisical secrets. Type: %s", secretType)) return nil } -func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error { +func (r *InfisicalSecretReconciler) updateInfisicalManagedKubeSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret, managedKubeSecret corev1.Secret, secretsFromAPI []model.SingleEnvironmentVariable, ETag string) error { managedTemplateData := infisicalSecret.Spec.ManagedSecretReference.Template plainProcessedSecrets := make(map[string][]byte) @@ -354,20 +281,20 @@ func (r *InfisicalSecretReconciler) UpdateInfisicalManagedKubeSecret(ctx context } managedKubeSecret.Data = plainProcessedSecrets - managedKubeSecret.ObjectMeta.Annotations[SECRET_VERSION_ANNOTATION] = ETag + managedKubeSecret.ObjectMeta.Annotations[constants.SECRET_VERSION_ANNOTATION] = ETag err := r.Client.Update(ctx, &managedKubeSecret) if err != nil { return fmt.Errorf("unable to update Kubernetes secret because [%w]", err) } - fmt.Println("successfully updated managed Kubernetes secret") + logger.Info("successfully updated managed Kubernetes secret") return nil } -func (r *InfisicalSecretReconciler) GetResourceVariables(infisicalSecret v1alpha1.InfisicalSecret) ResourceVariables { +func (r *InfisicalSecretReconciler) getResourceVariables(infisicalSecret v1alpha1.InfisicalSecret) util.ResourceVariables { - var resourceVariables ResourceVariables + var resourceVariables util.ResourceVariables if _, ok := resourceVariablesMap[string(infisicalSecret.UID)]; !ok { @@ -379,10 +306,10 @@ func (r *InfisicalSecretReconciler) GetResourceVariables(infisicalSecret v1alpha UserAgent: api.USER_AGENT_NAME, }) - resourceVariablesMap[string(infisicalSecret.UID)] = ResourceVariables{ - infisicalClient: client, - cancelCtx: cancel, - authDetails: AuthenticationDetails{}, + resourceVariablesMap[string(infisicalSecret.UID)] = util.ResourceVariables{ + InfisicalClient: client, + CancelCtx: cancel, + AuthDetails: util.AuthenticationDetails{}, } resourceVariables = resourceVariablesMap[string(infisicalSecret.UID)] @@ -395,36 +322,36 @@ func (r *InfisicalSecretReconciler) GetResourceVariables(infisicalSecret v1alpha } -func (r *InfisicalSecretReconciler) UpdateResourceVariables(infisicalSecret v1alpha1.InfisicalSecret, resourceVariables ResourceVariables) { +func (r *InfisicalSecretReconciler) updateResourceVariables(infisicalSecret v1alpha1.InfisicalSecret, resourceVariables util.ResourceVariables) { resourceVariablesMap[string(infisicalSecret.UID)] = resourceVariables } -func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret) error { +func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret) error { - resourceVariables := r.GetResourceVariables(infisicalSecret) - infisicalClient := resourceVariables.infisicalClient - cancelCtx := resourceVariables.cancelCtx - authDetails := resourceVariables.authDetails + resourceVariables := r.getResourceVariables(infisicalSecret) + infisicalClient := resourceVariables.InfisicalClient + cancelCtx := resourceVariables.CancelCtx + authDetails := resourceVariables.AuthDetails var err error - if authDetails.authStrategy == "" { - fmt.Println("ReconcileInfisicalSecret: No authentication strategy found. Attempting to authenticate") - authDetails, err = r.HandleAuthentication(ctx, infisicalSecret, infisicalClient) - r.SetInfisicalTokenLoadCondition(ctx, &infisicalSecret, authDetails.authStrategy, err) + if authDetails.AuthStrategy == "" { + logger.Info("No authentication strategy found. Attempting to authenticate") + authDetails, err = r.handleAuthentication(ctx, infisicalSecret, infisicalClient) + r.SetInfisicalTokenLoadCondition(ctx, logger, &infisicalSecret, authDetails.AuthStrategy, err) if err != nil { return fmt.Errorf("unable to authenticate [err=%s]", err) } - r.UpdateResourceVariables(infisicalSecret, ResourceVariables{ - infisicalClient: infisicalClient, - cancelCtx: cancelCtx, - authDetails: authDetails, + r.updateResourceVariables(infisicalSecret, util.ResourceVariables{ + InfisicalClient: infisicalClient, + CancelCtx: cancelCtx, + AuthDetails: authDetails, }) } // Look for managed secret by name and namespace - managedKubeSecret, err := r.GetKubeSecretByNamespacedName(ctx, types.NamespacedName{ + managedKubeSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{ Name: infisicalSecret.Spec.ManagedSecretReference.SecretName, Namespace: infisicalSecret.Spec.ManagedSecretReference.SecretNamespace, }) @@ -436,14 +363,14 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context // Get exiting Etag if exists secretVersionBasedOnETag := "" if managedKubeSecret != nil { - secretVersionBasedOnETag = managedKubeSecret.Annotations[SECRET_VERSION_ANNOTATION] + secretVersionBasedOnETag = managedKubeSecret.Annotations[constants.SECRET_VERSION_ANNOTATION] } var plainTextSecretsFromApi []model.SingleEnvironmentVariable var updateDetails model.RequestUpdateUpdateDetails - if authDetails.authStrategy == AuthStrategy.SERVICE_ACCOUNT { // Service Account // ! Legacy auth method - serviceAccountCreds, err := r.GetInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret) + if authDetails.AuthStrategy == util.AuthStrategy.SERVICE_ACCOUNT { // Service Account // ! Legacy auth method + serviceAccountCreds, err := r.getInfisicalServiceAccountCredentialsFromKubeSecret(ctx, infisicalSecret) if err != nil { return fmt.Errorf("ReconcileInfisicalSecret: unable to get service account creds from kube secret [err=%s]", err) } @@ -453,10 +380,10 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context return fmt.Errorf("\nfailed to get secrets because [err=%v]", err) } - fmt.Println("ReconcileInfisicalSecret: Fetched secrets via service account") + logger.Info("ReconcileInfisicalSecret: Fetched secrets via service account") - } else if authDetails.authStrategy == AuthStrategy.SERVICE_TOKEN { // Service Tokens // ! Legacy / Deprecated auth method - infisicalToken, err := r.GetInfisicalTokenFromKubeSecret(ctx, infisicalSecret) + } else if authDetails.AuthStrategy == util.AuthStrategy.SERVICE_TOKEN { // Service Tokens // ! Legacy / Deprecated auth method + infisicalToken, err := r.getInfisicalTokenFromKubeSecret(ctx, infisicalSecret) if err != nil { return fmt.Errorf("ReconcileInfisicalSecret: unable to get service token from kube secret [err=%s]", err) } @@ -470,28 +397,30 @@ func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context return fmt.Errorf("\nfailed to get secrets because [err=%v]", err) } - fmt.Println("ReconcileInfisicalSecret: Fetched secrets via [type=SERVICE_TOKEN]") - } else if authDetails.isMachineIdentityAuth { // * Machine Identity authentication, the SDK will be authenticated at this point - plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaMachineIdentity(infisicalClient, secretVersionBasedOnETag, authDetails.machineIdentityScope) + logger.Info("ReconcileInfisicalSecret: Fetched secrets via [type=SERVICE_TOKEN]") + + } else if authDetails.IsMachineIdentityAuth { // * Machine Identity authentication, the SDK will be authenticated at this point + plainTextSecretsFromApi, updateDetails, err = util.GetPlainTextSecretsViaMachineIdentity(infisicalClient, secretVersionBasedOnETag, authDetails.MachineIdentityScope) if err != nil { return fmt.Errorf("\nfailed to get secrets because [err=%v]", err) } - fmt.Printf("ReconcileInfisicalSecret: Fetched secrets via machine identity [type=%v]\n", authDetails.authStrategy) + + logger.Info(fmt.Sprintf("ReconcileInfisicalSecret: Fetched secrets via machine identity [type=%v]", authDetails.AuthStrategy)) } else { return errors.New("no authentication method provided yet. Please configure a authentication method then try again") } if !updateDetails.Modified { - fmt.Println("No secrets modified so reconcile not needed") + logger.Info("ReconcileInfisicalSecret: No secrets modified so reconcile not needed") return nil } if managedKubeSecret == nil { - return r.CreateInfisicalManagedKubeSecret(ctx, infisicalSecret, plainTextSecretsFromApi, updateDetails.ETag) + return r.createInfisicalManagedKubeSecret(ctx, logger, infisicalSecret, plainTextSecretsFromApi, updateDetails.ETag) } else { - return r.UpdateInfisicalManagedKubeSecret(ctx, infisicalSecret, *managedKubeSecret, plainTextSecretsFromApi, updateDetails.ETag) + return r.updateInfisicalManagedKubeSecret(ctx, logger, infisicalSecret, *managedKubeSecret, plainTextSecretsFromApi, updateDetails.ETag) } } diff --git a/k8-operator/controllers/suite_test.go b/k8-operator/controllers/infisicalsecret/suite_test.go similarity index 100% rename from k8-operator/controllers/suite_test.go rename to k8-operator/controllers/infisicalsecret/suite_test.go diff --git a/k8-operator/controllers/infisicalsecret_auth.go b/k8-operator/controllers/infisicalsecret_auth.go deleted file mode 100644 index 06e1c659a..000000000 --- a/k8-operator/controllers/infisicalsecret_auth.go +++ /dev/null @@ -1,150 +0,0 @@ -package controllers - -import ( - "context" - "errors" - "fmt" - - "github.com/Infisical/infisical/k8-operator/api/v1alpha1" - "github.com/Infisical/infisical/k8-operator/packages/util" - infisicalSdk "github.com/infisical/go-sdk" -) - -type AuthStrategyType string - -var AuthStrategy = struct { - SERVICE_TOKEN AuthStrategyType - SERVICE_ACCOUNT AuthStrategyType - UNIVERSAL_MACHINE_IDENTITY AuthStrategyType - KUBERNETES_MACHINE_IDENTITY AuthStrategyType - AWS_IAM_MACHINE_IDENTITY AuthStrategyType - AZURE_MACHINE_IDENTITY AuthStrategyType - GCP_ID_TOKEN_MACHINE_IDENTITY AuthStrategyType - GCP_IAM_MACHINE_IDENTITY AuthStrategyType -}{ - SERVICE_TOKEN: "SERVICE_TOKEN", - SERVICE_ACCOUNT: "SERVICE_ACCOUNT", - UNIVERSAL_MACHINE_IDENTITY: "UNIVERSAL_MACHINE_IDENTITY", - KUBERNETES_MACHINE_IDENTITY: "KUBERNETES_AUTH_MACHINE_IDENTITY", - AWS_IAM_MACHINE_IDENTITY: "AWS_IAM_MACHINE_IDENTITY", - AZURE_MACHINE_IDENTITY: "AZURE_MACHINE_IDENTITY", - GCP_ID_TOKEN_MACHINE_IDENTITY: "GCP_ID_TOKEN_MACHINE_IDENTITY", - GCP_IAM_MACHINE_IDENTITY: "GCP_IAM_MACHINE_IDENTITY", -} - -type AuthenticationDetails struct { - authStrategy AuthStrategyType - machineIdentityScope v1alpha1.MachineIdentityScopeInWorkspace // This will only be set if a machine identity auth method is used (e.g. UniversalAuth or KubernetesAuth, etc.) - isMachineIdentityAuth bool -} - -var ErrAuthNotApplicable = errors.New("authentication not applicable") - -func (r *InfisicalSecretReconciler) handleUniversalAuth(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { - - // Machine Identities: - universalAuthKubeSecret, err := r.GetInfisicalUniversalAuthFromKubeSecret(ctx, infisicalSecret) - universalAuthSpec := infisicalSecret.Spec.Authentication.UniversalAuth - - if err != nil { - return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get machine identity creds from kube secret [err=%s]", err) - } - - if universalAuthKubeSecret.ClientId == "" && universalAuthKubeSecret.ClientSecret == "" { - return AuthenticationDetails{}, ErrAuthNotApplicable - } - - _, err = infisicalClient.Auth().UniversalAuthLogin(universalAuthKubeSecret.ClientId, universalAuthKubeSecret.ClientSecret) - if err != nil { - return AuthenticationDetails{}, fmt.Errorf("unable to login with machine identity credentials [err=%s]", err) - } - - fmt.Println("Successfully authenticated with machine identity credentials") - - return AuthenticationDetails{authStrategy: AuthStrategy.UNIVERSAL_MACHINE_IDENTITY, machineIdentityScope: universalAuthSpec.SecretsScope, isMachineIdentityAuth: true}, nil - -} - -func (r *InfisicalSecretReconciler) handleKubernetesAuth(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { - kubernetesAuthSpec := infisicalSecret.Spec.Authentication.KubernetesAuth - - if kubernetesAuthSpec.IdentityID == "" { - return AuthenticationDetails{}, ErrAuthNotApplicable - } - - serviceAccountToken, err := util.GetServiceAccountToken(r.Client, kubernetesAuthSpec.ServiceAccountRef.Namespace, kubernetesAuthSpec.ServiceAccountRef.Name) - if err != nil { - return AuthenticationDetails{}, fmt.Errorf("unable to get service account token [err=%s]", err) - } - - _, err = infisicalClient.Auth().KubernetesRawServiceAccountTokenLogin(kubernetesAuthSpec.IdentityID, serviceAccountToken) - if err != nil { - return AuthenticationDetails{}, fmt.Errorf("unable to login with Kubernetes native auth [err=%s]", err) - } - - return AuthenticationDetails{authStrategy: AuthStrategy.KUBERNETES_MACHINE_IDENTITY, machineIdentityScope: kubernetesAuthSpec.SecretsScope, isMachineIdentityAuth: true}, nil - -} - -func (r *InfisicalSecretReconciler) handleAwsIamAuth(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { - awsIamAuthSpec := infisicalSecret.Spec.Authentication.AwsIamAuth - - if awsIamAuthSpec.IdentityID == "" { - return AuthenticationDetails{}, ErrAuthNotApplicable - } - - _, err := infisicalClient.Auth().AwsIamAuthLogin(awsIamAuthSpec.IdentityID) - if err != nil { - return AuthenticationDetails{}, fmt.Errorf("unable to login with AWS IAM auth [err=%s]", err) - } - - return AuthenticationDetails{authStrategy: AuthStrategy.AWS_IAM_MACHINE_IDENTITY, machineIdentityScope: awsIamAuthSpec.SecretsScope, isMachineIdentityAuth: true}, nil - -} - -func (r *InfisicalSecretReconciler) handleAzureAuth(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { - azureAuthSpec := infisicalSecret.Spec.Authentication.AzureAuth - - if azureAuthSpec.IdentityID == "" { - return AuthenticationDetails{}, ErrAuthNotApplicable - } - - _, err := infisicalClient.Auth().AzureAuthLogin(azureAuthSpec.IdentityID, azureAuthSpec.Resource) // If resource is empty(""), it will default to "https://management.azure.com/" in the SDK. - if err != nil { - return AuthenticationDetails{}, fmt.Errorf("unable to login with Azure auth [err=%s]", err) - } - - return AuthenticationDetails{authStrategy: AuthStrategy.AZURE_MACHINE_IDENTITY, machineIdentityScope: azureAuthSpec.SecretsScope, isMachineIdentityAuth: true}, nil - -} - -func (r *InfisicalSecretReconciler) handleGcpIdTokenAuth(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { - gcpIdTokenSpec := infisicalSecret.Spec.Authentication.GcpIdTokenAuth - - if gcpIdTokenSpec.IdentityID == "" { - return AuthenticationDetails{}, ErrAuthNotApplicable - } - - _, err := infisicalClient.Auth().GcpIdTokenAuthLogin(gcpIdTokenSpec.IdentityID) - if err != nil { - return AuthenticationDetails{}, fmt.Errorf("unable to login with GCP Id Token auth [err=%s]", err) - } - - return AuthenticationDetails{authStrategy: AuthStrategy.GCP_ID_TOKEN_MACHINE_IDENTITY, machineIdentityScope: gcpIdTokenSpec.SecretsScope, isMachineIdentityAuth: true}, nil - -} - -func (r *InfisicalSecretReconciler) handleGcpIamAuth(ctx context.Context, infisicalSecret v1alpha1.InfisicalSecret, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { - gcpIamSpec := infisicalSecret.Spec.Authentication.GcpIamAuth - - if gcpIamSpec.IdentityID == "" && gcpIamSpec.ServiceAccountKeyFilePath == "" { - return AuthenticationDetails{}, ErrAuthNotApplicable - } - - _, err := infisicalClient.Auth().GcpIamAuthLogin(gcpIamSpec.IdentityID, gcpIamSpec.ServiceAccountKeyFilePath) - if err != nil { - return AuthenticationDetails{}, fmt.Errorf("unable to login with GCP IAM auth [err=%s]", err) - } - - return AuthenticationDetails{authStrategy: AuthStrategy.GCP_IAM_MACHINE_IDENTITY, machineIdentityScope: gcpIamSpec.SecretsScope, isMachineIdentityAuth: true}, nil -} diff --git a/k8-operator/kubectl-install/install-secrets-operator.yaml b/k8-operator/kubectl-install/install-secrets-operator.yaml index 79a926805..26d924c76 100644 --- a/k8-operator/kubectl-install/install-secrets-operator.yaml +++ b/k8-operator/kubectl-install/install-secrets-operator.yaml @@ -13,6 +13,215 @@ metadata: --- apiVersion: apiextensions.k8s.io/v1 kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + creationTimestamp: null + name: infisicalpushsecrets.secrets.infisical.com +spec: + group: secrets.infisical.com + names: + kind: InfisicalPushSecret + listKind: InfisicalPushSecretList + plural: infisicalpushsecrets + singular: infisicalpushsecret + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: InfisicalPushSecret is the Schema for the infisicalpushsecrets 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: InfisicalPushSecretSpec defines the desired state of InfisicalPushSecret + 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: + description: Rest of your types should be defined similarly... + properties: + identityId: + type: string + serviceAccountRef: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + required: + - identityId + - serviceAccountRef + type: object + universalAuth: + description: PushSecretUniversalAuth defines universal authentication + 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 + deletionPolicy: + type: string + destination: + properties: + envSlug: + type: string + projectId: + type: string + secretsPath: + type: string + required: + - envSlug + - projectId + - secretsPath + type: object + hostAPI: + type: string + push: + properties: + secret: + 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: + - secret + type: object + resyncInterval: + type: string + updatePolicy: + type: string + required: + - destination + - push + - resyncInterval + type: object + status: + description: InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current state of this API Resource. --- This struct is intended for direct use as an array at the field path .status.conditions. For example, \n type FooStatus struct{ // Represents the observations of a foo's current state. // Known .status.conditions.type are: \"Available\", \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge // +listType=map // +listMapKey=type Conditions []metav1.Condition `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition transitioned from one status to another. This should be when the underlying condition changed. If that is not known, then using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation that the condition was set based upon. For instance, if .metadata.generation is currently 12, but the .status.conditions[x].observedGeneration is 9, the condition is out of date with respect to the current state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating the reason for the condition's last transition. Producers of specific condition types may define expected values and meanings for this field, and whether the values are considered a guaranteed API. The value should be a CamelCase string. This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. --- Many .condition.type values are consistent across resources like Available, but because arbitrary conditions can be useful (see .node.status.conditions), the ability to deconflict is important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + managedSecrets: + additionalProperties: + type: string + description: managed secrets is a map where the key is the ID, and the value is the secret key (string[id], string[key] ) + type: object + required: + - conditions + - managedSecrets + type: object + type: object + served: true + storage: true + subresources: + status: {} +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition metadata: annotations: controller-gen.kubebuilder.io/version: v0.10.0 diff --git a/k8-operator/main.go b/k8-operator/main.go index 50c0cda00..a0e11c0ca 100644 --- a/k8-operator/main.go +++ b/k8-operator/main.go @@ -16,7 +16,8 @@ import ( "sigs.k8s.io/controller-runtime/pkg/log/zap" secretsv1alpha1 "github.com/Infisical/infisical/k8-operator/api/v1alpha1" - "github.com/Infisical/infisical/k8-operator/controllers" + infisicalPushSecretController "github.com/Infisical/infisical/k8-operator/controllers/infisicalpushsecret" + infisicalSecretController "github.com/Infisical/infisical/k8-operator/controllers/infisicalsecret" //+kubebuilder:scaffold:imports ) @@ -73,13 +74,24 @@ func main() { os.Exit(1) } - if err = (&controllers.InfisicalSecretReconciler{ - Client: mgr.GetClient(), - Scheme: mgr.GetScheme(), + if err = (&infisicalSecretController.InfisicalSecretReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + BaseLogger: ctrl.Log, }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "InfisicalSecret") os.Exit(1) } + + if err = (&infisicalPushSecretController.InfisicalPushSecretReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + BaseLogger: ctrl.Log, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "InfisicalPushSecret") + 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 dd2af3353..de12b4bc5 100644 --- a/k8-operator/packages/api/api.go +++ b/k8-operator/packages/api/api.go @@ -24,10 +24,6 @@ func CallGetServiceTokenDetailsV2(httpClient *resty.Client) (GetServiceTokenDeta return GetServiceTokenDetailsResponse{}, fmt.Errorf("CallGetServiceTokenDetails: Unsuccessful response: [response=%s]", response) } - // logging for better debugging and user experience - fmt.Printf("Workspace ID: %v\n", tokenDetailsResponse.Workspace) - fmt.Printf("TokenName: %v\n", tokenDetailsResponse.Name) - return tokenDetailsResponse, nil } diff --git a/k8-operator/packages/constants/constants.go b/k8-operator/packages/constants/constants.go new file mode 100644 index 000000000..ff28e408d --- /dev/null +++ b/k8-operator/packages/constants/constants.go @@ -0,0 +1,24 @@ +package constants + +const SERVICE_ACCOUNT_ACCESS_KEY = "serviceAccountAccessKey" +const SERVICE_ACCOUNT_PUBLIC_KEY = "serviceAccountPublicKey" +const SERVICE_ACCOUNT_PRIVATE_KEY = "serviceAccountPrivateKey" + +const INFISICAL_MACHINE_IDENTITY_CLIENT_ID = "clientId" +const INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET = "clientSecret" + +const INFISICAL_TOKEN_SECRET_KEY_NAME = "infisicalToken" +const SECRET_VERSION_ANNOTATION = "secrets.infisical.com/version" // used to set the version of secrets via Etag +const OPERATOR_SETTINGS_CONFIGMAP_NAME = "infisical-config" +const OPERATOR_SETTINGS_CONFIGMAP_NAMESPACE = "infisical-operator-system" +const INFISICAL_DOMAIN = "https://app.infisical.com/api" + +const INFISICAL_PUSH_SECRET_FINALIZER_NAME = "infisical.secrets.infisical.com/finalizer" + +type PushSecretReplacePolicy string +type PushSecretDeletionPolicy string + +const ( + PUSH_SECRET_REPLACE_POLICY_ENABLED PushSecretReplacePolicy = "Replace" + PUSH_SECRET_DELETE_POLICY_ENABLED PushSecretDeletionPolicy = "Delete" +) diff --git a/k8-operator/packages/controllerutil/util.go b/k8-operator/packages/controllerutil/util.go new file mode 100644 index 000000000..8c610e2e5 --- /dev/null +++ b/k8-operator/packages/controllerutil/util.go @@ -0,0 +1,45 @@ +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/util/auth.go b/k8-operator/packages/util/auth.go index d3ee0ce3b..d01174277 100644 --- a/k8-operator/packages/util/auth.go +++ b/k8-operator/packages/util/auth.go @@ -4,7 +4,12 @@ import ( "context" "fmt" + "errors" + corev1 "k8s.io/api/core/v1" + + "github.com/Infisical/infisical/k8-operator/api/v1alpha1" + infisicalSdk "github.com/infisical/go-sdk" "sigs.k8s.io/controller-runtime/pkg/client" ) @@ -32,3 +37,324 @@ func GetServiceAccountToken(k8sClient client.Client, namespace string, serviceAc return string(token), nil } + +type AuthStrategyType string + +var AuthStrategy = struct { + SERVICE_TOKEN AuthStrategyType + SERVICE_ACCOUNT AuthStrategyType + UNIVERSAL_MACHINE_IDENTITY AuthStrategyType + KUBERNETES_MACHINE_IDENTITY AuthStrategyType + AWS_IAM_MACHINE_IDENTITY AuthStrategyType + AZURE_MACHINE_IDENTITY AuthStrategyType + GCP_ID_TOKEN_MACHINE_IDENTITY AuthStrategyType + GCP_IAM_MACHINE_IDENTITY AuthStrategyType +}{ + SERVICE_TOKEN: "SERVICE_TOKEN", + SERVICE_ACCOUNT: "SERVICE_ACCOUNT", + UNIVERSAL_MACHINE_IDENTITY: "UNIVERSAL_MACHINE_IDENTITY", + KUBERNETES_MACHINE_IDENTITY: "KUBERNETES_AUTH_MACHINE_IDENTITY", + AWS_IAM_MACHINE_IDENTITY: "AWS_IAM_MACHINE_IDENTITY", + AZURE_MACHINE_IDENTITY: "AZURE_MACHINE_IDENTITY", + GCP_ID_TOKEN_MACHINE_IDENTITY: "GCP_ID_TOKEN_MACHINE_IDENTITY", + GCP_IAM_MACHINE_IDENTITY: "GCP_IAM_MACHINE_IDENTITY", +} + +type SecretCrdType string + +var SecretCrd = struct { + INFISICAL_SECRET SecretCrdType + INFISICAL_PUSH_SECRET SecretCrdType +}{ + INFISICAL_SECRET: "INFISICAL_SECRET", + INFISICAL_PUSH_SECRET: "INFISICAL_PUSH_SECRET", +} + +type SecretAuthInput struct { + Secret interface{} + Type SecretCrdType +} + +type AuthenticationDetails struct { + AuthStrategy AuthStrategyType + MachineIdentityScope v1alpha1.MachineIdentityScopeInWorkspace // This will only be set if a machine identity auth method is used (e.g. UniversalAuth or KubernetesAuth, etc.) + IsMachineIdentityAuth bool + SecretType SecretCrdType +} + +var ErrAuthNotApplicable = errors.New("authentication not applicable") + +func HandleUniversalAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { + + var universalAuthSpec v1alpha1.UniversalAuthDetails + + switch secretCrd.Type { + case SecretCrd.INFISICAL_SECRET: + infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret) + + if !ok { + return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret") + } + universalAuthSpec = infisicalSecret.Spec.Authentication.UniversalAuth + case SecretCrd.INFISICAL_PUSH_SECRET: + infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret) + + if !ok { + return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret") + } + + universalAuthSpec = v1alpha1.UniversalAuthDetails{ + CredentialsRef: infisicalPushSecret.Spec.Authentication.UniversalAuth.CredentialsRef, + SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{}, + } + } + + universalAuthKubeSecret, err := GetInfisicalUniversalAuthFromKubeSecret(ctx, reconcilerClient, v1alpha1.KubeSecretReference{ + SecretNamespace: universalAuthSpec.CredentialsRef.SecretNamespace, + SecretName: universalAuthSpec.CredentialsRef.SecretName, + }) + + if err != nil { + return AuthenticationDetails{}, fmt.Errorf("ReconcileInfisicalSecret: unable to get machine identity creds from kube secret [err=%s]", err) + } + + if universalAuthKubeSecret.ClientId == "" && universalAuthKubeSecret.ClientSecret == "" { + return AuthenticationDetails{}, ErrAuthNotApplicable + } + + _, err = infisicalClient.Auth().UniversalAuthLogin(universalAuthKubeSecret.ClientId, universalAuthKubeSecret.ClientSecret) + if err != nil { + return AuthenticationDetails{}, fmt.Errorf("unable to login with machine identity credentials [err=%s]", err) + } + + return AuthenticationDetails{ + AuthStrategy: AuthStrategy.UNIVERSAL_MACHINE_IDENTITY, + MachineIdentityScope: universalAuthSpec.SecretsScope, + IsMachineIdentityAuth: true, + SecretType: secretCrd.Type, + }, nil +} + +func HandleKubernetesAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { + var kubernetesAuthSpec v1alpha1.KubernetesAuthDetails + + switch secretCrd.Type { + case SecretCrd.INFISICAL_SECRET: + infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret) + + if !ok { + return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret") + } + kubernetesAuthSpec = infisicalSecret.Spec.Authentication.KubernetesAuth + case SecretCrd.INFISICAL_PUSH_SECRET: + infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret) + + if !ok { + return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret") + } + kubernetesAuthSpec = v1alpha1.KubernetesAuthDetails{ + IdentityID: infisicalPushSecret.Spec.Authentication.KubernetesAuth.IdentityID, + ServiceAccountRef: v1alpha1.KubernetesServiceAccountRef{ + Namespace: infisicalPushSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Namespace, + Name: infisicalPushSecret.Spec.Authentication.KubernetesAuth.ServiceAccountRef.Name, + }, + SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{}, + } + } + + if kubernetesAuthSpec.IdentityID == "" { + return AuthenticationDetails{}, ErrAuthNotApplicable + } + + serviceAccountToken, err := GetServiceAccountToken(reconcilerClient, kubernetesAuthSpec.ServiceAccountRef.Namespace, kubernetesAuthSpec.ServiceAccountRef.Name) + if err != nil { + return AuthenticationDetails{}, fmt.Errorf("unable to get service account token [err=%s]", err) + } + + _, err = infisicalClient.Auth().KubernetesRawServiceAccountTokenLogin(kubernetesAuthSpec.IdentityID, serviceAccountToken) + if err != nil { + return AuthenticationDetails{}, fmt.Errorf("unable to login with Kubernetes native auth [err=%s]", err) + } + + return AuthenticationDetails{ + AuthStrategy: AuthStrategy.KUBERNETES_MACHINE_IDENTITY, + MachineIdentityScope: kubernetesAuthSpec.SecretsScope, + IsMachineIdentityAuth: true, + SecretType: secretCrd.Type, + }, nil + +} + +func HandleAwsIamAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { + awsIamAuthSpec := v1alpha1.AWSIamAuthDetails{} + + switch secretCrd.Type { + case SecretCrd.INFISICAL_SECRET: + infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret) + + if !ok { + return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret") + } + + awsIamAuthSpec = infisicalSecret.Spec.Authentication.AwsIamAuth + case SecretCrd.INFISICAL_PUSH_SECRET: + infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret) + + if !ok { + return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret") + } + + awsIamAuthSpec = v1alpha1.AWSIamAuthDetails{ + IdentityID: infisicalPushSecret.Spec.Authentication.AwsIamAuth.IdentityID, + SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{}, + } + } + + if awsIamAuthSpec.IdentityID == "" { + return AuthenticationDetails{}, ErrAuthNotApplicable + } + + _, err := infisicalClient.Auth().AwsIamAuthLogin(awsIamAuthSpec.IdentityID) + if err != nil { + return AuthenticationDetails{}, fmt.Errorf("unable to login with AWS IAM auth [err=%s]", err) + } + + return AuthenticationDetails{ + AuthStrategy: AuthStrategy.AWS_IAM_MACHINE_IDENTITY, + MachineIdentityScope: awsIamAuthSpec.SecretsScope, + IsMachineIdentityAuth: true, + SecretType: secretCrd.Type, + }, nil + +} + +func HandleAzureAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { + azureAuthSpec := v1alpha1.AzureAuthDetails{} + + switch secretCrd.Type { + case SecretCrd.INFISICAL_SECRET: + infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret) + + if !ok { + return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret") + } + + azureAuthSpec = infisicalSecret.Spec.Authentication.AzureAuth + + case SecretCrd.INFISICAL_PUSH_SECRET: + infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret) + + if !ok { + return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret") + } + + azureAuthSpec = v1alpha1.AzureAuthDetails{ + IdentityID: infisicalPushSecret.Spec.Authentication.AzureAuth.IdentityID, + Resource: infisicalPushSecret.Spec.Authentication.AzureAuth.Resource, + SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{}, + } + } + + if azureAuthSpec.IdentityID == "" { + return AuthenticationDetails{}, ErrAuthNotApplicable + } + + _, err := infisicalClient.Auth().AzureAuthLogin(azureAuthSpec.IdentityID, azureAuthSpec.Resource) // If resource is empty(""), it will default to "https://management.azure.com/" in the SDK. + if err != nil { + return AuthenticationDetails{}, fmt.Errorf("unable to login with Azure auth [err=%s]", err) + } + + return AuthenticationDetails{ + AuthStrategy: AuthStrategy.AZURE_MACHINE_IDENTITY, + MachineIdentityScope: azureAuthSpec.SecretsScope, + IsMachineIdentityAuth: true, + SecretType: secretCrd.Type, + }, nil + +} + +func HandleGcpIdTokenAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { + gcpIdTokenSpec := v1alpha1.GCPIdTokenAuthDetails{} + + switch secretCrd.Type { + case SecretCrd.INFISICAL_SECRET: + infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret) + + if !ok { + return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret") + } + + gcpIdTokenSpec = infisicalSecret.Spec.Authentication.GcpIdTokenAuth + case SecretCrd.INFISICAL_PUSH_SECRET: + infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret) + + if !ok { + return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret") + } + + gcpIdTokenSpec = v1alpha1.GCPIdTokenAuthDetails{ + IdentityID: infisicalPushSecret.Spec.Authentication.GcpIdTokenAuth.IdentityID, + SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{}, + } + } + + if gcpIdTokenSpec.IdentityID == "" { + return AuthenticationDetails{}, ErrAuthNotApplicable + } + + _, err := infisicalClient.Auth().GcpIdTokenAuthLogin(gcpIdTokenSpec.IdentityID) + if err != nil { + return AuthenticationDetails{}, fmt.Errorf("unable to login with GCP Id Token auth [err=%s]", err) + } + + return AuthenticationDetails{ + AuthStrategy: AuthStrategy.GCP_ID_TOKEN_MACHINE_IDENTITY, + MachineIdentityScope: gcpIdTokenSpec.SecretsScope, + IsMachineIdentityAuth: true, + SecretType: secretCrd.Type, + }, nil + +} + +func HandleGcpIamAuth(ctx context.Context, reconcilerClient client.Client, secretCrd SecretAuthInput, infisicalClient infisicalSdk.InfisicalClientInterface) (AuthenticationDetails, error) { + gcpIamSpec := v1alpha1.GcpIamAuthDetails{} + + switch secretCrd.Type { + case SecretCrd.INFISICAL_SECRET: + infisicalSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalSecret) + + if !ok { + return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalSecret") + } + + gcpIamSpec = infisicalSecret.Spec.Authentication.GcpIamAuth + case SecretCrd.INFISICAL_PUSH_SECRET: + infisicalPushSecret, ok := secretCrd.Secret.(v1alpha1.InfisicalPushSecret) + + if !ok { + return AuthenticationDetails{}, errors.New("unable to cast secret to InfisicalPushSecret") + } + + gcpIamSpec = v1alpha1.GcpIamAuthDetails{ + IdentityID: infisicalPushSecret.Spec.Authentication.GcpIamAuth.IdentityID, + ServiceAccountKeyFilePath: infisicalPushSecret.Spec.Authentication.GcpIamAuth.ServiceAccountKeyFilePath, + SecretsScope: v1alpha1.MachineIdentityScopeInWorkspace{}, + } + } + + if gcpIamSpec.IdentityID == "" && gcpIamSpec.ServiceAccountKeyFilePath == "" { + return AuthenticationDetails{}, ErrAuthNotApplicable + } + + _, err := infisicalClient.Auth().GcpIamAuthLogin(gcpIamSpec.IdentityID, gcpIamSpec.ServiceAccountKeyFilePath) + if err != nil { + return AuthenticationDetails{}, fmt.Errorf("unable to login with GCP IAM auth [err=%s]", err) + } + + return AuthenticationDetails{ + AuthStrategy: AuthStrategy.GCP_IAM_MACHINE_IDENTITY, + MachineIdentityScope: gcpIamSpec.SecretsScope, + IsMachineIdentityAuth: true, + SecretType: secretCrd.Type, + }, nil +} diff --git a/k8-operator/packages/util/kubernetes.go b/k8-operator/packages/util/kubernetes.go new file mode 100644 index 000000000..6397f4036 --- /dev/null +++ b/k8-operator/packages/util/kubernetes.go @@ -0,0 +1,50 @@ +package util + +import ( + "context" + "fmt" + + "github.com/Infisical/infisical/k8-operator/api/v1alpha1" + "github.com/Infisical/infisical/k8-operator/packages/model" + 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" +) + +const INFISICAL_MACHINE_IDENTITY_CLIENT_ID = "clientId" +const INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET = "clientSecret" + +func GetKubeSecretByNamespacedName(ctx context.Context, reconcilerClient client.Client, namespacedName types.NamespacedName) (*corev1.Secret, error) { + kubeSecret := &corev1.Secret{} + err := reconcilerClient.Get(ctx, namespacedName, kubeSecret) + if err != nil { + kubeSecret = nil + } + + return kubeSecret, err +} + +func GetInfisicalUniversalAuthFromKubeSecret(ctx context.Context, reconcilerClient client.Client, universalAuthRef v1alpha1.KubeSecretReference) (machineIdentityDetails model.MachineIdentityDetails, err error) { + + universalAuthCredsFromKubeSecret, err := GetKubeSecretByNamespacedName(ctx, reconcilerClient, types.NamespacedName{ + Namespace: universalAuthRef.SecretNamespace, + Name: universalAuthRef.SecretName, + // Namespace: infisicalSecret.Spec.Authentication.UniversalAuth.CredentialsRef.SecretNamespace, + // Name: infisicalSecret.Spec.Authentication.UniversalAuth.CredentialsRef.SecretName, + }) + + if k8Errors.IsNotFound(err) { + return model.MachineIdentityDetails{}, nil + } + + if err != nil { + return model.MachineIdentityDetails{}, fmt.Errorf("something went wrong when fetching your machine identity credentials [err=%s]", err) + } + + clientIdFromSecret := universalAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_CLIENT_ID] + clientSecretFromSecret := universalAuthCredsFromKubeSecret.Data[INFISICAL_MACHINE_IDENTITY_CLIENT_SECRET] + + return model.MachineIdentityDetails{ClientId: string(clientIdFromSecret), ClientSecret: string(clientSecretFromSecret)}, nil + +} diff --git a/k8-operator/packages/util/models.go b/k8-operator/packages/util/models.go new file mode 100644 index 000000000..8030731c2 --- /dev/null +++ b/k8-operator/packages/util/models.go @@ -0,0 +1,13 @@ +package util + +import ( + "context" + + infisicalSdk "github.com/infisical/go-sdk" +) + +type ResourceVariables struct { + InfisicalClient infisicalSdk.InfisicalClientInterface + CancelCtx context.CancelFunc + AuthDetails AuthenticationDetails +} diff --git a/k8-operator/packages/util/time.go b/k8-operator/packages/util/time.go new file mode 100644 index 000000000..0b78a16a6 --- /dev/null +++ b/k8-operator/packages/util/time.go @@ -0,0 +1,40 @@ +package util + +import ( + "fmt" + "strconv" + "time" +) + +func ConvertResyncIntervalToDuration(resyncInterval string) (time.Duration, error) { + length := len(resyncInterval) + if length < 2 { + return 0, fmt.Errorf("invalid format") + } + + unit := resyncInterval[length-1:] + numberPart := resyncInterval[:length-1] + + number, err := strconv.Atoi(numberPart) + if err != nil { + return 0, err + } + + switch unit { + case "s": + if number < 5 { + return 0, fmt.Errorf("resync interval must be at least 5 seconds") + } + return time.Duration(number) * time.Second, nil + case "m": + return time.Duration(number) * time.Minute, nil + case "h": + return time.Duration(number) * time.Hour, nil + case "d": + return time.Duration(number) * 24 * time.Hour, nil + case "w": + return time.Duration(number) * 7 * 24 * time.Hour, nil + default: + return 0, fmt.Errorf("invalid time unit") + } +} From 967dac9be63b7792fba6bad82c2cd97024ee03a5 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 29 Nov 2024 05:23:42 +0400 Subject: [PATCH 02/32] docs(k8-operator): push secrets --- docs/integrations/platforms/kubernetes.mdx | 421 ++++++++++++++++++++- 1 file changed, 417 insertions(+), 4 deletions(-) diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx index a925f6c4b..7edc8562a 100644 --- a/docs/integrations/platforms/kubernetes.mdx +++ b/docs/integrations/platforms/kubernetes.mdx @@ -781,9 +781,9 @@ type: Opaque -### Apply the Infisical CRD to your cluster +### Apply the InfisicalSecret CRD to your cluster -Once you have configured the Infisical CRD with the required fields, you can apply it to your cluster. +Once you have configured the InfisicalSecret CRD with the required fields, you can apply it to your cluster. After applying, you should notice that the managed secret has been created in the desired namespace your specified. ``` @@ -982,12 +982,12 @@ stringData: -----END CERTIFICATE----- ``` -## Auto redeployment +### Auto redeployment Deployments using managed secrets don't reload automatically on updates, so they may use outdated secrets unless manually redeployed. To address this, we added functionality to automatically redeploy your deployment when its managed secret updates. -### Enabling auto redeploy +#### Enabling auto redeploy To enable auto redeployment you simply have to add the following annotation to the deployment that consumes a managed secret @@ -1030,6 +1030,419 @@ spec: When a secret change occurs, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update. Then, for each deployment that has this annotation present, a rolling update will be triggered. + +## Push Secrets to Infisical + + +### Example usage + +Below is a sample InfisicalPushSecret CRD that pushes secrets defined in a Kubernetes secret to Infisical. + +After filling out the fields in the InfisicalPushSecret CRD, you can apply it directly to your cluster. + +Before applying the InfisicalPushSecret CRD, you need to create a Kubernetes secret containing the secrets you want to push to Infisical. An example can be seen below the InfisicalPushSecret CRD. + +```bash + kubectl apply -f source-secret.yaml +``` + +After applying the soruce-secret.yaml file, you are ready to apply the InfisicalPushSecret CRD. + +```bash + kubectl apply -f infisical-push-secret.yaml +``` + +After applying the InfisicalPushSecret CRD, you should notice that the secrets you have defined in your source-secret.yaml file have been pushed to your specified destination in Infisical. + +```yaml infisical-push-secret.yaml + apiVersion: secrets.infisical.com/v1alpha1 + kind: InfisicalPushSecret + metadata: + name: infisical-push-secret-demo + spec: + resyncInterval: 1m + hostAPI: https://app.infisical.com/api + + # Optional, defaults to no replacement. + updatePolicy: Replace # If set to replace, existing secrets inside Infisical will be replaced by the value of the PushSecret on sync. + + # Optional, defaults to no deletion. + deletionPolicy: Delete # If set to delete, the secret(s) inside Infisical managed by the operator, will be deleted if the InfisicalPushSecret CRD is deleted. + + destination: + projectId: + envSlug: + secretsPath: + + push: + secret: + secretName: push-secret-demo # Secret CRD + secretNamespace: default + + # Only have one authentication method defined or you are likely to run into authentication issues. + # Remove all except one authentication method. + authentication: + awsIamAuth: + identityId: + azureAuth: + identityId: + gcpIamAuth: + identityId: + serviceAccountKeyFilePath: + gcpIdTokenAuth: + identityId: + kubernetesAuth: + identityId: + serviceAccountRef: + name: + namespace: + universalAuth: + credentialsRef: + secretName: # universal-auth-credentials + secretNamespace: # default +``` + +```yaml source-secret.yaml + apiVersion: v1 + kind: Secret + metadata: + name: push-secret-demo + namespace: default + stringData: # can also be "data", but needs to be base64 encoded + API_KEY: some-api-key + DATABASE_URL: postgres://127.0.0.1:5432 + ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab +``` + +### InfisicalPushSecret CRD properties + + + If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to + ` https://your-self-hosted-instace.com/api` + + When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud. + + + If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet. + To achieve this, use the following address for the hostAPI field: + + ``` bash + http://..svc.cluster.local:4000/api + ``` + + Make sure to replace `` and `` with the appropriate values for your backend service and namespace. + + + + + + + The `resyncInterval` is a string-formatted duration that defines the time between each resync. + + The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time. + + The following units are supported: + - `s` for seconds (must be at least 5 seconds) + - `m` for minutes + - `h` for hours + - `d` for days + - `w` for weeks + + The default value is `1m` (1 minute). + + Valid intervals examples: + ```yaml + resyncInterval: 5s # 10 seconds + resyncInterval: 10s # 10 seconds + resyncInterval: 5m # 5 minutes + resyncInterval: 1h # 1 hour + resyncInterval: 1d # 1 day + ``` + + + + + The field is optional and will default to `None` if not defined. + + The update policy defines how the operator should handle conflicting secrets when pushing secrets to Infisical. + + Valid values are `None` and `Replace`. + + Behavior of each policy: + - `None`: The operator will not override existing secrets in Infisical. If a secret with the same key already exists, the operator will skip pushing that secret, and the secret will not be managed by the operator. + - `Replace`: The operator will replace existing secrets in Infisical with the new secrets. If a secret with the same key already exists, the operator will update the secret with the new value. + + ```yaml + spec: + updatePolicy: Replace + ``` + + + + + This field is optional and will default to `None` if not defined. + + The deletion policy defines what the operator should do in case the InfisicalPushSecret CRD is deleted. + + Valid values are `None` and `Delete`. + + Behavior of each policy: + - `None`: The operator will not delete the secrets in Infisical when the InfisicalPushSecret CRD is deleted. + - `Delete`: The operator will delete the secrets in Infisical that are managed by the operator when the InfisicalPushSecret CRD is deleted. + + ```yaml + spec: + deletionPolicy: Delete + ``` + + + + The `destination` field is used to specify where you want to create the secrets in Infisical. The required fields are `projectId`, `envSlug`, and `secretsPath`. + + ```yaml + spec: + destination: + projectId: + envSlug: + secretsPath: + ``` + + + The project ID where you want to create the secrets in Infisical. + + + + The environment slug where you want to create the secrets in Infisical. + + + + The path where you want to create the secrets in Infisical. The root path is `/`. + + + + + + The `push` field is used to define what you want to push to Infisical. Currently the operator only supports pushing Kubernetes secrets to Infisical. An example of the `push` field is shown below. + + + + + The `secret` field is used to define the Kubernetes secret you want to push to Infisical. The required fields are `secretName` and `secretNamespace`. + + + + Example usage of the `push.secret` field: + + ```yaml infisical-push-secret.yaml + push: + secret: + secretName: push-secret-demo + secretNamespace: default + ``` + + ```yaml push-secret-demo.yaml + apiVersion: v1 + kind: Secret + metadata: + name: push-secret-demo + namespace: default + # Pass in the secrets you wish to push to Infisical + stringData: + API_KEY: some-api-key + DATABASE_URL: postgres://127.0.0.1:5432 + ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab + ``` + + + + + + + The `authentication` field dictates which authentication method to use when pushing secrets to Infisical. + The available authentication methods are `universalAuth`, `kubernetesAuth`, `awsIamAuth`, `azureAuth`, `gcpIdTokenAuth`, and `gcpIamAuth`. + + + + The universal authentication method is one of the easiest ways to get started with Infisical. Universal Auth works anywhere and is not tied to any specific cloud provider. + [Read more about Universal Auth](/documentation/platform/identities/universal-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `credentialsRef`: The name and namespace of the Kubernetes secret that stores the service token. + - `credentialsRef.secretName`: The name of the Kubernetes secret. + - `credentialsRef.secretNamespace`: The namespace of the Kubernetes secret. + + Example: + + ```yaml + # infisical-push-secret.yaml + spec: + universalAuth: + credentialsRef: + secretName: + secretNamespace: + ``` + + ```yaml + # machine-identity-credentials.yaml + apiVersion: v1 + kind: Secret + metadata: + name: universal-auth-credentials + type: Opaque + stringData: + clientId: + clientSecret: + ``` + + + + The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment. + [Read more about Kubernetes Auth](/documentation/platform/identities/kubernetes-auth). + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `serviceAccountRef`: The name and namespace of the service account that will be used to authenticate with Infisical. + - `serviceAccountRef.name`: The name of the service account. + - `serviceAccountRef.namespace`: The namespace of the service account. + + Example: + + ```yaml + spec: + kubernetesAuth: + identityId: + serviceAccountRef: + name: + namespace: + ``` + + + + The AWS IAM machine identity authentication method is used to authenticate with Infisical. + [Read more about AWS IAM Auth](/documentation/platform/identities/aws-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + + Example: + + ```yaml + spec: + authentication: + awsIamAuth: + identityId: + ``` + + + + The AWS IAM machine identity authentication method is used to authenticate with Infisical. Azure Auth can only be used from within an Azure environment. + [Read more about Azure Auth](/documentation/platform/identities/azure-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + + Example: + + ```yaml + spec: + authentication: + azureAuth: + identityId: + ``` + + + The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments. + [Read more about Azure Auth](/documentation/platform/identities/gcp-auth). + + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `serviceAccountKeyFilePath`: The path to the GCP service account key file. + + Example: + + ```yaml + spec: + gcpIamAuth: + identityId: + serviceAccountKeyFilePath: + ``` + + + The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments. + [Read more about Azure Auth](/documentation/platform/identities/gcp-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + + Example: + + ```yaml + spec: + gcpIdTokenAuth: + identityId: + ``` + + + + + + + This block defines the TLS settings to use for connecting to the Infisical + instance. + + Fields: + + This block defines the reference to the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + + Valid fields: + - `secretName`: The name of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + - `secretNamespace`: The namespace of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + - `key`: The name of the key in the Kubernetes secret which contains the value of the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + + Example: + + ```yaml + tls: + caRef: + secretName: custom-ca-certificate + secretNamespace: default + key: ca.crt + ``` + + + + + +### Applying the InfisicalPushSecret CRD to your cluster + +Once you have configured the `InfisicalPushSecret` CRD with the required fields, you can apply it to your cluster. +After applying, you should notice that the secrets have been pushed to Infisical. + +```bash + kubectl apply -f source-push-secret.yaml # The secret that you're referencing in the InfisicalPushSecret CRD push.secret field + kubectl apply -f example-infisical-push-secret-crd.yaml # The InfisicalPushSecret CRD itself +``` + +### Connecting to instances with private/self-signed certificate + +To connect to Infisical instances behind a private/self-signed certificate, you can configure the TLS settings in the `InfisicalPushSecret` CRD +to point to a CA certificate stored in a Kubernetes secret resource. + +```yaml +spec: + hostAPI: https://app.infisical.com/api + resyncInterval: 30s + tls: + caRef: + secretName: custom-ca-certificate + secretNamespace: default + key: ca.crt + authentication: + # ... +``` + + ## Global configuration To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap. From a78455fde6033359fa2e82a45ba5efc6e74b0dbb Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 29 Nov 2024 05:23:48 +0400 Subject: [PATCH 03/32] remove print --- .../infisicalpushsecret/infisicalpushsecret_controller.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go index f52f27881..526b1059c 100644 --- a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go +++ b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go @@ -157,7 +157,7 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl. }, nil } - fmt.Println("Using custom CA certificate...") + logger.Info("Using custom CA certificate...") } else { api.API_CA_CERTIFICATE = "" } From e67b0540ddbad13da156c008da7e7f6dc3a21f5d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 29 Nov 2024 06:01:18 +0400 Subject: [PATCH 04/32] cleanup and resource seperation --- .../infisicalpushsecret_controller.go | 28 ++++++++++++---- .../infisicalpushsecret_helper.go | 10 +++--- .../infisicalsecret_controller.go | 33 ++++++++----------- .../infisicalsecret/infisicalsecret_helper.go | 10 +++--- 4 files changed, 45 insertions(+), 36 deletions(-) diff --git a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go index 526b1059c..2b6a64ca1 100644 --- a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go +++ b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go @@ -35,7 +35,7 @@ type InfisicalPushSecretReconciler struct { Scheme *runtime.Scheme } -var resourceVariablesMap map[string]util.ResourceVariables +var infisicalPushSecretResourceVariablesMap map[string]util.ResourceVariables = make(map[string]util.ResourceVariables) func (r *InfisicalPushSecretReconciler) GetLogger(req ctrl.Request) logr.Logger { return r.BaseLogger.WithValues("infisicalpushsecret", req.NamespacedName) @@ -61,10 +61,6 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl. var infisicalPushSecretCR secretsv1alpha1.InfisicalPushSecret requeueTime := time.Minute // seconds - if resourceVariablesMap == nil { - resourceVariablesMap = make(map[string]util.ResourceVariables) - } - err := r.Get(ctx, req.NamespacedName, &infisicalPushSecretCR) if err != nil { if errors.IsNotFound(err) { @@ -185,10 +181,30 @@ func (r *InfisicalPushSecretReconciler) SetupWithManager(mgr ctrl.Manager) error specChangeOrDelete := predicate.Funcs{ UpdateFunc: func(e event.UpdateEvent) bool { // Only reconcile if spec/generation changed - return e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() + + isSpecOrGenerationChange := e.ObjectOld.GetGeneration() != e.ObjectNew.GetGeneration() + + if isSpecOrGenerationChange { + if infisicalPushSecretResourceVariablesMap != nil { + if rv, ok := infisicalPushSecretResourceVariablesMap[string(e.ObjectNew.GetUID())]; ok { + rv.CancelCtx() + delete(infisicalPushSecretResourceVariablesMap, string(e.ObjectNew.GetUID())) + } + } + } + + return isSpecOrGenerationChange }, DeleteFunc: func(e event.DeleteEvent) bool { // Always reconcile on deletion + + if infisicalPushSecretResourceVariablesMap != nil { + if rv, ok := infisicalPushSecretResourceVariablesMap[string(e.Object.GetUID())]; ok { + rv.CancelCtx() + delete(infisicalPushSecretResourceVariablesMap, string(e.Object.GetUID())) + } + } + return true }, CreateFunc: func(e event.CreateEvent) bool { diff --git a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go index a13452b69..0ab0ab876 100644 --- a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go +++ b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go @@ -71,7 +71,7 @@ func (r *InfisicalPushSecretReconciler) getResourceVariables(infisicalPushSecret var resourceVariables util.ResourceVariables - if _, ok := resourceVariablesMap[string(infisicalPushSecret.UID)]; !ok { + if _, ok := infisicalPushSecretResourceVariablesMap[string(infisicalPushSecret.UID)]; !ok { ctx, cancel := context.WithCancel(context.Background()) @@ -81,16 +81,16 @@ func (r *InfisicalPushSecretReconciler) getResourceVariables(infisicalPushSecret UserAgent: api.USER_AGENT_NAME, }) - resourceVariablesMap[string(infisicalPushSecret.UID)] = util.ResourceVariables{ + infisicalPushSecretResourceVariablesMap[string(infisicalPushSecret.UID)] = util.ResourceVariables{ InfisicalClient: client, CancelCtx: cancel, AuthDetails: util.AuthenticationDetails{}, } - resourceVariables = resourceVariablesMap[string(infisicalPushSecret.UID)] + resourceVariables = infisicalPushSecretResourceVariablesMap[string(infisicalPushSecret.UID)] } else { - resourceVariables = resourceVariablesMap[string(infisicalPushSecret.UID)] + resourceVariables = infisicalPushSecretResourceVariablesMap[string(infisicalPushSecret.UID)] } return resourceVariables @@ -98,7 +98,7 @@ func (r *InfisicalPushSecretReconciler) getResourceVariables(infisicalPushSecret } func (r *InfisicalPushSecretReconciler) updateResourceVariables(infisicalPushSecret v1alpha1.InfisicalPushSecret, resourceVariables util.ResourceVariables) { - resourceVariablesMap[string(infisicalPushSecret.UID)] = resourceVariables + infisicalPushSecretResourceVariablesMap[string(infisicalPushSecret.UID)] = resourceVariables } func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context.Context, logger logr.Logger, infisicalPushSecret v1alpha1.InfisicalPushSecret) error { diff --git a/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go b/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go index 8b5508858..97d13c689 100644 --- a/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go +++ b/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go @@ -27,7 +27,13 @@ type InfisicalSecretReconciler struct { Scheme *runtime.Scheme } -var resourceVariablesMap map[string]util.ResourceVariables = make(map[string]util.ResourceVariables) +const FINALIZER_NAME = "secrets.finalizers.infisical.com" + +var infisicalSecretResourceVariablesMap map[string]util.ResourceVariables = make(map[string]util.ResourceVariables) + +func (r *InfisicalSecretReconciler) GetLogger(req ctrl.Request) logr.Logger { + return r.BaseLogger.WithValues("infisicalsecret", req.NamespacedName) +} //+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/status,verbs=get;update;patch @@ -42,15 +48,6 @@ var resourceVariablesMap map[string]util.ResourceVariables = make(map[string]uti // For more details, check Reconcile and its Result here: // - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.1/pkg/reconcile -const FINALIZER_NAME = "secrets.finalizers.infisical.com" - -// Maps the infisicalSecretCR.UID to a infisicalSdk.InfisicalClientInterface and AuthenticationDetails. -// var resourceVariablesMap = make(map[string]ResourceVariables) - -func (r *InfisicalSecretReconciler) GetLogger(req ctrl.Request) logr.Logger { - return r.BaseLogger.WithValues("infisicalsecret", req.NamespacedName) -} - func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := r.GetLogger(req) @@ -58,10 +55,6 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ var infisicalSecretCR secretsv1alpha1.InfisicalSecret requeueTime := time.Minute // seconds - if resourceVariablesMap == nil { - resourceVariablesMap = make(map[string]util.ResourceVariables) - } - err := r.Get(ctx, req.NamespacedName, &infisicalSecretCR) if err != nil { if errors.IsNotFound(err) { @@ -163,19 +156,19 @@ func (r *InfisicalSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { return ctrl.NewControllerManagedBy(mgr). For(&secretsv1alpha1.InfisicalSecret{}, builder.WithPredicates(predicate.Funcs{ UpdateFunc: func(e event.UpdateEvent) bool { - if resourceVariablesMap != nil { - if rv, ok := resourceVariablesMap[string(e.ObjectNew.GetUID())]; ok { + if infisicalSecretResourceVariablesMap != nil { + if rv, ok := infisicalSecretResourceVariablesMap[string(e.ObjectNew.GetUID())]; ok { rv.CancelCtx() - delete(resourceVariablesMap, string(e.ObjectNew.GetUID())) + delete(infisicalSecretResourceVariablesMap, string(e.ObjectNew.GetUID())) } } return true }, DeleteFunc: func(e event.DeleteEvent) bool { - if resourceVariablesMap != nil { - if rv, ok := resourceVariablesMap[string(e.Object.GetUID())]; ok { + if infisicalSecretResourceVariablesMap != nil { + if rv, ok := infisicalSecretResourceVariablesMap[string(e.Object.GetUID())]; ok { rv.CancelCtx() - delete(resourceVariablesMap, string(e.Object.GetUID())) + delete(infisicalSecretResourceVariablesMap, string(e.Object.GetUID())) } } return true diff --git a/k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go b/k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go index 7b9f8be93..28a9843c9 100644 --- a/k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go +++ b/k8-operator/controllers/infisicalsecret/infisicalsecret_helper.go @@ -296,7 +296,7 @@ func (r *InfisicalSecretReconciler) getResourceVariables(infisicalSecret v1alpha var resourceVariables util.ResourceVariables - if _, ok := resourceVariablesMap[string(infisicalSecret.UID)]; !ok { + if _, ok := infisicalSecretResourceVariablesMap[string(infisicalSecret.UID)]; !ok { ctx, cancel := context.WithCancel(context.Background()) @@ -306,16 +306,16 @@ func (r *InfisicalSecretReconciler) getResourceVariables(infisicalSecret v1alpha UserAgent: api.USER_AGENT_NAME, }) - resourceVariablesMap[string(infisicalSecret.UID)] = util.ResourceVariables{ + infisicalSecretResourceVariablesMap[string(infisicalSecret.UID)] = util.ResourceVariables{ InfisicalClient: client, CancelCtx: cancel, AuthDetails: util.AuthenticationDetails{}, } - resourceVariables = resourceVariablesMap[string(infisicalSecret.UID)] + resourceVariables = infisicalSecretResourceVariablesMap[string(infisicalSecret.UID)] } else { - resourceVariables = resourceVariablesMap[string(infisicalSecret.UID)] + resourceVariables = infisicalSecretResourceVariablesMap[string(infisicalSecret.UID)] } return resourceVariables @@ -323,7 +323,7 @@ func (r *InfisicalSecretReconciler) getResourceVariables(infisicalSecret v1alpha } func (r *InfisicalSecretReconciler) updateResourceVariables(infisicalSecret v1alpha1.InfisicalSecret, resourceVariables util.ResourceVariables) { - resourceVariablesMap[string(infisicalSecret.UID)] = resourceVariables + infisicalSecretResourceVariablesMap[string(infisicalSecret.UID)] = resourceVariables } func (r *InfisicalSecretReconciler) ReconcileInfisicalSecret(ctx context.Context, logger logr.Logger, infisicalSecret v1alpha1.InfisicalSecret) error { From 2f922d63432bca468f340f7e8d268abfa6bff5db Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 6 Dec 2024 01:43:41 +0400 Subject: [PATCH 05/32] fix: requested changes --- .../api/v1alpha1/infisicalpushsecret_types.go | 2 +- ...ts.infisical.com_infisicalpushsecrets.yaml | 4 +- .../infisicalpushsecret/conditions.go | 6 +-- .../infisicalpushsecret_controller.go | 40 +++++++++---------- .../infisicalpushsecret_helper.go | 22 +++++----- .../infisicalsecret_controller.go | 34 ++++++++-------- 6 files changed, 54 insertions(+), 54 deletions(-) diff --git a/k8-operator/api/v1alpha1/infisicalpushsecret_types.go b/k8-operator/api/v1alpha1/infisicalpushsecret_types.go index 336534ed2..334b5a847 100644 --- a/k8-operator/api/v1alpha1/infisicalpushsecret_types.go +++ b/k8-operator/api/v1alpha1/infisicalpushsecret_types.go @@ -10,7 +10,7 @@ type InfisicalPushSecretDestination struct { SecretsPath string `json:"secretsPath"` // +kubebuilder:validation:Required // +kubebuilder:validation:Immutable - EnvSlug string `json:"envSlug"` + EnvironmentSlug string `json:"EnvironmentSlug"` // +kubebuilder:validation:Required // +kubebuilder:validation:Immutable ProjectID string `json:"projectId"` diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml index a4ee1e21f..de0cd0b16 100644 --- a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml +++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml @@ -114,14 +114,14 @@ spec: type: string destination: properties: - envSlug: + EnvironmentSlug: type: string projectId: type: string secretsPath: type: string required: - - envSlug + - EnvironmentSlug - projectId - secretsPath type: object diff --git a/k8-operator/controllers/infisicalpushsecret/conditions.go b/k8-operator/controllers/infisicalpushsecret/conditions.go index bd1851eb2..335ea294d 100644 --- a/k8-operator/controllers/infisicalpushsecret/conditions.go +++ b/k8-operator/controllers/infisicalpushsecret/conditions.go @@ -51,7 +51,7 @@ func (r *InfisicalPushSecretReconciler) SetFailedToReplaceSecretsConditions(ctx Type: "secrets.infisical.com/FailedToReplaceSecrets", Status: metav1.ConditionFalse, Reason: "OK", - Message: "No errors, no secrets failed to be replaced", + Message: "No errors, no secrets failed to be replaced in Infisical", }) } @@ -75,7 +75,7 @@ func (r *InfisicalPushSecretReconciler) SetFailedToCreateSecretsConditions(ctx c Type: "secrets.infisical.com/FailedToCreateSecrets", Status: metav1.ConditionFalse, Reason: "OK", - Message: "No errors, no secrets failed to be created", + Message: "No errors, no secrets failed to be created in Infisical", }) } @@ -99,7 +99,7 @@ func (r *InfisicalPushSecretReconciler) SetFailedToUpdateSecretsConditions(ctx c Type: "secrets.infisical.com/FailedToUpdateSecrets", Status: metav1.ConditionFalse, Reason: "OK", - Message: "No errors, no secrets failed to be updated", + Message: "No errors, no secrets failed to be updated in Infisical", }) } diff --git a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go index 2b6a64ca1..af5428ee7 100644 --- a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go +++ b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_controller.go @@ -58,10 +58,10 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl. logger := r.GetLogger(req) - var infisicalPushSecretCR secretsv1alpha1.InfisicalPushSecret + var infisicalPushSecretCRD secretsv1alpha1.InfisicalPushSecret requeueTime := time.Minute // seconds - err := r.Get(ctx, req.NamespacedName, &infisicalPushSecretCR) + err := r.Get(ctx, req.NamespacedName, &infisicalPushSecretCRD) if err != nil { if errors.IsNotFound(err) { logger.Info("Infisical Push Secret CRD not found") @@ -77,25 +77,25 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl. } // Add finalizer if it doesn't exist - if !controllerutil.ContainsFinalizer(&infisicalPushSecretCR, constants.INFISICAL_PUSH_SECRET_FINALIZER_NAME) { - controllerutil.AddFinalizer(&infisicalPushSecretCR, constants.INFISICAL_PUSH_SECRET_FINALIZER_NAME) - if err := r.Update(ctx, &infisicalPushSecretCR); err != nil { + if !controllerutil.ContainsFinalizer(&infisicalPushSecretCRD, constants.INFISICAL_PUSH_SECRET_FINALIZER_NAME) { + controllerutil.AddFinalizer(&infisicalPushSecretCRD, constants.INFISICAL_PUSH_SECRET_FINALIZER_NAME) + if err := r.Update(ctx, &infisicalPushSecretCRD); err != nil { return ctrl.Result{}, err } } // Check if it's being deleted - if !infisicalPushSecretCR.DeletionTimestamp.IsZero() { + if !infisicalPushSecretCRD.DeletionTimestamp.IsZero() { logger.Info("Handling deletion of InfisicalPushSecret") - if controllerutil.ContainsFinalizer(&infisicalPushSecretCR, constants.INFISICAL_PUSH_SECRET_FINALIZER_NAME) { + if controllerutil.ContainsFinalizer(&infisicalPushSecretCRD, constants.INFISICAL_PUSH_SECRET_FINALIZER_NAME) { // We remove finalizers before running deletion logic to be completely safe from stuck resources - infisicalPushSecretCR.ObjectMeta.Finalizers = []string{} - if err := r.Update(ctx, &infisicalPushSecretCR); err != nil { - logger.Error(err, fmt.Sprintf("Error removing finalizers from InfisicalPushSecret %s", infisicalPushSecretCR.Name)) + infisicalPushSecretCRD.ObjectMeta.Finalizers = []string{} + if err := r.Update(ctx, &infisicalPushSecretCRD); err != nil { + logger.Error(err, fmt.Sprintf("Error removing finalizers from InfisicalPushSecret %s", infisicalPushSecretCRD.Name)) return ctrl.Result{}, err } - if err := r.DeleteManagedSecrets(ctx, logger, infisicalPushSecretCR); err != nil { + if err := r.DeleteManagedSecrets(ctx, logger, infisicalPushSecretCRD); err != nil { return ctrl.Result{}, err // Even if this fails, we still want to delete the CRD } @@ -103,9 +103,9 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl. return ctrl.Result{}, nil } - if infisicalPushSecretCR.Spec.ResyncInterval != "" { + if infisicalPushSecretCRD.Spec.ResyncInterval != "" { - duration, err := util.ConvertResyncIntervalToDuration(infisicalPushSecretCR.Spec.ResyncInterval) + duration, err := util.ConvertResyncIntervalToDuration(infisicalPushSecretCRD.Spec.ResyncInterval) if err != nil { logger.Error(err, fmt.Sprintf("unable to convert resync interval to duration. Will requeue after [requeueTime=%v]", requeueTime)) @@ -123,7 +123,7 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl. } // Check if the resource is already marked for deletion - if infisicalPushSecretCR.GetDeletionTimestamp() != nil { + if infisicalPushSecretCRD.GetDeletionTimestamp() != nil { return ctrl.Result{ Requeue: false, }, nil @@ -138,14 +138,14 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl. }, nil } - if infisicalPushSecretCR.Spec.HostAPI == "" { + if infisicalPushSecretCRD.Spec.HostAPI == "" { api.API_HOST_URL = infisicalConfig["hostAPI"] } else { - api.API_HOST_URL = infisicalPushSecretCR.Spec.HostAPI + api.API_HOST_URL = infisicalPushSecretCRD.Spec.HostAPI } - if infisicalPushSecretCR.Spec.TLS.CaRef.SecretName != "" { - api.API_CA_CERTIFICATE, err = r.getInfisicalCaCertificateFromKubeSecret(ctx, infisicalPushSecretCR) + if infisicalPushSecretCRD.Spec.TLS.CaRef.SecretName != "" { + api.API_CA_CERTIFICATE, err = r.getInfisicalCaCertificateFromKubeSecret(ctx, infisicalPushSecretCRD) if err != nil { logger.Error(err, fmt.Sprintf("unable to fetch CA certificate. Will requeue after [requeueTime=%v]", requeueTime)) return ctrl.Result{ @@ -158,8 +158,8 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl. api.API_CA_CERTIFICATE = "" } - err = r.ReconcileInfisicalPushSecret(ctx, logger, infisicalPushSecretCR) - r.SetSuccessfullyReconciledConditions(ctx, &infisicalPushSecretCR, err) + err = r.ReconcileInfisicalPushSecret(ctx, logger, infisicalPushSecretCRD) + r.SetSuccessfullyReconciledConditions(ctx, &infisicalPushSecretCRD, err) if err != nil { logger.Error(err, fmt.Sprintf("unable to reconcile Infisical Push Secret. Will requeue after [requeueTime=%v]", requeueTime)) diff --git a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go index 0ab0ab876..ebe125a49 100644 --- a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go +++ b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go @@ -143,7 +143,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context destination := infisicalPushSecret.Spec.Destination existingSecrets, err := infisicalClient.Secrets().List(infisicalSdk.ListSecretsOptions{ ProjectID: destination.ProjectID, - Environment: destination.EnvSlug, + Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, IncludeImports: false, }) @@ -197,7 +197,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{ SecretKey: secretKey, ProjectID: destination.ProjectID, - Environment: destination.EnvSlug, + Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, NewSecretValue: secretValue, }) @@ -215,7 +215,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context SecretKey: secretKey, SecretValue: secretValue, ProjectID: destination.ProjectID, - Environment: destination.EnvSlug, + Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, }) @@ -245,7 +245,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context deletedSecret, err := infisicalClient.Secrets().Delete(infisicalSdk.DeleteSecretOptions{ SecretKey: existingSecret.SecretKey, ProjectID: destination.ProjectID, - Environment: destination.EnvSlug, + Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, }) @@ -259,7 +259,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context SecretKey: managedSecretKey, SecretValue: existingSecret.SecretValue, ProjectID: destination.ProjectID, - Environment: destination.EnvSlug, + Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, }) @@ -288,7 +288,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context deletedSecret, err := infisicalClient.Secrets().Delete(infisicalSdk.DeleteSecretOptions{ SecretKey: managedSecretKey, ProjectID: destination.ProjectID, - Environment: destination.EnvSlug, + Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, }) @@ -318,7 +318,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context SecretKey: currentSecretKey, SecretValue: kubeSecrets[currentSecretKey], ProjectID: destination.ProjectID, - Environment: destination.EnvSlug, + Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, }) @@ -336,7 +336,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context SecretKey: currentSecretKey, NewSecretValue: kubeSecrets[currentSecretKey], ProjectID: destination.ProjectID, - Environment: destination.EnvSlug, + Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, }) @@ -369,7 +369,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context SecretKey: secretKey, NewSecretValue: secretValue, ProjectID: destination.ProjectID, - Environment: destination.EnvSlug, + Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, }) @@ -435,7 +435,7 @@ func (r *InfisicalPushSecretReconciler) DeleteManagedSecrets(ctx context.Context destination := infisicalPushSecret.Spec.Destination existingSecrets, err := infisicalClient.Secrets().List(infisicalSdk.ListSecretsOptions{ ProjectID: destination.ProjectID, - Environment: destination.EnvSlug, + Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, IncludeImports: false, }) @@ -457,7 +457,7 @@ func (r *InfisicalPushSecretReconciler) DeleteManagedSecrets(ctx context.Context _, err := infisicalClient.Secrets().Delete(infisicalSdk.DeleteSecretOptions{ SecretKey: managedSecretKey, ProjectID: destination.ProjectID, - Environment: destination.EnvSlug, + Environment: destination.EnvironmentSlug, SecretPath: destination.SecretsPath, }) diff --git a/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go b/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go index 97d13c689..f5a974a67 100644 --- a/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go +++ b/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go @@ -52,10 +52,10 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ logger := r.GetLogger(req) - var infisicalSecretCR secretsv1alpha1.InfisicalSecret + var infisicalSecretCRD secretsv1alpha1.InfisicalSecret requeueTime := time.Minute // seconds - err := r.Get(ctx, req.NamespacedName, &infisicalSecretCR) + err := r.Get(ctx, req.NamespacedName, &infisicalSecretCRD) if err != nil { if errors.IsNotFound(err) { return ctrl.Result{ @@ -71,18 +71,18 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ // Remove finalizers if they exist. This is to support previous InfisicalSecret CRD's that have finalizers on them. // In order to delete secrets with finalizers, we first remove the finalizers so we can use the simplified and improved deletion process - if !infisicalSecretCR.ObjectMeta.DeletionTimestamp.IsZero() && len(infisicalSecretCR.ObjectMeta.Finalizers) > 0 { - infisicalSecretCR.ObjectMeta.Finalizers = []string{} - if err := r.Update(ctx, &infisicalSecretCR); err != nil { - logger.Error(err, fmt.Sprintf("Error removing finalizers from Infisical Secret %s", infisicalSecretCR.Name)) + if !infisicalSecretCRD.ObjectMeta.DeletionTimestamp.IsZero() && len(infisicalSecretCRD.ObjectMeta.Finalizers) > 0 { + infisicalSecretCRD.ObjectMeta.Finalizers = []string{} + if err := r.Update(ctx, &infisicalSecretCRD); err != nil { + logger.Error(err, fmt.Sprintf("Error removing finalizers from Infisical Secret %s", infisicalSecretCRD.Name)) return ctrl.Result{}, err } // Our finalizers have been removed, so the reconciler can do nothing. return ctrl.Result{}, nil } - if infisicalSecretCR.Spec.ResyncInterval != 0 { - requeueTime = time.Second * time.Duration(infisicalSecretCR.Spec.ResyncInterval) + if infisicalSecretCRD.Spec.ResyncInterval != 0 { + requeueTime = time.Second * time.Duration(infisicalSecretCRD.Spec.ResyncInterval) logger.Info(fmt.Sprintf("Manual re-sync interval set. Interval: %v", requeueTime)) } else { @@ -90,7 +90,7 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ } // Check if the resource is already marked for deletion - if infisicalSecretCR.GetDeletionTimestamp() != nil { + if infisicalSecretCRD.GetDeletionTimestamp() != nil { return ctrl.Result{ Requeue: false, }, nil @@ -105,14 +105,14 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ }, nil } - if infisicalSecretCR.Spec.HostAPI == "" { + if infisicalSecretCRD.Spec.HostAPI == "" { api.API_HOST_URL = infisicalConfig["hostAPI"] } else { - api.API_HOST_URL = infisicalSecretCR.Spec.HostAPI + api.API_HOST_URL = infisicalSecretCRD.Spec.HostAPI } - if infisicalSecretCR.Spec.TLS.CaRef.SecretName != "" { - api.API_CA_CERTIFICATE, err = r.getInfisicalCaCertificateFromKubeSecret(ctx, infisicalSecretCR) + if infisicalSecretCRD.Spec.TLS.CaRef.SecretName != "" { + api.API_CA_CERTIFICATE, err = r.getInfisicalCaCertificateFromKubeSecret(ctx, infisicalSecretCRD) if err != nil { logger.Error(err, fmt.Sprintf("unable to fetch CA certificate. Will requeue after [requeueTime=%v]", requeueTime)) return ctrl.Result{ @@ -125,8 +125,8 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ api.API_CA_CERTIFICATE = "" } - err = r.ReconcileInfisicalSecret(ctx, logger, infisicalSecretCR) - r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCR, err) + err = r.ReconcileInfisicalSecret(ctx, logger, infisicalSecretCRD) + r.SetReadyToSyncSecretsConditions(ctx, &infisicalSecretCRD, err) if err != nil { @@ -136,8 +136,8 @@ func (r *InfisicalSecretReconciler) Reconcile(ctx context.Context, req ctrl.Requ }, nil } - numDeployments, err := r.ReconcileDeploymentsWithManagedSecrets(ctx, logger, infisicalSecretCR) - r.SetInfisicalAutoRedeploymentReady(ctx, logger, &infisicalSecretCR, numDeployments, err) + numDeployments, err := r.ReconcileDeploymentsWithManagedSecrets(ctx, logger, infisicalSecretCRD) + 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)) return ctrl.Result{ From 1b48ce21be666f7c44d8f2cda1751b08613f9f7f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 6 Dec 2024 01:44:57 +0400 Subject: [PATCH 06/32] Update conditions.go --- k8-operator/controllers/infisicalpushsecret/conditions.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/k8-operator/controllers/infisicalpushsecret/conditions.go b/k8-operator/controllers/infisicalpushsecret/conditions.go index 335ea294d..442cdaaf2 100644 --- a/k8-operator/controllers/infisicalpushsecret/conditions.go +++ b/k8-operator/controllers/infisicalpushsecret/conditions.go @@ -75,7 +75,7 @@ func (r *InfisicalPushSecretReconciler) SetFailedToCreateSecretsConditions(ctx c Type: "secrets.infisical.com/FailedToCreateSecrets", Status: metav1.ConditionFalse, Reason: "OK", - Message: "No errors, no secrets failed to be created in Infisical", + Message: "No errors encountered, no secrets failed to be created in Infisical", }) } @@ -99,7 +99,7 @@ func (r *InfisicalPushSecretReconciler) SetFailedToUpdateSecretsConditions(ctx c Type: "secrets.infisical.com/FailedToUpdateSecrets", Status: metav1.ConditionFalse, Reason: "OK", - Message: "No errors, no secrets failed to be updated in Infisical", + Message: "No errors encountered, no secrets failed to be updated in Infisical", }) } @@ -123,7 +123,7 @@ func (r *InfisicalPushSecretReconciler) SetFailedToDeleteSecretsConditions(ctx c Type: "secrets.infisical.com/FailedToDeleteSecrets", Status: metav1.ConditionFalse, Reason: "OK", - Message: "No errors, no secrets failed to be deleted", + Message: "No errors encountered, no secrets failed to be deleted", }) } From 41ba111a694049d5c76dd1346c77c268a9593aa1 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 6 Dec 2024 02:00:52 +0400 Subject: [PATCH 07/32] Update PROJECT --- k8-operator/PROJECT | 33 +++++++++++++++++++++++---------- 1 file changed, 23 insertions(+), 10 deletions(-) diff --git a/k8-operator/PROJECT b/k8-operator/PROJECT index 968a9a3d1..e57b8bee5 100644 --- a/k8-operator/PROJECT +++ b/k8-operator/PROJECT @@ -1,16 +1,29 @@ +# Code generated by tool. DO NOT EDIT. +# This file is used to track the info used to scaffold your project +# and allow the plugins properly work. +# More info: https://book.kubebuilder.io/reference/project-config.html domain: infisical.com layout: -- go.kubebuilder.io/v3 + - go.kubebuilder.io/v3 projectName: k8-operator repo: github.com/Infisical/infisical/k8-operator resources: -- api: - crdVersion: v1 - namespaced: true - controller: true - domain: infisical.com - group: secrets - kind: InfisicalSecret - path: github.com/Infisical/infisical/k8-operator/api/v1alpha1 - version: v1alpha1 + - api: + crdVersion: v1 + namespaced: true + controller: true + domain: infisical.com + group: secrets + kind: InfisicalSecret + path: github.com/Infisical/infisical/k8-operator/api/v1alpha1 + version: v1alpha1 + - api: + crdVersion: v1 + namespaced: true + controller: true + domain: infisical.com + group: secrets + kind: InfisicalPushSecretSecret + path: github.com/Infisical/infisical/k8-operator/api/v1alpha1 + version: v1alpha1 version: "3" From adb08191022b859f24a76fa45c394207f8d40608 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 6 Dec 2024 02:06:35 +0400 Subject: [PATCH 08/32] Added RBAC --- .../rbac/infisicalpushsecret_editor_role.yaml | 27 +++++++++++++++++++ .../rbac/infisicalpushsecret_viewer_role.yaml | 23 ++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 k8-operator/config/rbac/infisicalpushsecret_editor_role.yaml create mode 100644 k8-operator/config/rbac/infisicalpushsecret_viewer_role.yaml diff --git a/k8-operator/config/rbac/infisicalpushsecret_editor_role.yaml b/k8-operator/config/rbac/infisicalpushsecret_editor_role.yaml new file mode 100644 index 000000000..9344e17c5 --- /dev/null +++ b/k8-operator/config/rbac/infisicalpushsecret_editor_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to edit infisicalpushsecrets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: k8-operator + app.kubernetes.io/managed-by: kustomize + name: infisicalpushsecret-editor-role +rules: + - apiGroups: + - secrets.infisical.com + resources: + - infisicalpushsecrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch + - apiGroups: + - secrets.infisical.com + resources: + - infisicalpushsecrets/status + verbs: + - get diff --git a/k8-operator/config/rbac/infisicalpushsecret_viewer_role.yaml b/k8-operator/config/rbac/infisicalpushsecret_viewer_role.yaml new file mode 100644 index 000000000..ff4df91cc --- /dev/null +++ b/k8-operator/config/rbac/infisicalpushsecret_viewer_role.yaml @@ -0,0 +1,23 @@ +# permissions for end users to view infisicalpushsecrets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: k8-operator + app.kubernetes.io/managed-by: kustomize + name: infisicalpushsecret-viewer-role +rules: + - apiGroups: + - secrets.infisical.com + resources: + - infisicalpushsecrets + verbs: + - get + - list + - watch + - apiGroups: + - secrets.infisical.com + resources: + - infisicalpushsecrets/status + verbs: + - get From 01dcbb0122ebe707d0989857309df9cb5c8e06b8 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 6 Dec 2024 02:54:50 +0400 Subject: [PATCH 09/32] updated env slugs --- docs/integrations/platforms/kubernetes.mdx | 8 ++++---- k8-operator/api/v1alpha1/infisicalpushsecret_types.go | 2 +- k8-operator/config/samples/crd/pushsecret/pushSecret.yaml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx index 7edc8562a..0a61f4682 100644 --- a/docs/integrations/platforms/kubernetes.mdx +++ b/docs/integrations/platforms/kubernetes.mdx @@ -1071,7 +1071,7 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y destination: projectId: - envSlug: + environmentSlug: secretsPath: push: @@ -1197,13 +1197,13 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y - The `destination` field is used to specify where you want to create the secrets in Infisical. The required fields are `projectId`, `envSlug`, and `secretsPath`. + The `destination` field is used to specify where you want to create the secrets in Infisical. The required fields are `projectId`, `environmentSlug`, and `secretsPath`. ```yaml spec: destination: projectId: - envSlug: + environmentSlug: secretsPath: ``` @@ -1211,7 +1211,7 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y The project ID where you want to create the secrets in Infisical. - + The environment slug where you want to create the secrets in Infisical. diff --git a/k8-operator/api/v1alpha1/infisicalpushsecret_types.go b/k8-operator/api/v1alpha1/infisicalpushsecret_types.go index 334b5a847..a25e7ae9c 100644 --- a/k8-operator/api/v1alpha1/infisicalpushsecret_types.go +++ b/k8-operator/api/v1alpha1/infisicalpushsecret_types.go @@ -10,7 +10,7 @@ type InfisicalPushSecretDestination struct { SecretsPath string `json:"secretsPath"` // +kubebuilder:validation:Required // +kubebuilder:validation:Immutable - EnvironmentSlug string `json:"EnvironmentSlug"` + EnvironmentSlug string `json:"environmentSlug"` // +kubebuilder:validation:Required // +kubebuilder:validation:Immutable ProjectID string `json:"projectId"` diff --git a/k8-operator/config/samples/crd/pushsecret/pushSecret.yaml b/k8-operator/config/samples/crd/pushsecret/pushSecret.yaml index 8df2d3da5..ab54f1384 100644 --- a/k8-operator/config/samples/crd/pushsecret/pushSecret.yaml +++ b/k8-operator/config/samples/crd/pushsecret/pushSecret.yaml @@ -14,7 +14,7 @@ spec: destination: projectId: - envSlug: + environmentSlug: secretsPath: push: From 92a80b3314a79ab3d4070d331a386edaef150d92 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sun, 8 Dec 2024 21:21:14 +0400 Subject: [PATCH 10/32] fix(k8-operator): helm and cleanup --- helm-charts/secrets-operator/Chart.yaml | 4 +- .../templates/infisicalpushsecret-crd.yaml | 268 ++++++++++++++++++ .../templates/manager-rbac.yaml | 26 ++ helm-charts/secrets-operator/values.yaml | 2 +- .../api/v1alpha1/infisicalpushsecret_types.go | 1 - ...ts.infisical.com_infisicalpushsecrets.yaml | 5 +- 6 files changed, 299 insertions(+), 7 deletions(-) create mode 100644 helm-charts/secrets-operator/templates/infisicalpushsecret-crd.yaml diff --git a/helm-charts/secrets-operator/Chart.yaml b/helm-charts/secrets-operator/Chart.yaml index f212ce4eb..ca6747984 100644 --- a/helm-charts/secrets-operator/Chart.yaml +++ b/helm-charts/secrets-operator/Chart.yaml @@ -13,9 +13,9 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: v0.7.5 +version: v0.7.6 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v0.7.5" +appVersion: "v0.7.6" diff --git a/helm-charts/secrets-operator/templates/infisicalpushsecret-crd.yaml b/helm-charts/secrets-operator/templates/infisicalpushsecret-crd.yaml new file mode 100644 index 000000000..3d3239fbb --- /dev/null +++ b/helm-charts/secrets-operator/templates/infisicalpushsecret-crd.yaml @@ -0,0 +1,268 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: infisicalpushsecrets.secrets.infisical.com + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + labels: + {{- include "secrets-operator.labels" . | nindent 4 }} +spec: + group: secrets.infisical.com + names: + kind: InfisicalPushSecret + listKind: InfisicalPushSecretList + plural: infisicalpushsecrets + singular: infisicalpushsecret + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: InfisicalPushSecret is the Schema for the infisicalpushsecrets + 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: InfisicalPushSecretSpec defines the desired state of InfisicalPushSecret + 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: + description: PushSecretUniversalAuth defines universal authentication + 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 + deletionPolicy: + type: string + destination: + properties: + environmentSlug: + type: string + projectId: + type: string + secretsPath: + type: string + required: + - environmentSlug + - projectId + - secretsPath + type: object + hostAPI: + description: Infisical host to pull secrets from + type: string + push: + properties: + secret: + 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: + - secret + type: object + resyncInterval: + type: string + 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 + updatePolicy: + type: string + required: + - destination + - push + - resyncInterval + type: object + status: + description: InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a foo's + current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating details + about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers of + specific condition types may define expected values and meanings + for this field, and whether the values are considered a guaranteed + API. The value should be a CamelCase string. This field may + not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + managedSecrets: + additionalProperties: + type: string + description: managed secrets is a map where the key is the ID, and the + value is the secret key (string[id], string[key] ) + type: object + required: + - conditions + - managedSecrets + type: object + type: object + served: true + storage: true + subresources: + status: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] \ No newline at end of file diff --git a/helm-charts/secrets-operator/templates/manager-rbac.yaml b/helm-charts/secrets-operator/templates/manager-rbac.yaml index ca6fd36e1..12cb11a64 100644 --- a/helm-charts/secrets-operator/templates/manager-rbac.yaml +++ b/helm-charts/secrets-operator/templates/manager-rbac.yaml @@ -44,6 +44,32 @@ rules: - list - update - watch +- apiGroups: + - secrets.infisical.com + resources: + - infisicalpushsecrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - secrets.infisical.com + resources: + - infisicalpushsecrets/finalizers + verbs: + - update +- apiGroups: + - secrets.infisical.com + resources: + - infisicalpushsecrets/status + verbs: + - get + - patch + - update - apiGroups: - secrets.infisical.com resources: diff --git a/helm-charts/secrets-operator/values.yaml b/helm-charts/secrets-operator/values.yaml index dc342c5ac..342c4ea1b 100644 --- a/helm-charts/secrets-operator/values.yaml +++ b/helm-charts/secrets-operator/values.yaml @@ -32,7 +32,7 @@ controllerManager: - ALL image: repository: infisical/kubernetes-operator - tag: v0.7.5 + tag: v0.7.6 resources: limits: cpu: 500m diff --git a/k8-operator/api/v1alpha1/infisicalpushsecret_types.go b/k8-operator/api/v1alpha1/infisicalpushsecret_types.go index a25e7ae9c..1eb466d0d 100644 --- a/k8-operator/api/v1alpha1/infisicalpushsecret_types.go +++ b/k8-operator/api/v1alpha1/infisicalpushsecret_types.go @@ -52,7 +52,6 @@ type PushSecretGcpIamAuth struct { ServiceAccountKeyFilePath string `json:"serviceAccountKeyFilePath"` } -// Rest of your types should be defined similarly... type PushSecretKubernetesAuth struct { // +kubebuilder:validation:Required IdentityID string `json:"identityId"` diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml index de0cd0b16..5e37fdbfa 100644 --- a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml +++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml @@ -72,7 +72,6 @@ spec: - identityId type: object kubernetesAuth: - description: Rest of your types should be defined similarly... properties: identityId: type: string @@ -114,14 +113,14 @@ spec: type: string destination: properties: - EnvironmentSlug: + environmentSlug: type: string projectId: type: string secretsPath: type: string required: - - EnvironmentSlug + - environmentSlug - projectId - secretsPath type: object From 5ad8dab250de55c817d8487aecf7cef66c7397ac Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sun, 8 Dec 2024 21:29:45 +0400 Subject: [PATCH 11/32] fix(k8-operator): resource-based finalizer names --- k8-operator/packages/constants/constants.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/k8-operator/packages/constants/constants.go b/k8-operator/packages/constants/constants.go index ff28e408d..909d806e5 100644 --- a/k8-operator/packages/constants/constants.go +++ b/k8-operator/packages/constants/constants.go @@ -13,7 +13,7 @@ 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" type PushSecretReplacePolicy string type PushSecretDeletionPolicy string From 0d35273857e00266958bd843007b9b32d05af314 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Fri, 29 Nov 2024 03:05:50 +0400 Subject: [PATCH 12/32] feat(k8-operator): push secrets --- ...ts.infisical.com_infisicalpushsecrets.yaml | 295 ++++++++++++++---- .../infisicalsecret_controller.go | 2 + 2 files changed, 232 insertions(+), 65 deletions(-) diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml index 5e37fdbfa..707bfdec6 100644 --- a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml +++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml @@ -191,74 +191,239 @@ spec: `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" properties: - lastTransitionTime: - description: lastTransitionTime is the last time the condition - transitioned from one status to another. This should be when - the underlying condition changed. If that is not known, then - using the time when the API field changed is acceptable. - format: date-time + 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: + description: Rest of your types should be defined similarly... + properties: + identityId: + type: string + serviceAccountRef: + properties: + name: + type: string + namespace: + type: string + required: + - name + - namespace + type: object + required: + - identityId + - serviceAccountRef + type: object + universalAuth: + description: PushSecretUniversalAuth defines universal authentication + 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 + deletionPolicy: + type: string + destination: + properties: + EnvironmentSlug: type: string - message: - description: message is a human readable message indicating - details about the transition. This may be an empty string. - maxLength: 32768 + projectId: type: string - observedGeneration: - description: observedGeneration represents the .metadata.generation - that the condition was set based upon. For instance, if .metadata.generation - is currently 12, but the .status.conditions[x].observedGeneration - is 9, the condition is out of date with respect to the current - state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: reason contains a programmatic identifier indicating - the reason for the condition's last transition. Producers - of specific condition types may define expected values and - meanings for this field, and whether the values are considered - a guaranteed API. The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: type of condition in CamelCase or in foo.example.com/CamelCase. - --- Many .condition.type values are consistent across resources - like Available, but because arbitrary conditions can be useful - (see .node.status.conditions), the ability to deconflict is - important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + secretsPath: type: string required: - - lastTransitionTime - - message - - reason - - status - - type + - EnvironmentSlug + - projectId + - secretsPath type: object - type: array - managedSecrets: - additionalProperties: + hostAPI: + description: Infisical host to pull secrets from type: string - description: managed secrets is a map where the key is the ID, and - the value is the secret key (string[id], string[key] ) - type: object - required: - - conditions - - managedSecrets - type: object - type: object - served: true - storage: true - subresources: - status: {} + push: + properties: + secret: + 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: + - secret + type: object + resyncInterval: + type: string + 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 + updatePolicy: + type: string + required: + - destination + - push + - resyncInterval + type: object + status: + description: InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret + properties: + conditions: + items: + description: + "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: + lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: + message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: + observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: + reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: + type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + managedSecrets: + additionalProperties: + type: string + description: + managed secrets is a map where the key is the ID, and + the value is the secret key (string[id], string[key] ) + type: object + required: + - conditions + - managedSecrets + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go b/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go index f5a974a67..9e4656c55 100644 --- a/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go +++ b/k8-operator/controllers/infisicalsecret/infisicalsecret_controller.go @@ -35,6 +35,8 @@ func (r *InfisicalSecretReconciler) GetLogger(req ctrl.Request) logr.Logger { return r.BaseLogger.WithValues("infisicalsecret", req.NamespacedName) } +var resourceVariablesMap map[string]util.ResourceVariables = make(map[string]util.ResourceVariables) + //+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets,verbs=get;list;watch;create;update;patch;delete //+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/status,verbs=get;update;patch //+kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicalsecrets/finalizers,verbs=update From 4552f0efa434d88900c65bd799d8474823303440 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 7 Dec 2024 00:13:18 +0400 Subject: [PATCH 13/32] 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 | 62 +-- .../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 | 11 + .../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, 1741 insertions(+), 325 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 e57b8bee5..59ebed6f6 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 000000000..387bc7c9e --- /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 000000000..142cb671f --- /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 1eb466d0d..9a1040868 100644 --- a/k8-operator/api/v1alpha1/infisicalpushsecret_types.go +++ b/k8-operator/api/v1alpha1/infisicalpushsecret_types.go @@ -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 diff --git a/k8-operator/api/v1alpha1/infisicalsecret_types.go b/k8-operator/api/v1alpha1/infisicalsecret_types.go index 1af2faf20..cf22d6499 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 fa54d4130..ddc5be206 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 { +// 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 } 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 000000000..53fff8e4e --- /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 bb7b81e6b..ea6db574a 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 000000000..9d68cdc75 --- /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 000000000..b80f51fa6 --- /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 00fab4a89..f237a324b 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 ab54f1384..f1738fc47 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 000000000..8e1738eae --- /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 000000000..c5e2703c9 --- /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 af5428ee7..d288c693a 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 9e4656c55..cadbd05ce 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 5c7d268f2..0731666fa 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 c78515f74..f6b61945a 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 000000000..8d75eef69 --- /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 a0e11c0ca..4afbf6e56 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 de12b4bc5..36edfa5c1 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 b2316117b..01f835397 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 909d806e5..75f15606a 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" @@ -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") 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 599e126b5..a036675ba 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 8c610e2e5..000000000 --- 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 e3328061c..2dbc6d259 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 d01174277..f4f9348f1 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 0b78a16a6..02621dcfd 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 000000000..ad3694fcf --- /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 +} From 7ede4e2cf52a7a98079450f4a54aafad5674adc8 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sat, 7 Dec 2024 05:48:28 +0400 Subject: [PATCH 14/32] fix(k8-operator): moved template --- k8-operator/api/v1alpha1/common.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/k8-operator/api/v1alpha1/common.go b/k8-operator/api/v1alpha1/common.go index 387bc7c9e..f9197bc8b 100644 --- a/k8-operator/api/v1alpha1/common.go +++ b/k8-operator/api/v1alpha1/common.go @@ -102,4 +102,8 @@ type ManagedKubeSecretConfig struct { // +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"` } From 9b50d451ec250f534d0b599ea79da3a8065bf2f3 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sun, 8 Dec 2024 21:25:35 +0400 Subject: [PATCH 15/32] fix(k8-operator): common types support --- .../api/v1alpha1/zz_generated.deepcopy.go | 4 +- ...infisical.com_infisicaldynamicsecrets.yaml | 14 + ...ts.infisical.com_infisicalpushsecrets.yaml | 296 ++++-------------- 3 files changed, 81 insertions(+), 233 deletions(-) diff --git a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go index ddc5be206..d30811f71 100644 --- a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -261,7 +261,7 @@ func (in *InfisicalDynamicSecret) DeepCopyInto(out *InfisicalDynamicSecret) { *out = *in out.TypeMeta = in.TypeMeta in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) - out.Spec = in.Spec + in.Spec.DeepCopyInto(&out.Spec) in.Status.DeepCopyInto(&out.Status) } @@ -335,7 +335,7 @@ func (in *InfisicalDynamicSecretList) DeepCopyObject() runtime.Object { // 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 + in.ManagedSecretReference.DeepCopyInto(&out.ManagedSecretReference) out.Authentication = in.Authentication out.DynamicSecret = in.DynamicSecret out.TLS = in.TLS diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_infisicaldynamicsecrets.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_infisicaldynamicsecrets.yaml index 53fff8e4e..7595709a1 100644 --- a/k8-operator/config/crd/bases/secrets.infisical.com_infisicaldynamicsecrets.yaml +++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicaldynamicsecrets.yaml @@ -152,6 +152,20 @@ spec: description: 'The Kubernetes Secret type (experimental feature). More info: https://kubernetes.io/docs/concepts/configuration/secret/#secret-types' type: string + template: + description: The template to transform the secret data + properties: + data: + additionalProperties: + type: string + description: The template key values + type: object + includeAllSecrets: + description: This injects all retrieved secrets into the top + level of your template. Secrets defined in the template + will take precedence over the injected ones. + type: boolean + type: object required: - secretName - secretNamespace diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml index 707bfdec6..a12ec9dbe 100644 --- a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml +++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml @@ -90,7 +90,6 @@ spec: - serviceAccountRef type: object universalAuth: - description: PushSecretUniversalAuth defines universal authentication properties: credentialsRef: properties: @@ -191,239 +190,74 @@ spec: `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" 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: - description: Rest of your types should be defined similarly... - properties: - identityId: - type: string - serviceAccountRef: - properties: - name: - type: string - namespace: - type: string - required: - - name - - namespace - type: object - required: - - identityId - - serviceAccountRef - type: object - universalAuth: - description: PushSecretUniversalAuth defines universal authentication - 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 - deletionPolicy: - type: string - destination: - properties: - EnvironmentSlug: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time type: string - projectId: + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 type: string - secretsPath: + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ type: string required: - - EnvironmentSlug - - projectId - - secretsPath + - lastTransitionTime + - message + - reason + - status + - type type: object - hostAPI: - description: Infisical host to pull secrets from + type: array + managedSecrets: + additionalProperties: type: string - push: - properties: - secret: - 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: - - secret - type: object - resyncInterval: - type: string - 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 - updatePolicy: - type: string - required: - - destination - - push - - resyncInterval - type: object - status: - description: InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret - properties: - conditions: - items: - description: - "Condition contains details for one aspect of the current - state of this API Resource. --- This struct is intended for direct - use as an array at the field path .status.conditions. For example, - \n type FooStatus struct{ // Represents the observations of a - foo's current state. // Known .status.conditions.type are: \"Available\", - \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge - // +listType=map // +listMapKey=type Conditions []metav1.Condition - `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" - protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" - properties: - lastTransitionTime: - description: - lastTransitionTime is the last time the condition - transitioned from one status to another. This should be when - the underlying condition changed. If that is not known, then - using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: - message is a human readable message indicating - details about the transition. This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: - observedGeneration represents the .metadata.generation - that the condition was set based upon. For instance, if .metadata.generation - is currently 12, but the .status.conditions[x].observedGeneration - is 9, the condition is out of date with respect to the current - state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: - reason contains a programmatic identifier indicating - the reason for the condition's last transition. Producers - of specific condition types may define expected values and - meanings for this field, and whether the values are considered - a guaranteed API. The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: - type of condition in CamelCase or in foo.example.com/CamelCase. - --- Many .condition.type values are consistent across resources - like Available, but because arbitrary conditions can be useful - (see .node.status.conditions), the ability to deconflict is - important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ - type: string - required: - - lastTransitionTime - - message - - reason - - status - - type - type: object - type: array - managedSecrets: - additionalProperties: - type: string - description: - managed secrets is a map where the key is the ID, and - the value is the secret key (string[id], string[key] ) - type: object - required: - - conditions - - managedSecrets - type: object - type: object - served: true - storage: true - subresources: - status: {} + description: managed secrets is a map where the key is the ID, and + the value is the secret key (string[id], string[key] ) + type: object + required: + - conditions + - managedSecrets + type: object + type: object + served: true + storage: true + subresources: + status: {} From 3c32d8dd90f3ed3c5e67f2ae2d1e0f49c66a6cb7 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sun, 8 Dec 2024 21:27:15 +0400 Subject: [PATCH 16/32] fix(k8-operator): helm --- .../templates/infisicaldynamicsecret-crd.yaml | 240 ++++++++++++++++++ .../templates/infisicalpushsecret-crd.yaml | 1 - .../templates/manager-rbac.yaml | 26 ++ helm-charts/secrets-operator/values.yaml | 2 +- 4 files changed, 267 insertions(+), 2 deletions(-) create mode 100644 helm-charts/secrets-operator/templates/infisicaldynamicsecret-crd.yaml diff --git a/helm-charts/secrets-operator/templates/infisicaldynamicsecret-crd.yaml b/helm-charts/secrets-operator/templates/infisicaldynamicsecret-crd.yaml new file mode 100644 index 000000000..8ff822a6d --- /dev/null +++ b/helm-charts/secrets-operator/templates/infisicaldynamicsecret-crd.yaml @@ -0,0 +1,240 @@ +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + name: infisicaldynamicsecrets.secrets.infisical.com + annotations: + controller-gen.kubebuilder.io/version: v0.10.0 + labels: + {{- include "secrets-operator.labels" . | nindent 4 }} +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 + template: + description: The template to transform the secret data + properties: + data: + additionalProperties: + type: string + description: The template key values + type: object + includeAllSecrets: + description: This injects all retrieved secrets into the top + level of your template. Secrets defined in the template will + take precedence over the injected ones. + type: boolean + type: object + 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: {} +status: + acceptedNames: + kind: "" + plural: "" + conditions: [] + storedVersions: [] \ No newline at end of file diff --git a/helm-charts/secrets-operator/templates/infisicalpushsecret-crd.yaml b/helm-charts/secrets-operator/templates/infisicalpushsecret-crd.yaml index 3d3239fbb..bf0a36a9e 100644 --- a/helm-charts/secrets-operator/templates/infisicalpushsecret-crd.yaml +++ b/helm-charts/secrets-operator/templates/infisicalpushsecret-crd.yaml @@ -90,7 +90,6 @@ spec: - serviceAccountRef type: object universalAuth: - description: PushSecretUniversalAuth defines universal authentication properties: credentialsRef: properties: diff --git a/helm-charts/secrets-operator/templates/manager-rbac.yaml b/helm-charts/secrets-operator/templates/manager-rbac.yaml index 12cb11a64..3eecd9032 100644 --- a/helm-charts/secrets-operator/templates/manager-rbac.yaml +++ b/helm-charts/secrets-operator/templates/manager-rbac.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/helm-charts/secrets-operator/values.yaml b/helm-charts/secrets-operator/values.yaml index 342c4ea1b..6759b30ea 100644 --- a/helm-charts/secrets-operator/values.yaml +++ b/helm-charts/secrets-operator/values.yaml @@ -32,7 +32,7 @@ controllerManager: - ALL image: repository: infisical/kubernetes-operator - tag: v0.7.6 + tag: v0.7.7 resources: limits: cpu: 500m From ee54d460a014ec82a8a94ac1a53630b58582a69d Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sun, 8 Dec 2024 21:28:04 +0400 Subject: [PATCH 17/32] fix(k8-operator): update charts --- helm-charts/secrets-operator/Chart.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/helm-charts/secrets-operator/Chart.yaml b/helm-charts/secrets-operator/Chart.yaml index ca6747984..fc76e455f 100644 --- a/helm-charts/secrets-operator/Chart.yaml +++ b/helm-charts/secrets-operator/Chart.yaml @@ -13,9 +13,9 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: v0.7.6 +version: v0.7.7 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "v0.7.6" +appVersion: "v0.7.7" From 36af975594ad57a0ac993d59084d07a777525835 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sun, 8 Dec 2024 22:42:29 +0400 Subject: [PATCH 18/32] docs(k8-operator): k8's dynamic secret docs --- docs/integrations/platforms/kubernetes.mdx | 484 +++++++++++++++++++-- 1 file changed, 448 insertions(+), 36 deletions(-) diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx index 0a61f4682..2c699c197 100644 --- a/docs/integrations/platforms/kubernetes.mdx +++ b/docs/integrations/platforms/kubernetes.mdx @@ -58,6 +58,16 @@ Once you apply the manifest, the operator will be installed in `infisical-operat + +## Custom Resource Definitions (CRD's) + +Currently the operator supports the following CRD's. We are constantly expanding the functionality of the operator, and this list will be updated as new CRD's are added. + +1. [InfisicalSecret](#sync-infisical-secrets-to-your-cluster): Sync secrets from Infisical to a Kubernetes secret. +2. [InfisicalPushSecret](#push-secrets-to-infisical): Push secrets from a Kubernetes secret to Infisical. +3. [InfisicalDynamicSecret](#sync-dynamic-secrets-to-your-cluster): Sync dynamic secrets and create leases automatically in Kubernetes. + + ## Sync Infisical Secrets to your cluster Once you have installed the operator to your cluster, you'll need to create a `InfisicalSecret` custom resource definition (CRD). @@ -946,25 +956,6 @@ spec: -### Connecting to instances with private/self-signed certificate - -To connect to Infisical instances behind a private/self-signed certificate, you can configure the TLS settings in the `InfisicalSecret` CRD -to point to a CA certificate stored in a Kubernetes secret resource. - -```yaml ---- -spec: - hostAPI: https://app.infisical.com/api - resyncInterval: 10 - tls: - caRef: - secretName: custom-ca-certificate - secretNamespace: default - key: ca.crt - authentication: ---- -``` - The definition file of the Kubernetes secret for the CA certificate can be structured like the following: ```yaml @@ -1042,18 +1033,6 @@ After filling out the fields in the InfisicalPushSecret CRD, you can apply it di Before applying the InfisicalPushSecret CRD, you need to create a Kubernetes secret containing the secrets you want to push to Infisical. An example can be seen below the InfisicalPushSecret CRD. -```bash - kubectl apply -f source-secret.yaml -``` - -After applying the soruce-secret.yaml file, you are ready to apply the InfisicalPushSecret CRD. - -```bash - kubectl apply -f infisical-push-secret.yaml -``` - -After applying the InfisicalPushSecret CRD, you should notice that the secrets you have defined in your source-secret.yaml file have been pushed to your specified destination in Infisical. - ```yaml infisical-push-secret.yaml apiVersion: secrets.infisical.com/v1alpha1 kind: InfisicalPushSecret @@ -1114,6 +1093,19 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab ``` +```bash + kubectl apply -f source-secret.yaml +``` + +After applying the soruce-secret.yaml file, you are ready to apply the InfisicalPushSecret CRD. + +```bash + kubectl apply -f infisical-push-secret.yaml +``` + +After applying the InfisicalPushSecret CRD, you should notice that the secrets you have defined in your source-secret.yaml file have been pushed to your specified destination in Infisical. + + ### InfisicalPushSecret CRD properties @@ -1424,22 +1416,442 @@ After applying, you should notice that the secrets have been pushed to Infisical kubectl apply -f example-infisical-push-secret-crd.yaml # The InfisicalPushSecret CRD itself ``` -### Connecting to instances with private/self-signed certificate +## Sync Dynamic Secrets to your cluster -To connect to Infisical instances behind a private/self-signed certificate, you can configure the TLS settings in the `InfisicalPushSecret` CRD +### Example usage + +The example below demonstrates a sample InfisicalDynamicSecret CRD that creates a dynamic secret lease in Infisical, and syncs the lease to your Kubernetes cluster. + +```yaml dynamic-secret-crd.yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalDynamicSecret +metadata: + name: infisicaldynamicsecret +spec: + hostAPI: https://app.infisical.com/api # Optional, defaults to https://app.infisical.com/api + + dynamicSecret: + secretName: + projectId: + secretsPath: # Root directory is / + environmentSlug: + + # Lease revocation policy defines what should happen to leases created by the operator if the CRD is deleted. + # If set to "Revoke", leases will be revoked when the InfisicalDynamicSecret CRD is deleted. + leaseRevocationPolicy: Revoke + + # Lease TTL defines how long the lease should last for the dynamic secret. + # This value must be less than 1 day, and if a max TTL is defined on the dynamic secret, it must be below the max TTL. + leaseTTL: 1m + + # A reference to the secret that the dynamic secret lease should be stored in. + # If the secret doesn't exist, it will automatically be created. + managedSecretReference: + secretName: + secretNamespace: default # Must be the same namespace as the InfisicalDynamicSecret CRD. + creationPolicy: Orphan + + # Only have one authentication method defined or you are likely to run into authentication issues. + # Remove all except one authentication method. + authentication: + awsIamAuth: + identityId: + azureAuth: + identityId: + gcpIamAuth: + identityId: + serviceAccountKeyFilePath: + gcpIdTokenAuth: + identityId: + kubernetesAuth: + identityId: + serviceAccountRef: + name: + namespace: + universalAuth: + credentialsRef: + secretName: # universal-auth-credentials + secretNamespace: # default +``` + +Apply the InfisicalDynamicSecret CRD to your cluster. +```bash +kubectl apply -f dynamic-secret-crd.yaml +``` + +After applying the InfisicalDynamicSecret CRD, you should notice that the dynamic secret lease has been created in Infisical and synced to your Kubernetes cluster. You can verify that the lease has been created by doing: +```bash +kubectl get secret -o yaml +``` + +After getting the secret, you should should see that the secret has data that contains the lease credentials. +```yaml +apiVersion: v1 +data: + DB_PASSWORD: VHhETjZ4c2xsTXpOSWdPYW5LLlRyNEc2alVKYml6WiQjQS0tNTdodyREM3ZLZWtYSi4hTkdyS0F+TVFsLU9CSA== + DB_USERNAME: cHg4Z0dJTUVBcHdtTW1aYnV3ZWRsekJRRll6cW4wFEE= +kind: Secret +# ..... +``` + +### InfisicalDynamicSecret CRD properties + + + If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to + ` https://your-self-hosted-instace.com/api` + + When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud. + + + If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet. + To achieve this, use the following address for the hostAPI field: + + ``` bash + http://..svc.cluster.local:4000/api + ``` + + Make sure to replace `` and `` with the appropriate values for your backend service and namespace. + + + + + + The `leaseTTL` is a string-formatted duration that defines the time the lease should last for the dynamic secret. + + The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time. + + The following units are supported: + - `s` for seconds (must be at least 5 seconds) + - `m` for minutes + - `h` for hours + - `d` for days + + + The lease duration at most be 1 day (24 hours). And the TTL must be less than the max TTL defined on the dynamic secret. + + + + + The `managedSecretReference` field is used to define the Kubernetes secret where the dynamic secret lease should be stored. The required fields are `secretName` and `secretNamespace`. + + ```yaml + spec: + managedSecretReference: + secretName: + secretNamespace: default + ``` + + + The name of the Kubernetes secret where the dynamic secret lease should be stored. + + + + The namespace of the Kubernetes secret where the dynamic secret lease should be stored. + + + + Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. + This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. + + #### Available options + - `Orphan` (default) + - `Owner` + + + When creation policy is set to `Owner`, the `InfisicalSecret` CRD must be in + the same namespace as where the managed kubernetes secret. + + + This field is optional. + + + + Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets. + + This field is optional. + + + + + + + The field is optional and will default to `None` if not defined. + + The lease revocation policy defines what the operator should do with the leases created by the operator, when the InfisicalDynamicSecret CRD is deleted. + + Valid values are `None` and `Revoke`. + + Behavior of each policy: + - `None`: The operator will not override existing secrets in Infisical. If a secret with the same key already exists, the operator will skip pushing that secret, and the secret will not be managed by the operator. + - `Revoke`: The operator will revoke the leases created by the operator when the InfisicalDynamicSecret CRD is deleted. + + ```yaml + spec: + leaseRevocationPolicy: Revoke + ``` + + + + The `dynamicSecret` field is used to specify which dynamic secret to create leases for. The required fields are `secretName`, `projectId`, `secretsPath`, and `environmentSlug`. + + ```yaml + spec: + dynamicSecret: + secretName: + projectId: + environmentSlug: + secretsPath: + ``` + + + The name of the dynamic secret. + + + + The project ID of where the dynamic secret is stored in Infisical. + + + + The environment slug of where the dynamic secret is stored in Infisical. + + + + The path of where the dynamic secret is stored in Infisical. The root path is `/`. + + + + + + + The `authentication` field dictates which authentication method to use when pushing secrets to Infisical. + The available authentication methods are `universalAuth`, `kubernetesAuth`, `awsIamAuth`, `azureAuth`, `gcpIdTokenAuth`, and `gcpIamAuth`. + + + + The universal authentication method is one of the easiest ways to get started with Infisical. Universal Auth works anywhere and is not tied to any specific cloud provider. + [Read more about Universal Auth](/documentation/platform/identities/universal-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `credentialsRef`: The name and namespace of the Kubernetes secret that stores the service token. + - `credentialsRef.secretName`: The name of the Kubernetes secret. + - `credentialsRef.secretNamespace`: The namespace of the Kubernetes secret. + + Example: + + ```yaml + # infisical-push-secret.yaml + spec: + universalAuth: + credentialsRef: + secretName: + secretNamespace: + ``` + + ```yaml + # machine-identity-credentials.yaml + apiVersion: v1 + kind: Secret + metadata: + name: universal-auth-credentials + type: Opaque + stringData: + clientId: + clientSecret: + ``` + + + + The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment. + [Read more about Kubernetes Auth](/documentation/platform/identities/kubernetes-auth). + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `serviceAccountRef`: The name and namespace of the service account that will be used to authenticate with Infisical. + - `serviceAccountRef.name`: The name of the service account. + - `serviceAccountRef.namespace`: The namespace of the service account. + + Example: + + ```yaml + spec: + kubernetesAuth: + identityId: + serviceAccountRef: + name: + namespace: + ``` + + + + The AWS IAM machine identity authentication method is used to authenticate with Infisical. + [Read more about AWS IAM Auth](/documentation/platform/identities/aws-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + + Example: + + ```yaml + spec: + authentication: + awsIamAuth: + identityId: + ``` + + + + The AWS IAM machine identity authentication method is used to authenticate with Infisical. Azure Auth can only be used from within an Azure environment. + [Read more about Azure Auth](/documentation/platform/identities/azure-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + + Example: + + ```yaml + spec: + authentication: + azureAuth: + identityId: + ``` + + + The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments. + [Read more about Azure Auth](/documentation/platform/identities/gcp-auth). + + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `serviceAccountKeyFilePath`: The path to the GCP service account key file. + + Example: + + ```yaml + spec: + gcpIamAuth: + identityId: + serviceAccountKeyFilePath: + ``` + + + The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments. + [Read more about Azure Auth](/documentation/platform/identities/gcp-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + + Example: + + ```yaml + spec: + gcpIdTokenAuth: + identityId: + ``` + + + + + + + This block defines the TLS settings to use for connecting to the Infisical + instance. + + Fields: + + This block defines the reference to the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + + Valid fields: + - `secretName`: The name of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + - `secretNamespace`: The namespace of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + - `key`: The name of the key in the Kubernetes secret which contains the value of the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + + Example: + + ```yaml + tls: + caRef: + secretName: custom-ca-certificate + secretNamespace: default + key: ca.crt + ``` + + + + + +### Applying the InfisicalDynamicSecret CRD to your cluster + +Once you have configured the `InfisicalDynamicSecret` CRD with the required fields, you can apply it to your cluster. After applying, you should notice that a lease has been created in Infisical and synced to your Kubernetes cluster. + +```bash +kubectl apply -f dynamic-secret-crd.yaml +``` + +### Auto redeployment + +Deployments using managed secrets don't reload automatically on updates, so they may use outdated secrets unless manually redeployed. +To address this, we added functionality to automatically redeploy your deployment when its managed secret updates. + +#### Enabling auto redeploy + +To enable auto redeployment you simply have to add the following annotation to the deployment that consumes a managed secret + +```yaml +secrets.infisical.com/auto-reload: "true" +``` + + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx + annotations: + secrets.infisical.com/auto-reload: "true" # <- redeployment annotation +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + envFrom: + - secretRef: + name: managed-secret # The name of your managed secret, the same that you're using in your InfisicalDynamicSecret CRD (spec.managedSecretReference.secretName) + ports: + - containerPort: 80 +``` + + + #### How it works + When the lease changes, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update. + Then, for each deployment that has this annotation present, a rolling update will be triggered. A redeployment won't happen if the lease is renewed, only if it's recreated. + + + +## Connecting to instances with private/self-signed certificate + +To connect to Infisical instances behind a private/self-signed certificate, you can configure the TLS settings in the CRD to point to a CA certificate stored in a Kubernetes secret resource. ```yaml +--- spec: hostAPI: https://app.infisical.com/api - resyncInterval: 30s tls: caRef: secretName: custom-ca-certificate secretNamespace: default key: ca.crt - authentication: - # ... +--- ``` From 925a594a1bde716e788cf64e7f513695ab1432c2 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sun, 8 Dec 2024 23:24:30 +0400 Subject: [PATCH 19/32] feat(k8-operator): dynamic secrets status conditions logging --- .../v1alpha1/infisicaldynamicsecret_types.go | 6 +- .../api/v1alpha1/zz_generated.deepcopy.go | 7 + ...infisical.com_infisicaldynamicsecrets.yaml | 70 +++++++ .../infisicaldynamicsecret/conditions.go | 173 ++++++++++++++++++ .../infisicaldynamicsecret_controller.go | 18 +- .../infisicaldynamicsecret_helper.go | 11 +- 6 files changed, 275 insertions(+), 10 deletions(-) create mode 100644 k8-operator/controllers/infisicaldynamicsecret/conditions.go diff --git a/k8-operator/api/v1alpha1/infisicaldynamicsecret_types.go b/k8-operator/api/v1alpha1/infisicaldynamicsecret_types.go index 142cb671f..a55e215a3 100644 --- a/k8-operator/api/v1alpha1/infisicaldynamicsecret_types.go +++ b/k8-operator/api/v1alpha1/infisicaldynamicsecret_types.go @@ -65,10 +65,10 @@ type InfisicalDynamicSecretSpec struct { // InfisicalDynamicSecretStatus defines the observed state of InfisicalDynamicSecret. type InfisicalDynamicSecretStatus struct { - Lease *InfisicalDynamicSecretLease `json:"lease,omitempty"` - - DynamicSecretID string `json:"dynamicSecretId,omitempty"` + Conditions []metav1.Condition `json:"conditions"` + 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"` } diff --git a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go index d30811f71..bad990bc4 100644 --- a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -354,6 +354,13 @@ func (in *InfisicalDynamicSecretSpec) DeepCopy() *InfisicalDynamicSecretSpec { // 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.Conditions != nil { + in, out := &in.Conditions, &out.Conditions + *out = make([]v1.Condition, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } if in.Lease != nil { in, out := &in.Lease, &out.Lease *out = new(InfisicalDynamicSecretLease) diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_infisicaldynamicsecrets.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_infisicaldynamicsecrets.yaml index 7595709a1..c1cb7255d 100644 --- a/k8-operator/config/crd/bases/secrets.infisical.com_infisicaldynamicsecrets.yaml +++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicaldynamicsecrets.yaml @@ -203,6 +203,74 @@ spec: description: InfisicalDynamicSecretStatus defines the observed state of InfisicalDynamicSecret. properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array dynamicSecretId: type: string lease: @@ -228,6 +296,8 @@ spec: description: The MaxTTL can be null, if it's null, there's no max TTL and we should never have to renew. type: string + required: + - conditions type: object type: object served: true diff --git a/k8-operator/controllers/infisicaldynamicsecret/conditions.go b/k8-operator/controllers/infisicaldynamicsecret/conditions.go new file mode 100644 index 000000000..9620167e1 --- /dev/null +++ b/k8-operator/controllers/infisicaldynamicsecret/conditions.go @@ -0,0 +1,173 @@ +package controllers + +import ( + "context" + "fmt" + + "github.com/Infisical/infisical/k8-operator/api/v1alpha1" + "github.com/go-logr/logr" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (r *InfisicalDynamicSecretReconciler) SetReconcileAutoRedeploymentStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, numDeployments int, errorToConditionOn error) { + if infisicalDynamicSecret.Status.Conditions == nil { + infisicalDynamicSecret.Status.Conditions = []metav1.Condition{} + } + + if errorToConditionOn == nil { + meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/AutoRedeployReady", + Status: metav1.ConditionTrue, + Reason: "OK", + Message: fmt.Sprintf("Infisical has found %v deployments which are ready to be auto redeployed when dynamic secret lease changes", numDeployments), + }) + } else { + meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/AutoRedeployReady", + Status: metav1.ConditionFalse, + Reason: "Error", + Message: fmt.Sprintf("Failed reconcile deployments because: %v", errorToConditionOn), + }) + } + + err := r.Client.Status().Update(ctx, infisicalDynamicSecret) + if err != nil { + logger.Error(err, "Could not set condition for AutoRedeployReady") + } +} + +func (r *InfisicalDynamicSecretReconciler) SetAuthenticatedStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) { + if infisicalDynamicSecret.Status.Conditions == nil { + infisicalDynamicSecret.Status.Conditions = []metav1.Condition{} + } + + if errorToConditionOn == nil { + meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/Authenticated", + Status: metav1.ConditionTrue, + Reason: "OK", + Message: "Infisical has successfully authenticated with the Infisical API", + }) + } else { + meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/Authenticated", + Status: metav1.ConditionFalse, + Reason: "Error", + Message: fmt.Sprintf("Failed to authenticate with Infisical API because: %v", errorToConditionOn), + }) + } + + err := r.Client.Status().Update(ctx, infisicalDynamicSecret) + if err != nil { + logger.Error(err, "Could not set condition for Authenticated") + } +} + +func (r *InfisicalDynamicSecretReconciler) SetLeaseRenewalStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) { + if infisicalDynamicSecret.Status.Conditions == nil { + infisicalDynamicSecret.Status.Conditions = []metav1.Condition{} + } + + if errorToConditionOn == nil { + meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/LeaseRenewal", + Status: metav1.ConditionTrue, + Reason: "OK", + Message: "Infisical has successfully renewed the lease", + }) + } else { + meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/LeaseRenewal", + Status: metav1.ConditionFalse, + Reason: "Error", + Message: fmt.Sprintf("Failed to renew the lease because: %v", errorToConditionOn), + }) + } + + err := r.Client.Status().Update(ctx, infisicalDynamicSecret) + if err != nil { + logger.Error(err, "Could not set condition for LeaseRenewal") + } +} + +func (r *InfisicalDynamicSecretReconciler) SetCreatedLeaseStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) { + if infisicalDynamicSecret.Status.Conditions == nil { + infisicalDynamicSecret.Status.Conditions = []metav1.Condition{} + } + + if errorToConditionOn == nil { + meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/LeaseCreated", + Status: metav1.ConditionTrue, + Reason: "OK", + Message: "Infisical has successfully created the lease", + }) + } else { + meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/LeaseCreated", + Status: metav1.ConditionFalse, + Reason: "Error", + Message: fmt.Sprintf("Failed to create the lease because: %v", errorToConditionOn), + }) + } + + err := r.Client.Status().Update(ctx, infisicalDynamicSecret) + if err != nil { + logger.Error(err, "Could not set condition for LeaseCreated") + } +} + +func (r *InfisicalDynamicSecretReconciler) SetRevokedLeaseStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) { + if infisicalDynamicSecret.Status.Conditions == nil { + infisicalDynamicSecret.Status.Conditions = []metav1.Condition{} + } + + if errorToConditionOn == nil { + meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/LeaseRevoked", + Status: metav1.ConditionTrue, + Reason: "OK", + Message: "Infisical has successfully revoked the lease", + }) + } else { + meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/LeaseRevoked", + Status: metav1.ConditionFalse, + Reason: "Error", + Message: fmt.Sprintf("Failed to revoke the lease because: %v", errorToConditionOn), + }) + } + + err := r.Client.Status().Update(ctx, infisicalDynamicSecret) + if err != nil { + logger.Error(err, "Could not set condition for LeaseRevoked") + } +} + +func (r *InfisicalDynamicSecretReconciler) SetReconcileStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) { + if infisicalDynamicSecret.Status.Conditions == nil { + infisicalDynamicSecret.Status.Conditions = []metav1.Condition{} + } + + if errorToConditionOn == nil { + meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/Reconcile", + Status: metav1.ConditionTrue, + Reason: "OK", + Message: "Infisical has successfully reconciled the InfisicalDynamicSecret", + }) + } else { + meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{ + Type: "secrets.infisical.com/Reconcile", + Status: metav1.ConditionFalse, + Reason: "Error", + Message: fmt.Sprintf("Failed to reconcile the InfisicalDynamicSecret because: %v", errorToConditionOn), + }) + } + + err := r.Client.Status().Update(ctx, infisicalDynamicSecret) + if err != nil { + logger.Error(err, "Could not set condition for Reconcile") + } +} diff --git a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go index 8e1738eae..9be907e96 100644 --- a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go +++ b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go @@ -30,16 +30,20 @@ type InfisicalDynamicSecretReconciler struct { 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) } +// +kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicaldynamicsecrets,verbs=get;list;watch;create;update;patch;delete +// +kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicaldynamicsecrets/status,verbs=get;update;patch +// +kubebuilder:rbac:groups=secrets.infisical.com,resources=infisicaldynamicsecrets/finalizers,verbs=update +// +kubebuilder:rbac:groups="",resources=secrets,verbs=get;list;watch;create;update;delete +// +kubebuilder:rbac:groups="",resources=configmaps,verbs=get;list;watch;create;update;delete +// +kubebuilder:rbac:groups=apps,resources=deployments,verbs=list;watch;get;update +// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=get;list;watch + func (r *InfisicalDynamicSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { logger := r.GetLogger(req) @@ -82,6 +86,7 @@ func (r *InfisicalDynamicSecretReconciler) Reconcile(ctx context.Context, req ct } err := r.HandleLeaseRevocation(ctx, logger, infisicalDynamicSecretCRD) + r.SetRevokedLeaseStatus(ctx, logger, &infisicalDynamicSecretCRD, err) if infisicalDynamicSecretsResourceVariablesMap != nil { if rv, ok := infisicalDynamicSecretsResourceVariablesMap[string(infisicalDynamicSecretCRD.GetUID())]; ok { @@ -128,7 +133,7 @@ func (r *InfisicalDynamicSecretReconciler) Reconcile(ctx context.Context, req ct } nextReconcile, err := r.ReconcileInfisicalDynamicSecret(ctx, logger, infisicalDynamicSecretCRD) - // r.SetSuccessfullyReconciledConditions(ctx, &infisicalDynamicSecretCRD, err) + r.SetReconcileStatus(ctx, logger, &infisicalDynamicSecretCRD, err) if err == nil && nextReconcile.Seconds() >= 5 { requeueTime = nextReconcile @@ -141,7 +146,8 @@ func (r *InfisicalDynamicSecretReconciler) Reconcile(ctx context.Context, req ct }, nil } - _, err = controllerhelpers.ReconcileDeploymentsWithManagedSecrets(ctx, r.Client, logger, infisicalDynamicSecretCRD.Spec.ManagedSecretReference) + numDeployments, err := controllerhelpers.ReconcileDeploymentsWithManagedSecrets(ctx, r.Client, logger, infisicalDynamicSecretCRD.Spec.ManagedSecretReference) + r.SetReconcileAutoRedeploymentStatus(ctx, logger, &infisicalDynamicSecretCRD, 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/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go index c5e2703c9..f049d6883 100644 --- a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go +++ b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go @@ -270,6 +270,7 @@ func (r *InfisicalDynamicSecretReconciler) HandleLeaseRevocation(ctx context.Con logger.Info("Authenticating for lease revocation") authDetails, err := r.handleAuthentication(ctx, infisicalDynamicSecret, infisicalClient) + r.SetAuthenticatedStatus(ctx, logger, &infisicalDynamicSecret, err) if err != nil { return fmt.Errorf("unable to authenticate for lease revocation [err=%s]", err) @@ -334,6 +335,7 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c if authDetails.AuthStrategy == "" { logger.Info("No authentication strategy found. Attempting to authenticate") authDetails, err = r.handleAuthentication(ctx, infisicalDynamicSecret, infisicalClient) + r.SetAuthenticatedStatus(ctx, logger, &infisicalDynamicSecret, err) if err != nil { return nextReconcile, fmt.Errorf("unable to authenticate [err=%s]", err) @@ -368,7 +370,10 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c } if infisicalDynamicSecret.Status.Lease == nil { - r.CreateDynamicSecretLease(ctx, logger, infisicalClient, &infisicalDynamicSecret, destination) + err := r.CreateDynamicSecretLease(ctx, logger, infisicalClient, &infisicalDynamicSecret, destination) + r.SetCreatedLeaseStatus(ctx, logger, &infisicalDynamicSecret, err) + + return defaultNextReconcile, err // Short requeue after creation } else { now := time.Now() leaseExpiresAt := infisicalDynamicSecret.Status.Lease.ExpiresAt.Time @@ -403,6 +408,7 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c maxTTLThreshold)) err := r.CreateDynamicSecretLease(ctx, logger, infisicalClient, &infisicalDynamicSecret, destination) + r.SetCreatedLeaseStatus(ctx, logger, &infisicalDynamicSecret, err) return defaultNextReconcile, err // Short requeue after creation } } @@ -411,6 +417,7 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c if now.After(leaseExpiresAt) { logger.Info("Lease has expired, creating new lease...") err = r.CreateDynamicSecretLease(ctx, logger, infisicalClient, &infisicalDynamicSecret, destination) + r.SetCreatedLeaseStatus(ctx, logger, &infisicalDynamicSecret, err) return defaultNextReconcile, err // Short requeue after creation } @@ -421,10 +428,12 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c renewalThreshold)) err = r.RenewDynamicSecretLease(ctx, logger, infisicalClient, &infisicalDynamicSecret, destination) + r.SetLeaseRenewalStatus(ctx, logger, &infisicalDynamicSecret, err) if err == constants.ErrInvalidLease { logger.Info("Failed to renew expired lease, creating new lease...") err = r.CreateDynamicSecretLease(ctx, logger, infisicalClient, &infisicalDynamicSecret, destination) + r.SetCreatedLeaseStatus(ctx, logger, &infisicalDynamicSecret, err) } return defaultNextReconcile, err // Short requeue after renewal/creation From 74df37499880ec4c89c34ce4cc20fed913c821ce Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Sun, 8 Dec 2024 23:24:38 +0400 Subject: [PATCH 20/32] Update infisicaldynamicsecret-crd.yaml --- .../templates/infisicaldynamicsecret-crd.yaml | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/helm-charts/secrets-operator/templates/infisicaldynamicsecret-crd.yaml b/helm-charts/secrets-operator/templates/infisicaldynamicsecret-crd.yaml index 8ff822a6d..2a65896ae 100644 --- a/helm-charts/secrets-operator/templates/infisicaldynamicsecret-crd.yaml +++ b/helm-charts/secrets-operator/templates/infisicaldynamicsecret-crd.yaml @@ -201,6 +201,74 @@ spec: description: InfisicalDynamicSecretStatus defines the observed state of InfisicalDynamicSecret. properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a foo's + current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating details + about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers of + specific condition types may define expected values and meanings + for this field, and whether the values are considered a guaranteed + API. The value should be a CamelCase string. This field may + not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array dynamicSecretId: type: string lease: @@ -226,6 +294,8 @@ spec: description: The MaxTTL can be null, if it's null, there's no max TTL and we should never have to renew. type: string + required: + - conditions type: object type: object served: true From a757ea22a12805bac8ac1242471ecaadff4e2109 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 9 Dec 2024 00:22:22 +0400 Subject: [PATCH 21/32] fix(k8-operator): improvements for dynamic secrets --- .../infisicaldynamicsecret/dynamicSecret.yaml | 27 ++++++++++++ .../infisicalsecret/infisicalSecretCrd.yaml} | 0 .../infisicaldynamicsecret_controller.go | 2 +- .../infisicaldynamicsecret_helper.go | 44 +++++++++---------- 4 files changed, 50 insertions(+), 23 deletions(-) create mode 100644 k8-operator/config/samples/crd/infisicaldynamicsecret/dynamicSecret.yaml rename k8-operator/config/samples/{sample.yaml => crd/infisicalsecret/infisicalSecretCrd.yaml} (100%) diff --git a/k8-operator/config/samples/crd/infisicaldynamicsecret/dynamicSecret.yaml b/k8-operator/config/samples/crd/infisicaldynamicsecret/dynamicSecret.yaml new file mode 100644 index 000000000..b34259927 --- /dev/null +++ b/k8-operator/config/samples/crd/infisicaldynamicsecret/dynamicSecret.yaml @@ -0,0 +1,27 @@ +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalDynamicSecret +metadata: + name: infisicaldynamicsecret-demo +spec: + hostAPI: https://app.infisical.com/api + + dynamicSecret: + secretName: + projectId: + secretsPath: + environmentSlug: + + leaseRevocationPolicy: Revoke # Revoke or None. Revoke will revoke leases created by the operator if the CRD is deleted. + leaseTTL: 1m # TTL for the leases created. Must be below 24 hours. + + # Reference to the secret that you want to store the lease credentials in. If a secret with the name specified name does not exist, it will automatically be created. + managedSecretReference: + secretName: lease + secretNamespace: default + creationPolicy: Orphan # Orphan or Owner + + authentication: + universalAuth: + credentialsRef: + secretName: universal-auth-credentials # universal-auth-credentials + secretNamespace: default # default diff --git a/k8-operator/config/samples/sample.yaml b/k8-operator/config/samples/crd/infisicalsecret/infisicalSecretCrd.yaml similarity index 100% rename from k8-operator/config/samples/sample.yaml rename to k8-operator/config/samples/crd/infisicalsecret/infisicalSecretCrd.yaml diff --git a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go index 9be907e96..468a0e799 100644 --- a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go +++ b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go @@ -132,7 +132,7 @@ func (r *InfisicalDynamicSecretReconciler) Reconcile(ctx context.Context, req ct api.API_CA_CERTIFICATE = "" } - nextReconcile, err := r.ReconcileInfisicalDynamicSecret(ctx, logger, infisicalDynamicSecretCRD) + nextReconcile, err := r.ReconcileInfisicalDynamicSecret(ctx, logger, &infisicalDynamicSecretCRD) r.SetReconcileStatus(ctx, logger, &infisicalDynamicSecretCRD, err) if err == nil && nextReconcile.Seconds() >= 5 { diff --git a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go index f049d6883..c5d7a5a06 100644 --- a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go +++ b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go @@ -207,10 +207,6 @@ func (r *InfisicalDynamicSecretReconciler) CreateDynamicSecretLease(ctx context. 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 } @@ -320,9 +316,9 @@ func (r *InfisicalDynamicSecretReconciler) HandleLeaseRevocation(ctx context.Con return nil } -func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx context.Context, logger logr.Logger, infisicalDynamicSecret v1alpha1.InfisicalDynamicSecret) (time.Duration, error) { +func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret) (time.Duration, error) { - resourceVariables := r.getResourceVariables(infisicalDynamicSecret) + resourceVariables := r.getResourceVariables(*infisicalDynamicSecret) infisicalClient := resourceVariables.InfisicalClient cancelCtx := resourceVariables.CancelCtx authDetails := resourceVariables.AuthDetails @@ -334,14 +330,14 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c if authDetails.AuthStrategy == "" { logger.Info("No authentication strategy found. Attempting to authenticate") - authDetails, err = r.handleAuthentication(ctx, infisicalDynamicSecret, infisicalClient) - r.SetAuthenticatedStatus(ctx, logger, &infisicalDynamicSecret, err) + authDetails, err = r.handleAuthentication(ctx, *infisicalDynamicSecret, infisicalClient) + r.SetAuthenticatedStatus(ctx, logger, infisicalDynamicSecret, err) if err != nil { return nextReconcile, fmt.Errorf("unable to authenticate [err=%s]", err) } - r.updateResourceVariables(infisicalDynamicSecret, util.ResourceVariables{ + r.updateResourceVariables(*infisicalDynamicSecret, util.ResourceVariables{ InfisicalClient: infisicalClient, CancelCtx: cancelCtx, AuthDetails: authDetails, @@ -358,7 +354,7 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c if infisicalDynamicSecret.Status.Lease != nil { annotationValue = fmt.Sprintf("%s-%d", infisicalDynamicSecret.Status.Lease.ID, infisicalDynamicSecret.Status.Lease.Version) } - r.createInfisicalManagedKubeSecret(ctx, logger, infisicalDynamicSecret, annotationValue) + r.createInfisicalManagedKubeSecret(ctx, logger, *infisicalDynamicSecret, annotationValue) } if err != nil { @@ -370,8 +366,8 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c } if infisicalDynamicSecret.Status.Lease == nil { - err := r.CreateDynamicSecretLease(ctx, logger, infisicalClient, &infisicalDynamicSecret, destination) - r.SetCreatedLeaseStatus(ctx, logger, &infisicalDynamicSecret, err) + err := r.CreateDynamicSecretLease(ctx, logger, infisicalClient, infisicalDynamicSecret, destination) + r.SetCreatedLeaseStatus(ctx, logger, infisicalDynamicSecret, err) return defaultNextReconcile, err // Short requeue after creation } else { @@ -381,8 +377,8 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c // 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 + // 30% of the original duration (if the TTL has 50% or less of its time left, renew) + renewalThreshold := originalLeaseDuration * 50 / 100 timeUntilExpiration := time.Until(leaseExpiresAt) nextReconcile = timeUntilExpiration / 2 @@ -407,8 +403,8 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c timeUntilMaxTTL, maxTTLThreshold)) - err := r.CreateDynamicSecretLease(ctx, logger, infisicalClient, &infisicalDynamicSecret, destination) - r.SetCreatedLeaseStatus(ctx, logger, &infisicalDynamicSecret, err) + err := r.CreateDynamicSecretLease(ctx, logger, infisicalClient, infisicalDynamicSecret, destination) + r.SetCreatedLeaseStatus(ctx, logger, infisicalDynamicSecret, err) return defaultNextReconcile, err // Short requeue after creation } } @@ -416,8 +412,8 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c // 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) - r.SetCreatedLeaseStatus(ctx, logger, &infisicalDynamicSecret, err) + err = r.CreateDynamicSecretLease(ctx, logger, infisicalClient, infisicalDynamicSecret, destination) + r.SetCreatedLeaseStatus(ctx, logger, infisicalDynamicSecret, err) return defaultNextReconcile, err // Short requeue after creation } @@ -427,13 +423,13 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c timeUntilExpiration, renewalThreshold)) - err = r.RenewDynamicSecretLease(ctx, logger, infisicalClient, &infisicalDynamicSecret, destination) - r.SetLeaseRenewalStatus(ctx, logger, &infisicalDynamicSecret, err) + err = r.RenewDynamicSecretLease(ctx, logger, infisicalClient, infisicalDynamicSecret, destination) + r.SetLeaseRenewalStatus(ctx, logger, infisicalDynamicSecret, err) if err == constants.ErrInvalidLease { logger.Info("Failed to renew expired lease, creating new lease...") - err = r.CreateDynamicSecretLease(ctx, logger, infisicalClient, &infisicalDynamicSecret, destination) - r.SetCreatedLeaseStatus(ctx, logger, &infisicalDynamicSecret, err) + err = r.CreateDynamicSecretLease(ctx, logger, infisicalClient, infisicalDynamicSecret, destination) + r.SetCreatedLeaseStatus(ctx, logger, infisicalDynamicSecret, err) } return defaultNextReconcile, err // Short requeue after renewal/creation @@ -451,5 +447,9 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c nextReconcile = max(5*time.Second, min(nextReconcile, 5*time.Minute)) } + if err := r.Client.Status().Update(ctx, infisicalDynamicSecret); err != nil { + return nextReconcile, fmt.Errorf("unable to update InfisicalDynamicSecret status [err=%s]", err) + } + return nextReconcile, nil } From 0edf0dac98fedc7edadaca578fb3d2a6f6cea2db Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Tue, 10 Dec 2024 04:31:55 +0400 Subject: [PATCH 22/32] fix(k8-operator): PushSecret CRd causing endless snapshot updates --- .../infisicalpushsecret_helper.go | 57 +++++++++++-------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go index ebe125a49..9ba514dbc 100644 --- a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go +++ b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go @@ -148,15 +148,6 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context IncludeImports: false, }) - existingSecretsContainsKey := func(key string) bool { - for _, secret := range existingSecrets { - if secret.SecretKey == key { - return true - } - } - return false - } - getExistingSecretByKey := func(key string) *infisicalSdk.Secret { for _, secret := range existingSecrets { if secret.SecretKey == key { @@ -175,6 +166,15 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context return nil } + updateExistingSecretByKey := func(key string, newSecretValue string) { + for i := range existingSecrets { + if existingSecrets[i].SecretKey == key { + existingSecrets[i].SecretValue = newSecretValue + break + } + } + } + if err != nil { return fmt.Errorf("unable to list secrets [err=%s]", err) } @@ -192,7 +192,8 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context infisicalPushSecret.Status.ManagedSecrets = make(map[string]string) // (string[id], string[key] ) for secretKey, secretValue := range kubeSecrets { - if existingSecretsContainsKey(secretKey) { + if exists := getExistingSecretByKey(secretKey); exists != nil { + if updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) { updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{ SecretKey: secretKey, @@ -306,7 +307,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context // We need to check if any new secrets have been added in the kube secret for currentSecretKey := range kubeSecrets { - if !existingSecretsContainsKey(currentSecretKey) { + if exists := getExistingSecretByKey(currentSecretKey); exists == nil { // Some secrets has been added, verify that the secret that has been added is not already managed by the operator if _, ok := infisicalPushSecret.Status.ManagedSecrets[currentSecretKey]; !ok { @@ -332,21 +333,29 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context } } else { if updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) { - updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{ - SecretKey: currentSecretKey, - NewSecretValue: kubeSecrets[currentSecretKey], - ProjectID: destination.ProjectID, - Environment: destination.EnvironmentSlug, - SecretPath: destination.SecretsPath, - }) - if err != nil { - secretsFailedToUpdate = append(secretsFailedToUpdate, currentSecretKey) - logger.Info(fmt.Sprintf("unable to update secret [key=%s] [err=%s]", currentSecretKey, err)) - continue + existingSecret := getExistingSecretByKey(currentSecretKey) + + if existingSecret != nil && existingSecret.SecretValue != kubeSecrets[currentSecretKey] { + logger.Info(fmt.Sprintf("Secret with key [key=%s] has changed value. Updating secret in Infisical", currentSecretKey)) + + updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{ + SecretKey: currentSecretKey, + NewSecretValue: kubeSecrets[currentSecretKey], + ProjectID: destination.ProjectID, + Environment: destination.EnvironmentSlug, + SecretPath: destination.SecretsPath, + }) + + if err != nil { + secretsFailedToUpdate = append(secretsFailedToUpdate, currentSecretKey) + logger.Info(fmt.Sprintf("unable to update secret [key=%s] [err=%s]", currentSecretKey, err)) + continue + } + + updateExistingSecretByKey(currentSecretKey, kubeSecrets[currentSecretKey]) + infisicalPushSecret.Status.ManagedSecrets[updatedSecret.ID] = currentSecretKey } - - infisicalPushSecret.Status.ManagedSecrets[updatedSecret.ID] = currentSecretKey } } } From 35d3581e23cb38be2401936ca97f35fd61393946 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 11 Dec 2024 22:22:52 +0400 Subject: [PATCH 23/32] fix(k8s): fixed dynamic secret bugs --- .../infisicaldynamicsecret/conditions.go | 27 --------------- .../infisicaldynamicsecret_controller.go | 1 - .../infisicaldynamicsecret_helper.go | 33 +++++++++++-------- 3 files changed, 20 insertions(+), 41 deletions(-) diff --git a/k8-operator/controllers/infisicaldynamicsecret/conditions.go b/k8-operator/controllers/infisicaldynamicsecret/conditions.go index 9620167e1..417451952 100644 --- a/k8-operator/controllers/infisicaldynamicsecret/conditions.go +++ b/k8-operator/controllers/infisicaldynamicsecret/conditions.go @@ -118,33 +118,6 @@ func (r *InfisicalDynamicSecretReconciler) SetCreatedLeaseStatus(ctx context.Con } } -func (r *InfisicalDynamicSecretReconciler) SetRevokedLeaseStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) { - if infisicalDynamicSecret.Status.Conditions == nil { - infisicalDynamicSecret.Status.Conditions = []metav1.Condition{} - } - - if errorToConditionOn == nil { - meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{ - Type: "secrets.infisical.com/LeaseRevoked", - Status: metav1.ConditionTrue, - Reason: "OK", - Message: "Infisical has successfully revoked the lease", - }) - } else { - meta.SetStatusCondition(&infisicalDynamicSecret.Status.Conditions, metav1.Condition{ - Type: "secrets.infisical.com/LeaseRevoked", - Status: metav1.ConditionFalse, - Reason: "Error", - Message: fmt.Sprintf("Failed to revoke the lease because: %v", errorToConditionOn), - }) - } - - err := r.Client.Status().Update(ctx, infisicalDynamicSecret) - if err != nil { - logger.Error(err, "Could not set condition for LeaseRevoked") - } -} - func (r *InfisicalDynamicSecretReconciler) SetReconcileStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) { if infisicalDynamicSecret.Status.Conditions == nil { infisicalDynamicSecret.Status.Conditions = []metav1.Condition{} diff --git a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go index 468a0e799..0158cc732 100644 --- a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go +++ b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go @@ -86,7 +86,6 @@ func (r *InfisicalDynamicSecretReconciler) Reconcile(ctx context.Context, req ct } err := r.HandleLeaseRevocation(ctx, logger, infisicalDynamicSecretCRD) - r.SetRevokedLeaseStatus(ctx, logger, &infisicalDynamicSecretCRD, err) if infisicalDynamicSecretsResourceVariablesMap != nil { if rv, ok := infisicalDynamicSecretsResourceVariablesMap[string(infisicalDynamicSecretCRD.GetUID())]; ok { diff --git a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go index c5d7a5a06..3f61eb9d1 100644 --- a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go +++ b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go @@ -256,7 +256,7 @@ func (r *InfisicalDynamicSecretReconciler) updateResourceVariables(infisicalDyna infisicalDynamicSecretsResourceVariablesMap[string(infisicalDynamicSecret.UID)] = resourceVariables } -func (r *InfisicalDynamicSecretReconciler) HandleLeaseRevocation(ctx context.Context, logger logr.Logger, infisicalDynamicSecret v1alpha1.InfisicalDynamicSecret) error { +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 } @@ -266,7 +266,6 @@ func (r *InfisicalDynamicSecretReconciler) HandleLeaseRevocation(ctx context.Con logger.Info("Authenticating for lease revocation") authDetails, err := r.handleAuthentication(ctx, infisicalDynamicSecret, infisicalClient) - r.SetAuthenticatedStatus(ctx, logger, &infisicalDynamicSecret, err) if err != nil { return fmt.Errorf("unable to authenticate for lease revocation [err=%s]", err) @@ -349,20 +348,28 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c 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") + annotationValue := "" + if infisicalDynamicSecret.Status.Lease != nil { + annotationValue = fmt.Sprintf("%s-%d", infisicalDynamicSecret.Status.Lease.ID, infisicalDynamicSecret.Status.Lease.Version) + } + + r.createInfisicalManagedKubeSecret(ctx, logger, *infisicalDynamicSecret, annotationValue) + + destination, err = util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{ + Name: infisicalDynamicSecret.Spec.ManagedSecretReference.SecretName, + Namespace: infisicalDynamicSecret.Spec.ManagedSecretReference.SecretNamespace, + }) + + if err != nil { + return nextReconcile, fmt.Errorf("unable to fetch destination secret after creation [err=%s]", err) + } + + } else { + return nextReconcile, fmt.Errorf("unable to fetch destination secret") + } } if infisicalDynamicSecret.Status.Lease == nil { From 8b26670d7338a83e7753f728047322bebfacf428 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 11 Dec 2024 22:25:02 +0400 Subject: [PATCH 24/32] fix(k8s): dynamic secret structual change --- .../infisicaldynamicsecret_controller.go | 2 +- .../infisicaldynamicsecret/infisicaldynamicsecret_helper.go | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go index 0158cc732..946a5e528 100644 --- a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go +++ b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go @@ -85,7 +85,7 @@ func (r *InfisicalDynamicSecretReconciler) Reconcile(ctx context.Context, req ct return ctrl.Result{}, err } - err := r.HandleLeaseRevocation(ctx, logger, infisicalDynamicSecretCRD) + err := r.HandleLeaseRevocation(ctx, logger, &infisicalDynamicSecretCRD) if infisicalDynamicSecretsResourceVariablesMap != nil { if rv, ok := infisicalDynamicSecretsResourceVariablesMap[string(infisicalDynamicSecretCRD.GetUID())]; ok { diff --git a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go index 3f61eb9d1..0d9cf53ee 100644 --- a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go +++ b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go @@ -261,17 +261,17 @@ func (r *InfisicalDynamicSecretReconciler) HandleLeaseRevocation(ctx context.Con return nil } - resourceVariables := r.getResourceVariables(infisicalDynamicSecret) + resourceVariables := r.getResourceVariables(*infisicalDynamicSecret) infisicalClient := resourceVariables.InfisicalClient logger.Info("Authenticating for lease revocation") - authDetails, err := r.handleAuthentication(ctx, infisicalDynamicSecret, infisicalClient) + 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{ + r.updateResourceVariables(*infisicalDynamicSecret, util.ResourceVariables{ InfisicalClient: infisicalClient, CancelCtx: resourceVariables.CancelCtx, AuthDetails: authDetails, From 36a13d182fa6b86faf40aa26f8b739bcb94498ed Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 12 Dec 2024 06:13:55 +0400 Subject: [PATCH 25/32] requested changes --- .../infisicaldynamicsecret_controller.go | 2 ++ .../infisicaldynamicsecret_helper.go | 7 ++++--- k8-operator/go.sum | 4 ---- k8-operator/main.go | 4 ++++ 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go index 946a5e528..a7c5464a9 100644 --- a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go +++ b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go @@ -3,6 +3,7 @@ package controllers import ( "context" "fmt" + "math/rand" "time" "k8s.io/apimachinery/pkg/api/errors" @@ -28,6 +29,7 @@ type InfisicalDynamicSecretReconciler struct { Scheme *runtime.Scheme BaseLogger logr.Logger + Random *rand.Rand } var infisicalDynamicSecretsResourceVariablesMap map[string]util.ResourceVariables = make(map[string]util.ResourceVariables) diff --git a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go index 0d9cf53ee..c923a0be0 100644 --- a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go +++ b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go @@ -384,8 +384,9 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c // Calculate from creation to expiration originalLeaseDuration := leaseExpiresAt.Sub(infisicalDynamicSecret.Status.Lease.CreationTimestamp.Time) - // 30% of the original duration (if the TTL has 50% or less of its time left, renew) - renewalThreshold := originalLeaseDuration * 50 / 100 + // Generate a random percentage between 20% and 30% + jitterPercentage := 20 + r.Random.Intn(11) // Random int from 0 to 10, then add 20 + renewalThreshold := originalLeaseDuration * time.Duration(jitterPercentage) / 100 timeUntilExpiration := time.Until(leaseExpiresAt) nextReconcile = timeUntilExpiration / 2 @@ -424,7 +425,7 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c return defaultNextReconcile, err // Short requeue after creation } - if timeUntilExpiration < renewalThreshold { + if timeUntilExpiration < renewalThreshold || timeUntilExpiration < 30*time.Second { logger.Info(fmt.Sprintf("Lease renewal needed [leaseId=%s] [timeUntilExpiration=%v] [threshold=%v]", infisicalDynamicSecret.Status.Lease.ID, timeUntilExpiration, diff --git a/k8-operator/go.sum b/k8-operator/go.sum index f6b61945a..9037bc4a0 100644 --- a/k8-operator/go.sum +++ b/k8-operator/go.sum @@ -217,10 +217,6 @@ github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= 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= diff --git a/k8-operator/main.go b/k8-operator/main.go index 4afbf6e56..4ae3d866e 100644 --- a/k8-operator/main.go +++ b/k8-operator/main.go @@ -3,9 +3,12 @@ package main import ( "flag" "os" + "time" // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. + "math/rand" + _ "k8s.io/client-go/plugin/pkg/client/auth" "k8s.io/apimachinery/pkg/runtime" @@ -97,6 +100,7 @@ func main() { Client: mgr.GetClient(), Scheme: mgr.GetScheme(), BaseLogger: ctrl.Log, + Random: rand.New(rand.NewSource(time.Now().UnixNano())), }).SetupWithManager(mgr); err != nil { setupLog.Error(err, "unable to create controller", "controller", "InfisicalDynamicSecret") os.Exit(1) From e76d2f58eaeabf47a5cb028f141a975847b91597 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 18 Dec 2024 01:39:32 +0100 Subject: [PATCH 26/32] fix: move fixes from different branch --- .../infisicalpushsecret_helper.go | 57 +++++++++++-------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go index ebe125a49..9ba514dbc 100644 --- a/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go +++ b/k8-operator/controllers/infisicalpushsecret/infisicalpushsecret_helper.go @@ -148,15 +148,6 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context IncludeImports: false, }) - existingSecretsContainsKey := func(key string) bool { - for _, secret := range existingSecrets { - if secret.SecretKey == key { - return true - } - } - return false - } - getExistingSecretByKey := func(key string) *infisicalSdk.Secret { for _, secret := range existingSecrets { if secret.SecretKey == key { @@ -175,6 +166,15 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context return nil } + updateExistingSecretByKey := func(key string, newSecretValue string) { + for i := range existingSecrets { + if existingSecrets[i].SecretKey == key { + existingSecrets[i].SecretValue = newSecretValue + break + } + } + } + if err != nil { return fmt.Errorf("unable to list secrets [err=%s]", err) } @@ -192,7 +192,8 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context infisicalPushSecret.Status.ManagedSecrets = make(map[string]string) // (string[id], string[key] ) for secretKey, secretValue := range kubeSecrets { - if existingSecretsContainsKey(secretKey) { + if exists := getExistingSecretByKey(secretKey); exists != nil { + if updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) { updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{ SecretKey: secretKey, @@ -306,7 +307,7 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context // We need to check if any new secrets have been added in the kube secret for currentSecretKey := range kubeSecrets { - if !existingSecretsContainsKey(currentSecretKey) { + if exists := getExistingSecretByKey(currentSecretKey); exists == nil { // Some secrets has been added, verify that the secret that has been added is not already managed by the operator if _, ok := infisicalPushSecret.Status.ManagedSecrets[currentSecretKey]; !ok { @@ -332,21 +333,29 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context } } else { if updatePolicy == string(constants.PUSH_SECRET_REPLACE_POLICY_ENABLED) { - updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{ - SecretKey: currentSecretKey, - NewSecretValue: kubeSecrets[currentSecretKey], - ProjectID: destination.ProjectID, - Environment: destination.EnvironmentSlug, - SecretPath: destination.SecretsPath, - }) - if err != nil { - secretsFailedToUpdate = append(secretsFailedToUpdate, currentSecretKey) - logger.Info(fmt.Sprintf("unable to update secret [key=%s] [err=%s]", currentSecretKey, err)) - continue + existingSecret := getExistingSecretByKey(currentSecretKey) + + if existingSecret != nil && existingSecret.SecretValue != kubeSecrets[currentSecretKey] { + logger.Info(fmt.Sprintf("Secret with key [key=%s] has changed value. Updating secret in Infisical", currentSecretKey)) + + updatedSecret, err := infisicalClient.Secrets().Update(infisicalSdk.UpdateSecretOptions{ + SecretKey: currentSecretKey, + NewSecretValue: kubeSecrets[currentSecretKey], + ProjectID: destination.ProjectID, + Environment: destination.EnvironmentSlug, + SecretPath: destination.SecretsPath, + }) + + if err != nil { + secretsFailedToUpdate = append(secretsFailedToUpdate, currentSecretKey) + logger.Info(fmt.Sprintf("unable to update secret [key=%s] [err=%s]", currentSecretKey, err)) + continue + } + + updateExistingSecretByKey(currentSecretKey, kubeSecrets[currentSecretKey]) + infisicalPushSecret.Status.ManagedSecrets[updatedSecret.ID] = currentSecretKey } - - infisicalPushSecret.Status.ManagedSecrets[updatedSecret.ID] = currentSecretKey } } } From 4daaf80caa3ff5f786acbe9c0d1ee807a0405d4c Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Wed, 18 Dec 2024 02:56:13 +0100 Subject: [PATCH 27/32] fix: better naming --- .../controllers/infisicaldynamicsecret/conditions.go | 10 +++++----- .../infisicaldynamicsecret_controller.go | 4 ++-- .../infisicaldynamicsecret_helper.go | 12 ++++++------ 3 files changed, 13 insertions(+), 13 deletions(-) diff --git a/k8-operator/controllers/infisicaldynamicsecret/conditions.go b/k8-operator/controllers/infisicaldynamicsecret/conditions.go index 417451952..a26e5b71d 100644 --- a/k8-operator/controllers/infisicaldynamicsecret/conditions.go +++ b/k8-operator/controllers/infisicaldynamicsecret/conditions.go @@ -10,7 +10,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) -func (r *InfisicalDynamicSecretReconciler) SetReconcileAutoRedeploymentStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, numDeployments int, errorToConditionOn error) { +func (r *InfisicalDynamicSecretReconciler) SetReconcileAutoRedeploymentConditionStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, numDeployments int, errorToConditionOn error) { if infisicalDynamicSecret.Status.Conditions == nil { infisicalDynamicSecret.Status.Conditions = []metav1.Condition{} } @@ -37,7 +37,7 @@ func (r *InfisicalDynamicSecretReconciler) SetReconcileAutoRedeploymentStatus(ct } } -func (r *InfisicalDynamicSecretReconciler) SetAuthenticatedStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) { +func (r *InfisicalDynamicSecretReconciler) SetAuthenticatedConditionStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) { if infisicalDynamicSecret.Status.Conditions == nil { infisicalDynamicSecret.Status.Conditions = []metav1.Condition{} } @@ -64,7 +64,7 @@ func (r *InfisicalDynamicSecretReconciler) SetAuthenticatedStatus(ctx context.Co } } -func (r *InfisicalDynamicSecretReconciler) SetLeaseRenewalStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) { +func (r *InfisicalDynamicSecretReconciler) SetLeaseRenewalConditionStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) { if infisicalDynamicSecret.Status.Conditions == nil { infisicalDynamicSecret.Status.Conditions = []metav1.Condition{} } @@ -91,7 +91,7 @@ func (r *InfisicalDynamicSecretReconciler) SetLeaseRenewalStatus(ctx context.Con } } -func (r *InfisicalDynamicSecretReconciler) SetCreatedLeaseStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) { +func (r *InfisicalDynamicSecretReconciler) SetCreatedLeaseConditionStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) { if infisicalDynamicSecret.Status.Conditions == nil { infisicalDynamicSecret.Status.Conditions = []metav1.Condition{} } @@ -118,7 +118,7 @@ func (r *InfisicalDynamicSecretReconciler) SetCreatedLeaseStatus(ctx context.Con } } -func (r *InfisicalDynamicSecretReconciler) SetReconcileStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) { +func (r *InfisicalDynamicSecretReconciler) SetReconcileConditionStatus(ctx context.Context, logger logr.Logger, infisicalDynamicSecret *v1alpha1.InfisicalDynamicSecret, errorToConditionOn error) { if infisicalDynamicSecret.Status.Conditions == nil { infisicalDynamicSecret.Status.Conditions = []metav1.Condition{} } diff --git a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go index a7c5464a9..5d2470067 100644 --- a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go +++ b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_controller.go @@ -134,7 +134,7 @@ func (r *InfisicalDynamicSecretReconciler) Reconcile(ctx context.Context, req ct } nextReconcile, err := r.ReconcileInfisicalDynamicSecret(ctx, logger, &infisicalDynamicSecretCRD) - r.SetReconcileStatus(ctx, logger, &infisicalDynamicSecretCRD, err) + r.SetReconcileConditionStatus(ctx, logger, &infisicalDynamicSecretCRD, err) if err == nil && nextReconcile.Seconds() >= 5 { requeueTime = nextReconcile @@ -148,7 +148,7 @@ func (r *InfisicalDynamicSecretReconciler) Reconcile(ctx context.Context, req ct } numDeployments, err := controllerhelpers.ReconcileDeploymentsWithManagedSecrets(ctx, r.Client, logger, infisicalDynamicSecretCRD.Spec.ManagedSecretReference) - r.SetReconcileAutoRedeploymentStatus(ctx, logger, &infisicalDynamicSecretCRD, numDeployments, err) + r.SetReconcileAutoRedeploymentConditionStatus(ctx, logger, &infisicalDynamicSecretCRD, numDeployments, err) if err != nil { logger.Error(err, fmt.Sprintf("unable to reconcile auto redeployment. Will requeue after [requeueTime=%v]", requeueTime)) diff --git a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go index c923a0be0..861880d95 100644 --- a/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go +++ b/k8-operator/controllers/infisicaldynamicsecret/infisicaldynamicsecret_helper.go @@ -330,7 +330,7 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c if authDetails.AuthStrategy == "" { logger.Info("No authentication strategy found. Attempting to authenticate") authDetails, err = r.handleAuthentication(ctx, *infisicalDynamicSecret, infisicalClient) - r.SetAuthenticatedStatus(ctx, logger, infisicalDynamicSecret, err) + r.SetAuthenticatedConditionStatus(ctx, logger, infisicalDynamicSecret, err) if err != nil { return nextReconcile, fmt.Errorf("unable to authenticate [err=%s]", err) @@ -374,7 +374,7 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c if infisicalDynamicSecret.Status.Lease == nil { err := r.CreateDynamicSecretLease(ctx, logger, infisicalClient, infisicalDynamicSecret, destination) - r.SetCreatedLeaseStatus(ctx, logger, infisicalDynamicSecret, err) + r.SetCreatedLeaseConditionStatus(ctx, logger, infisicalDynamicSecret, err) return defaultNextReconcile, err // Short requeue after creation } else { @@ -412,7 +412,7 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c maxTTLThreshold)) err := r.CreateDynamicSecretLease(ctx, logger, infisicalClient, infisicalDynamicSecret, destination) - r.SetCreatedLeaseStatus(ctx, logger, infisicalDynamicSecret, err) + r.SetCreatedLeaseConditionStatus(ctx, logger, infisicalDynamicSecret, err) return defaultNextReconcile, err // Short requeue after creation } } @@ -421,7 +421,7 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c if now.After(leaseExpiresAt) { logger.Info("Lease has expired, creating new lease...") err = r.CreateDynamicSecretLease(ctx, logger, infisicalClient, infisicalDynamicSecret, destination) - r.SetCreatedLeaseStatus(ctx, logger, infisicalDynamicSecret, err) + r.SetCreatedLeaseConditionStatus(ctx, logger, infisicalDynamicSecret, err) return defaultNextReconcile, err // Short requeue after creation } @@ -432,12 +432,12 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c renewalThreshold)) err = r.RenewDynamicSecretLease(ctx, logger, infisicalClient, infisicalDynamicSecret, destination) - r.SetLeaseRenewalStatus(ctx, logger, infisicalDynamicSecret, err) + r.SetLeaseRenewalConditionStatus(ctx, logger, infisicalDynamicSecret, err) if err == constants.ErrInvalidLease { logger.Info("Failed to renew expired lease, creating new lease...") err = r.CreateDynamicSecretLease(ctx, logger, infisicalClient, infisicalDynamicSecret, destination) - r.SetCreatedLeaseStatus(ctx, logger, infisicalDynamicSecret, err) + r.SetCreatedLeaseConditionStatus(ctx, logger, infisicalDynamicSecret, err) } return defaultNextReconcile, err // Short requeue after renewal/creation From aa39451bc25d846bd1daa1e2bd7f9fd9c545f7a7 Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Mon, 6 Jan 2025 22:03:56 +0100 Subject: [PATCH 28/32] fix: generated files --- .../api/v1alpha1/zz_generated.deepcopy.go | 145 ----- ...ts.infisical.com_infisicalpushsecrets.yaml | 494 +++++++++--------- 2 files changed, 240 insertions(+), 399 deletions(-) diff --git a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go index ee6044383..bad990bc4 100644 --- a/k8-operator/api/v1alpha1/zz_generated.deepcopy.go +++ b/k8-operator/api/v1alpha1/zz_generated.deepcopy.go @@ -720,151 +720,6 @@ func (in *SecretPush) DeepCopy() *SecretPush { 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) - in.DeepCopyInto(out) - return out -} - -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *SecretPush) DeepCopyInto(out *SecretPush) { - *out = *in - out.Secret = in.Secret -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretPush. -func (in *SecretPush) DeepCopy() *SecretPush { - if in == nil { - return nil - } - out := new(SecretPush) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SecretScopeInWorkspace) DeepCopyInto(out *SecretScopeInWorkspace) { *out = *in diff --git a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml index d32534aee..a12ec9dbe 100644 --- a/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml +++ b/k8-operator/config/crd/bases/secrets.infisical.com_infisicalpushsecrets.yaml @@ -15,263 +15,249 @@ spec: singular: infisicalpushsecret scope: Namespaced versions: - - name: v1alpha1 - schema: - openAPIV3Schema: - description: - InfisicalPushSecret is the Schema for the infisicalpushsecrets - 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: InfisicalPushSecretSpec defines the desired state of InfisicalPushSecret - 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 - deletionPolicy: - type: string - destination: - properties: - environmentSlug: - type: string - projectId: - type: string - secretsPath: - type: string - required: - - environmentSlug - - projectId - - secretsPath - type: object - hostAPI: - description: Infisical host to pull secrets from - type: string - push: - properties: - secret: - 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: - - secret - type: object - resyncInterval: - type: string - 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 - updatePolicy: - type: string - required: - - destination - - push - - resyncInterval - type: object - status: - description: InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret - properties: - conditions: - items: - description: - "Condition contains details for one aspect of the current - state of this API Resource. --- This struct is intended for direct - use as an array at the field path .status.conditions. For example, - \n type FooStatus struct{ // Represents the observations of a - foo's current state. // Known .status.conditions.type are: \"Available\", - \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge - // +listType=map // +listMapKey=type Conditions []metav1.Condition - `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" - protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + - name: v1alpha1 + schema: + openAPIV3Schema: + description: InfisicalPushSecret is the Schema for the infisicalpushsecrets + 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: InfisicalPushSecretSpec defines the desired state of InfisicalPushSecret + properties: + authentication: + properties: + awsIamAuth: properties: - lastTransitionTime: - description: - lastTransitionTime is the last time the condition - transitioned from one status to another. This should be when - the underlying condition changed. If that is not known, then - using the time when the API field changed is acceptable. - format: date-time - type: string - message: - description: - message is a human readable message indicating - details about the transition. This may be an empty string. - maxLength: 32768 - type: string - observedGeneration: - description: - observedGeneration represents the .metadata.generation - that the condition was set based upon. For instance, if .metadata.generation - is currently 12, but the .status.conditions[x].observedGeneration - is 9, the condition is out of date with respect to the current - state of the instance. - format: int64 - minimum: 0 - type: integer - reason: - description: - reason contains a programmatic identifier indicating - the reason for the condition's last transition. Producers - of specific condition types may define expected values and - meanings for this field, and whether the values are considered - a guaranteed API. The value should be a CamelCase string. - This field may not be empty. - maxLength: 1024 - minLength: 1 - pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ - type: string - status: - description: status of the condition, one of True, False, Unknown. - enum: - - "True" - - "False" - - Unknown - type: string - type: - description: - type of condition in CamelCase or in foo.example.com/CamelCase. - --- Many .condition.type values are consistent across resources - like Available, but because arbitrary conditions can be useful - (see .node.status.conditions), the ability to deconflict is - important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) - maxLength: 316 - pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + identityId: type: string required: - - lastTransitionTime - - message - - reason - - status - - type + - identityId type: object - type: array - managedSecrets: - additionalProperties: + 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 + deletionPolicy: + type: string + destination: + properties: + environmentSlug: type: string - description: - managed secrets is a map where the key is the ID, and - the value is the secret key (string[id], string[key] ) + projectId: + type: string + secretsPath: + type: string + required: + - environmentSlug + - projectId + - secretsPath + type: object + hostAPI: + description: Infisical host to pull secrets from + type: string + push: + properties: + secret: + 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: + - secret + type: object + resyncInterval: + type: string + 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 + updatePolicy: + type: string + required: + - destination + - push + - resyncInterval + type: object + status: + description: InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type type: object - required: - - conditions - - managedSecrets - type: object - type: object - served: true - storage: true - subresources: - status: {} + type: array + managedSecrets: + additionalProperties: + type: string + description: managed secrets is a map where the key is the ID, and + the value is the secret key (string[id], string[key] ) + type: object + required: + - conditions + - managedSecrets + type: object + type: object + served: true + storage: true + subresources: + status: {} From e29f7f656c5e79d2de7e86365f1b6f9d81aa4d1f Mon Sep 17 00:00:00 2001 From: Daniel Hougaard Date: Thu, 9 Jan 2025 16:07:07 +0100 Subject: [PATCH 29/32] docs(k8s): better documentation layout --- docs/integrations/platforms/kubernetes.mdx | 1995 ----------------- .../infisical-dynamic-secret-crd.mdx | 425 ++++ .../kubernetes/infisical-push-secret-crd.mdx | 400 ++++ .../kubernetes/infisical-secret-crd.mdx | 965 ++++++++ .../platforms/kubernetes/overview.mdx | 202 ++ docs/mint.json | 10 +- 6 files changed, 2001 insertions(+), 1996 deletions(-) delete mode 100644 docs/integrations/platforms/kubernetes.mdx create mode 100644 docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx create mode 100644 docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx create mode 100644 docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx create mode 100644 docs/integrations/platforms/kubernetes/overview.mdx diff --git a/docs/integrations/platforms/kubernetes.mdx b/docs/integrations/platforms/kubernetes.mdx deleted file mode 100644 index 4f835c4f7..000000000 --- a/docs/integrations/platforms/kubernetes.mdx +++ /dev/null @@ -1,1995 +0,0 @@ ---- -title: "Kubernetes Operator" -description: "How to use Infisical to inject secrets into Kubernetes clusters." ---- - -![title](../../images/k8-diagram.png) - -The Infisical Secrets Operator is a Kubernetes controller that retrieves secrets from Infisical and stores them in a designated cluster. -It uses an `InfisicalSecret` resource to specify authentication and storage methods. -The operator continuously updates secrets and can also reload dependent deployments automatically. - - - If you are already using the External Secrets operator, you can view the - integration documentation for it - [here](https://external-secrets.io/latest/provider/infisical/). - - -## Install Operator - -The operator can be install via [Helm](https://helm.sh) or [kubectl](https://github.com/kubernetes/kubectl) - - - - **Install the latest Infisical Helm repository** - ```bash - helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' - - helm repo update - ``` - - **Install the Helm chart** - - To select a specific version, view the application versions [here](https://hub.docker.com/r/infisical/kubernetes-operator/tags) and chart versions [here](https://cloudsmith.io/~infisical/repos/helm-charts/packages/detail/helm/secrets-operator/#versions) - - ```bash - helm install --generate-name infisical-helm-charts/secrets-operator - ``` - - ```bash - # Example installing app version v0.2.0 and chart version 0.1.4 - helm install --generate-name infisical-helm-charts/secrets-operator --version=0.1.4 --set controllerManager.manager.image.tag=v0.2.0 - ``` - - **Namespace-scoped Installation** - - The operator can be configured to watch and manage secrets in a specific namespace instead of having cluster-wide access. This is useful for: - - - **Enhanced Security**: Limit the operator's permissions to only specific namespaces instead of cluster-wide access - - **Multi-tenant Clusters**: Run separate operator instances for different teams or applications - - **Resource Isolation**: Ensure operators in different namespaces don't interfere with each other - - **Development & Testing**: Run development and production operators side by side in isolated namespaces - - **Note**: For multiple namespace-scoped installations, only the first installation should install CRDs. Subsequent installations should set `installCRDs: false` to avoid conflicts. - - ```bash - # First namespace installation (with CRDs) - helm install operator-namespace1 infisical-helm-charts/secrets-operator \ - --namespace first-namespace \ - --set scopedNamespace=first-namespace \ - --set scopedRBAC=true - - # Subsequent namespace installations - helm install operator-namespace2 infisical-helm-charts/secrets-operator \ - --namespace another-namespace \ - --set scopedNamespace=another-namespace \ - --set scopedRBAC=true \ - --set installCRDs=false - ``` - - When scoped to a namespace, the operator will: - - - Only watch InfisicalSecrets in the specified namespace - - Only create/update Kubernetes secrets in that namespace - - Only access deployments in that namespace - - The default configuration gives cluster-wide access: - - ```yaml - installCRDs: true # Install CRDs (set to false for additional namespace installations) - scopedNamespace: "" # Empty for cluster-wide access - scopedRBAC: false # Cluster-wide permissions - ``` - - If you want to install operators in multiple namespaces simultaneously: - - Make sure to set `installCRDs: false` for all but one of the installations to avoid conflicts, as CRDs are cluster-wide resources. - - Use unique release names for each installation (e.g., operator-namespace1, operator-namespace2). - - - - For production deployments, it is highly recommended to set the version of the Kubernetes operator manually instead of pointing to the latest version. - Doing so will help you avoid accidental updates to the newest release which may introduce unintended breaking changes. View all application versions [here](https://hub.docker.com/r/infisical/kubernetes-operator/tags). - -The command below will install the most recent version of the Kubernetes operator. -However, to set the version manually, download the manifest and set the image tag version of `infisical/kubernetes-operator` according to your desired version. - -Once you apply the manifest, the operator will be installed in `infisical-operator-system` namespace. - - ``` - kubectl apply -f https://raw.githubusercontent.com/Infisical/infisical/main/k8-operator/kubectl-install/install-secrets-operator.yaml - ``` - - - - - -## Custom Resource Definitions (CRD's) - -Currently the operator supports the following CRD's. We are constantly expanding the functionality of the operator, and this list will be updated as new CRD's are added. - -1. [InfisicalSecret](#sync-infisical-secrets-to-your-cluster): Sync secrets from Infisical to a Kubernetes secret. -2. [InfisicalPushSecret](#push-secrets-to-infisical): Push secrets from a Kubernetes secret to Infisical. -3. [InfisicalDynamicSecret](#sync-dynamic-secrets-to-your-cluster): Sync dynamic secrets and create leases automatically in Kubernetes. - - -## Sync Infisical Secrets to your cluster - -Once you have installed the operator to your cluster, you'll need to create a `InfisicalSecret` custom resource definition (CRD). - -```yaml example-infisical-secret-crd.yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample - labels: - label-to-be-passed-to-managed-secret: sample-value - annotations: - example.com/annotation-to-be-passed-to-managed-secret: "sample-value" -spec: - hostAPI: https://app.infisical.com/api - resyncInterval: 10 - authentication: - # Make sure to only have 1 authentication method defined, serviceToken/universalAuth. - # If you have multiple authentication methods defined, it may cause issues. - - # (Deprecated) Service Token Auth - serviceToken: - serviceTokenSecretReference: - secretName: service-token - secretNamespace: default - secretsScope: - envSlug: - secretsPath: - recursive: true - - # Universal Auth - universalAuth: - secretsScope: - projectSlug: new-ob-em - envSlug: dev # "dev", "staging", "prod", etc.. - secretsPath: "/" # Root is "/" - recursive: true # Whether or not to use recursive mode (Fetches all secrets in an environment from a given secret path, and all folders inside the path) / defaults to false - credentialsRef: - secretName: universal-auth-credentials - secretNamespace: default - - # Native Kubernetes Auth - kubernetesAuth: - identityId: - serviceAccountRef: - name: - namespace: - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - - # AWS IAM Auth - awsIamAuth: - identityId: - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - - # Azure Auth - azureAuth: - identityId: - resource: https://management.azure.com/&client_id=CLIENT_ID # (Optional) This is the Azure resource that you want to access. For example, "https://management.azure.com/". If no value is provided, it will default to "https://management.azure.com/" - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - - # GCP ID Token Auth - gcpIdTokenAuth: - identityId: - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - - # GCP IAM Auth - gcpIamAuth: - identityId: - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - - managedSecretReference: - secretName: managed-secret - secretNamespace: default - creationPolicy: "Orphan" ## Owner | Orphan - # template: - # includeAllSecrets: true - # data: - # CUSTOM_KEY: "{{ .KEY.SecretPath }} {{ .KEY.Value }}" - # secretType: kubernetes.io/dockerconfigjson -``` - -### InfisicalSecret CRD properties - - - If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to - ` https://your-self-hosted-instace.com/api` - -When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud. - - - If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet. - To achieve this, use the following address for the hostAPI field: - - ``` bash - http://..svc.cluster.local:4000/api - ``` - - Make sure to replace `` and `` with the appropriate values for your backend service and namespace. - - - - - - This property defines the time in seconds between each secret re-sync from - Infisical. Shorter time between re-syncs will require higher rate limits only - available on paid plans. Default re-sync interval is every 1 minute. - - - - This block defines the TLS settings to use for connecting to the Infisical - instance. - - - - This block defines the reference to the CA certificate to use for connecting - to the Infisical instance with SSL/TLS. - - - - The name of the Kubernetes secret containing the CA certificate to use for - connecting to the Infisical instance with SSL/TLS. - - - - The namespace of the Kubernetes secret containing the CA certificate to use - for connecting to the Infisical instance with SSL/TLS. - - - - The name of the key in the Kubernetes secret which contains the value of the - CA certificate to use for connecting to the Infisical instance with SSL/TLS. - - - - This block defines the method that will be used to authenticate with Infisical - so that secrets can be fetched - - - - The universal machine identity authentication method is used to authenticate with Infisical. The client ID and client secret needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores these credentials. - - - - You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about machine identities here](/documentation/platform/identities/universal-auth). - - - Once you have created your machine identity and added it to your project(s), you will need to create a Kubernetes secret containing the identity credentials. - To quickly create a Kubernetes secret containing the identity credentials, you can run the command below. - - Make sure you replace `` with the identity client ID and `` with the identity client secret. - - ``` bash - kubectl create secret generic universal-auth-credentials --from-literal=clientId="" --from-literal=clientSecret="" - ``` - - - - Once the secret is created, add the `secretName` and `secretNamespace` of the secret that was just created under `authentication.universalAuth.credentialsRef` field in the InfisicalSecret resource. - - - - - - Make sure to also populate the `secretsScope` field with the project slug - _`projectSlug`_, environment slug _`envSlug`_, and secrets path - _`secretsPath`_ that you want to fetch secrets from. Please see the example - below. - - -## Example - -```yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample-crd -spec: - authentication: - universalAuth: - secretsScope: - projectSlug: # <-- project slug - envSlug: # "dev", "staging", "prod", etc.. - secretsPath: "" # Root is "/" - credentialsRef: - secretName: universal-auth-credentials # <-- name of the Kubernetes secret that stores our machine identity credentials - secretNamespace: default # <-- namespace of the Kubernetes secret that stores our machine identity credentials - ... -``` - - - - - The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment. - - - - 1.1. Start by creating a service account in your Kubernetes cluster that will be used by Infisical to authenticate with the Kubernetes API Server. - - ```yaml infisical-service-account.yaml - apiVersion: v1 - kind: ServiceAccount - metadata: - name: infisical-auth - namespace: default - - ``` - - ``` - kubectl apply -f infisical-service-account.yaml - ``` - - 1.2. Bind the service account to the `system:auth-delegator` cluster role. As described [here](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#other-component-roles), this role allows delegated authentication and authorization checks, specifically for Infisical to access the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/). You can apply the following configuration file: - - ```yaml cluster-role-binding.yaml - apiVersion: rbac.authorization.k8s.io/v1 - kind: ClusterRoleBinding - metadata: - name: role-tokenreview-binding - namespace: default - roleRef: - apiGroup: rbac.authorization.k8s.io - kind: ClusterRole - name: system:auth-delegator - subjects: - - kind: ServiceAccount - name: infisical-auth - namespace: default - ``` - - ``` - kubectl apply -f cluster-role-binding.yaml - ``` - - 1.3. Next, create a long-lived service account JWT token (i.e. the token reviewer JWT token) for the service account using this configuration file for a new `Secret` resource: - - ```yaml service-account-token.yaml - apiVersion: v1 - kind: Secret - type: kubernetes.io/service-account-token - metadata: - name: infisical-auth-token - annotations: - kubernetes.io/service-account.name: "infisical-auth" - ``` - - - ``` - kubectl apply -f service-account-token.yaml - ``` - - 1.4. Link the secret in step 1.3 to the service account in step 1.1: - - ```bash - kubectl patch serviceaccount infisical-auth -p '{"secrets": [{"name": "infisical-auth-token"}]}' -n default - ``` - - 1.5. Finally, retrieve the token reviewer JWT token from the secret. - - ```bash - kubectl get secret infisical-auth-token -n default -o=jsonpath='{.data.token}' | base64 --decode - ``` - - Keep this JWT token handy as you will need it for the **Token Reviewer JWT** field when configuring the Kubernetes Auth authentication method for the identity in step 2. - - - - - To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. - - ![identities organization](/images/platform/identities/identities-org.png) - - When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles. - - ![identities organization create](/images/platform/identities/identities-org-create.png) - - Now input a few details for your new identity. Here's some guidance for each field: - - - Name (required): A friendly name for the identity. - - Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to. - - Once you've created an identity, you'll be prompted to configure the authentication method for it. Here, select **Kubernetes Auth**. - - - To learn more about each field of the Kubernetes native authentication method, see step 2 of [guide](/documentation/platform/identities/kubernetes-auth#guide). - - - ![identities organization create auth method](/images/platform/identities/identities-org-create-kubernetes-auth-method.png) - - - - - To allow the operator to use the given identity to access secrets, you will need to add the identity to project(s) that you would like to grant it access to. - - To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**. - - Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to. - - ![identities project](/images/platform/identities/identities-project.png) - - ![identities project create](/images/platform/identities/identities-project-create.png) - - - - Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. - In the `authentication.kubernetesAuth.identityId` field, add the identity ID of the machine identity you created. - See the example below for more details. - - - Add the service account details from the previous steps under `authentication.kubernetesAuth.serviceAccountRef`. - Here you will need to enter the name and namespace of the service account. - The example below shows a complete InfisicalSecret resource with all required fields defined. - - - - - - Make sure to also populate the `secretsScope` field with the project slug - _`projectSlug`_, environment slug _`envSlug`_, and secrets path - _`secretsPath`_ that you want to fetch secrets from. Please see the example - below. - - -## Example - -```yaml example-kubernetes-auth.yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample-crd -spec: - authentication: - kubernetesAuth: - identityId: - serviceAccountRef: - name: - namespace: - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - ... -``` - - - - - The AWS IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within an AWS environment like an EC2 or a Lambda function. - - - - You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about AWS machine identities here](/documentation/platform/identities/aws-auth). - - - Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. In the `authentication.awsIamAuth.identityId` field, add the identity ID of the machine identity you created. See the example below for more details. - - - - - - Make sure to also populate the `secretsScope` field with the project slug - _`projectSlug`_, environment slug _`envSlug`_, and secrets path - _`secretsPath`_ that you want to fetch secrets from. Please see the example - below. - - -## Example - -```yaml example-aws-iam-auth.yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample-crd -spec: - authentication: - awsIamAuth: - identityId: - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - ... -``` - - - - - The Azure machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within an Azure environment. - - - - You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about Azure machine identities here](/documentation/platform/identities/azure-auth). - - - Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. In the `authentication.azureAuth.identityId` field, add the identity ID of the machine identity you created. See the example below for more details. - - - - - - Make sure to also populate the `secretsScope` field with the project slug - _`projectSlug`_, environment slug _`envSlug`_, and secrets path - _`secretsPath`_ that you want to fetch secrets from. Please see the example - below. - - -## Example - -```yaml example-azure-auth.yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample-crd -spec: - authentication: - azureAuth: - identityId: - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - ... -``` - - - - - The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments. - - - - You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about GCP machine identities here](/documentation/platform/identities/gcp-auth). - - - Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. In the `authentication.gcpIdTokenAuth.identityId` field, add the identity ID of the machine identity you created. See the example below for more details. - - - - - - Make sure to also populate the `secretsScope` field with the project slug - _`projectSlug`_, environment slug _`envSlug`_, and secrets path - _`secretsPath`_ that you want to fetch secrets from. Please see the example - below. - - -## Example - -```yaml example-gcp-id-token-auth.yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample-crd -spec: - authentication: - gcpIdTokenAuth: - identityId: - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - ... -``` - - - - - The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments. - - - - You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about GCP machine identities here](/documentation/platform/identities/gcp-auth). - - - Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. In the `authentication.gcpIamAuth.identityId` field, add the identity ID of the machine identity you created. - You'll also need to add the service account key file path to your InfisicalSecret resource. In the `authentication.gcpIamAuth.serviceAccountKeyFilePath` field, add the path to your service account key file path. Please see the example below for more details. - - - - - - Make sure to also populate the `secretsScope` field with the project slug - _`projectSlug`_, environment slug _`envSlug`_, and secrets path - _`secretsPath`_ that you want to fetch secrets from. Please see the example - below. - - -## Example - -```yaml example-gcp-id-token-auth.yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample-crd -spec: - authentication: - gcpIamAuth: - identityId: - serviceAccountKeyFilePath: "/path/to-service-account-key-file-path.json" - - # secretsScope is identical to the secrets scope in the universalAuth field in this sample. - secretsScope: - projectSlug: your-project-slug - envSlug: prod - secretsPath: "/path" - recursive: true - ... -``` - - - - - -The service token required to authenticate with Infisical needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores this service token. -Follow the instructions below to create and store the service token in a Kubernetes secrets and reference it in your CRD. - -#### 1. Generate service token - -You can generate a [service token](../../documentation/platform/token) for an Infisical project by heading over to the Infisical dashboard then to Project Settings. - -#### 2. Create Kubernetes secret containing service token - -Once you have generated the service token, you will need to create a Kubernetes secret containing the service token you generated. -To quickly create a Kubernetes secret containing the generated service token, you can run the command below. Make sure you replace `` with your service token. - -```bash -kubectl create secret generic service-token --from-literal=infisicalToken="" -``` - -#### 3. Add reference for the Kubernetes secret containing service token - -Once the secret is created, add the name and namespace of the secret that was just created under `authentication.serviceToken.serviceTokenSecretReference` field in the InfisicalSecret resource. - -{" "} - - - Make sure to also populate the `secretsScope` field with the, environment slug - _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets - from. Please see the example below. - - -## Example - -```yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample-crd -spec: - authentication: - serviceToken: - serviceTokenSecretReference: - secretName: service-token # <-- name of the Kubernetes secret that stores our service token - secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token - secretsScope: - envSlug: # "dev", "staging", "prod", etc.. - secretsPath: # Root is "/" - ... -``` - - - - -The `managedSecretReference` field is used to define the target location for storing secrets retrieved from an Infisical project. -This field requires specifying both the name and namespace of the Kubernetes secret that will hold these secrets. -The Infisical operator will automatically create the Kubernetes secret with the specified name/namespace and keep it continuously updated. - -Note: The managed secret be should be created in the same namespace as the deployment that will use it. - - - -The name of the managed Kubernetes secret to be created - - -The namespace of the managed Kubernetes secret to be created. - - -Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets. - - -Templates enable you to transform data from Infisical before storing it as a Kubernetes Secret. - - -When set to true, this option injects all secrets retrieved from Infisical into your configuration. -Secrets defined in the template will override the automatically injected secrets. - - -Define secret keys and their corresponding templates. -Each data value uses a Golang template with access to all secrets retrieved from the specified scope. - -Secrets are structured as follows: - -```golang -type TemplateSecret struct { - Value string `json:"value"` - SecretPath string `json:"secretPath"` -} -``` - -#### Example template configuration: - -```golang - managedSecretReference: - secretName: managed-secret - secretNamespace: default - template: - includeAllSecrets: true - data: - NEW_KEY: "{{ .KEY1.SecretPath }} {{ .KEY1.Value }}" -``` - -When you run the following command: - -```bash -kubectl get secret managed-secret -o jsonpath='{.data}' -``` - -You'll receive Kubernetes secrets output that includes the NEW_KEY: - -```bash -{... "KEY":"d29ybGQ=","NEW_KEY":"LyBoZWxsbw=="} -``` - -When you set `includeAllSecrets` as `false` the Kubernetes secrets outputs will be: - -```bash -{"NEW_KEY":"LyBoZWxsbw=="} -``` - - - -Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. -This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. - -#### Available options - -- `Orphan` (default) -- `Owner` - - - When creation policy is set to `Owner`, the `InfisicalSecret` CRD must be in - the same namespace as where the managed kubernetes secret. - - - - -### Propagating labels & annotations - -The operator will transfer all labels & annotations present on the `InfisicalSecret` CRD to the managed Kubernetes secret to be created. -Thus, if a specific label is required on the resulting secret, it can be applied as demonstrated in the following example: - - -```yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample - labels: - label-to-be-passed-to-managed-secret: sample-value - annotations: - example.com/annotation-to-be-passed-to-managed-secret: "sample-value" -spec: - .. - authentication: - ... - managedSecretReference: - ... -``` - -This would result in the following managed secret to be created: - -```yaml -apiVersion: v1 -data: ... -kind: Secret -metadata: - annotations: - example.com/annotation-to-be-passed-to-managed-secret: sample-value - secrets.infisical.com/version: W/"3f1-ZyOSsrCLGSkAhhCkY2USPu2ivRw" - labels: - label-to-be-passed-to-managed-secret: sample-value - name: managed-token - namespace: default -type: Opaque -``` - - - -### Apply the InfisicalSecret CRD to your cluster - -Once you have configured the InfisicalSecret CRD with the required fields, you can apply it to your cluster. -After applying, you should notice that the managed secret has been created in the desired namespace your specified. - -``` -kubectl apply -f example-infisical-secret-crd.yaml -``` - -### Verify managed secret creation - -To verify that the operator has successfully created the managed secret, you can check the secrets in the namespace that was specified. - -```bash -# Verify managed secret is created -kubectl get secrets -n -``` - - - The Infisical secrets will be synced and stored into the managed secret every - 1 minutes. - - -### Using managed secret in your deployment - -Incorporating the managed secret created by the operator into your deployment can be achieved through several methods. -Here, we will highlight three of the most common ways to utilize it. Learn more about Kubernetes secrets [here](https://kubernetes.io/docs/concepts/configuration/secret/) - - - This will take all the secrets from your managed secret and expose them to your container - -````yaml - envFrom: - - secretRef: - name: managed-secret # managed secret name - ``` - - Example usage in a deployment - ```yaml - apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx -spec: - replicas: 1 - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - envFrom: - - secretRef: - name: managed-secret # <- name of managed secret - ports: - - containerPort: 80 -```` - - - - - This will allow you to select individual secrets by key name from your managed secret and expose them to your container - - ```yaml - env: - - name: SECRET_NAME # The environment variable's name which is made available in the container - valueFrom: - secretKeyRef: - name: managed-secret # managed secret name - key: SOME_SECRET_KEY # The name of the key which exists in the managed secret - ``` - -Example usage in a deployment - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: -name: nginx-deployment -labels: -app: nginx -spec: -replicas: 1 -selector: -matchLabels: -app: nginx -template: -metadata: -labels: -app: nginx -spec: -containers: - name: nginx -image: nginx:1.14.2 -env: - name: STRIPE_API_SECRET -valueFrom: -secretKeyRef: -name: managed-secret # <- name of managed secret -key: STRIPE_API_SECRET -ports: - containerPort: 80 - -``` - - - - -This will allow you to create a volume on your container which comprises of files holding the secrets in your managed kubernetes secret -```yaml -volumes: - - name: secrets-volume-name # The name of the volume under which secrets will be stored - secret: - secretName: managed-secret # managed secret name -```` - -You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets - -```yaml -volumeMounts: - - name: secrets-volume-name - mountPath: /etc/secrets - readOnly: true -``` - -Example usage in a deployment - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx -spec: - replicas: 1 - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - volumeMounts: - - name: secrets-volume-name - mountPath: /etc/secrets - readOnly: true - ports: - - containerPort: 80 - volumes: - - name: secrets-volume-name - secret: - secretName: managed-secret # <- managed secrets -``` - - - -The definition file of the Kubernetes secret for the CA certificate can be structured like the following: - -```yaml -apiVersion: v1 -kind: Secret -metadata: - name: custom-ca-certificate -type: Opaque -stringData: - ca.crt: | - -----BEGIN CERTIFICATE----- - MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL - ... - BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz - -----END CERTIFICATE----- -``` - -### Auto redeployment - -Deployments using managed secrets don't reload automatically on updates, so they may use outdated secrets unless manually redeployed. -To address this, we added functionality to automatically redeploy your deployment when its managed secret updates. - -#### Enabling auto redeploy - -To enable auto redeployment you simply have to add the following annotation to the deployment that consumes a managed secret - -```yaml -secrets.infisical.com/auto-reload: "true" -``` - - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx - annotations: - secrets.infisical.com/auto-reload: "true" # <- redeployment annotation -spec: - replicas: 1 - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - envFrom: - - secretRef: - name: managed-secret - ports: - - containerPort: 80 -``` - - - #### How it works - When a secret change occurs, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update. - Then, for each deployment that has this annotation present, a rolling update will be triggered. - - -## Push Secrets to Infisical - - -### Example usage - -Below is a sample InfisicalPushSecret CRD that pushes secrets defined in a Kubernetes secret to Infisical. - -After filling out the fields in the InfisicalPushSecret CRD, you can apply it directly to your cluster. - -Before applying the InfisicalPushSecret CRD, you need to create a Kubernetes secret containing the secrets you want to push to Infisical. An example can be seen below the InfisicalPushSecret CRD. - -```yaml infisical-push-secret.yaml - apiVersion: secrets.infisical.com/v1alpha1 - kind: InfisicalPushSecret - metadata: - name: infisical-push-secret-demo - spec: - resyncInterval: 1m - hostAPI: https://app.infisical.com/api - - # Optional, defaults to no replacement. - updatePolicy: Replace # If set to replace, existing secrets inside Infisical will be replaced by the value of the PushSecret on sync. - - # Optional, defaults to no deletion. - deletionPolicy: Delete # If set to delete, the secret(s) inside Infisical managed by the operator, will be deleted if the InfisicalPushSecret CRD is deleted. - - destination: - projectId: - environmentSlug: - secretsPath: - - push: - secret: - secretName: push-secret-demo # Secret CRD - secretNamespace: default - - # Only have one authentication method defined or you are likely to run into authentication issues. - # Remove all except one authentication method. - authentication: - awsIamAuth: - identityId: - azureAuth: - identityId: - gcpIamAuth: - identityId: - serviceAccountKeyFilePath: - gcpIdTokenAuth: - identityId: - kubernetesAuth: - identityId: - serviceAccountRef: - name: - namespace: - universalAuth: - credentialsRef: - secretName: # universal-auth-credentials - secretNamespace: # default -``` - -```yaml source-secret.yaml - apiVersion: v1 - kind: Secret - metadata: - name: push-secret-demo - namespace: default - stringData: # can also be "data", but needs to be base64 encoded - API_KEY: some-api-key - DATABASE_URL: postgres://127.0.0.1:5432 - ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab -``` - -```bash - kubectl apply -f source-secret.yaml -``` - -After applying the soruce-secret.yaml file, you are ready to apply the InfisicalPushSecret CRD. - -```bash - kubectl apply -f infisical-push-secret.yaml -``` - -After applying the InfisicalPushSecret CRD, you should notice that the secrets you have defined in your source-secret.yaml file have been pushed to your specified destination in Infisical. - - -### InfisicalPushSecret CRD properties - - - If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to - ` https://your-self-hosted-instace.com/api` - - When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud. - - - If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet. - To achieve this, use the following address for the hostAPI field: - - ``` bash - http://..svc.cluster.local:4000/api - ``` - - Make sure to replace `` and `` with the appropriate values for your backend service and namespace. - - - - - - - The `resyncInterval` is a string-formatted duration that defines the time between each resync. - - The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time. - - The following units are supported: - - `s` for seconds (must be at least 5 seconds) - - `m` for minutes - - `h` for hours - - `d` for days - - `w` for weeks - - The default value is `1m` (1 minute). - - Valid intervals examples: - ```yaml - resyncInterval: 5s # 10 seconds - resyncInterval: 10s # 10 seconds - resyncInterval: 5m # 5 minutes - resyncInterval: 1h # 1 hour - resyncInterval: 1d # 1 day - ``` - - - - - The field is optional and will default to `None` if not defined. - - The update policy defines how the operator should handle conflicting secrets when pushing secrets to Infisical. - - Valid values are `None` and `Replace`. - - Behavior of each policy: - - `None`: The operator will not override existing secrets in Infisical. If a secret with the same key already exists, the operator will skip pushing that secret, and the secret will not be managed by the operator. - - `Replace`: The operator will replace existing secrets in Infisical with the new secrets. If a secret with the same key already exists, the operator will update the secret with the new value. - - ```yaml - spec: - updatePolicy: Replace - ``` - - - - - This field is optional and will default to `None` if not defined. - - The deletion policy defines what the operator should do in case the InfisicalPushSecret CRD is deleted. - - Valid values are `None` and `Delete`. - - Behavior of each policy: - - `None`: The operator will not delete the secrets in Infisical when the InfisicalPushSecret CRD is deleted. - - `Delete`: The operator will delete the secrets in Infisical that are managed by the operator when the InfisicalPushSecret CRD is deleted. - - ```yaml - spec: - deletionPolicy: Delete - ``` - - - - The `destination` field is used to specify where you want to create the secrets in Infisical. The required fields are `projectId`, `environmentSlug`, and `secretsPath`. - - ```yaml - spec: - destination: - projectId: - environmentSlug: - secretsPath: - ``` - - - The project ID where you want to create the secrets in Infisical. - - - - The environment slug where you want to create the secrets in Infisical. - - - - The path where you want to create the secrets in Infisical. The root path is `/`. - - - - - - The `push` field is used to define what you want to push to Infisical. Currently the operator only supports pushing Kubernetes secrets to Infisical. An example of the `push` field is shown below. - - - - - The `secret` field is used to define the Kubernetes secret you want to push to Infisical. The required fields are `secretName` and `secretNamespace`. - - - - Example usage of the `push.secret` field: - - ```yaml infisical-push-secret.yaml - push: - secret: - secretName: push-secret-demo - secretNamespace: default - ``` - - ```yaml push-secret-demo.yaml - apiVersion: v1 - kind: Secret - metadata: - name: push-secret-demo - namespace: default - # Pass in the secrets you wish to push to Infisical - stringData: - API_KEY: some-api-key - DATABASE_URL: postgres://127.0.0.1:5432 - ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab - ``` - - - - - - - The `authentication` field dictates which authentication method to use when pushing secrets to Infisical. - The available authentication methods are `universalAuth`, `kubernetesAuth`, `awsIamAuth`, `azureAuth`, `gcpIdTokenAuth`, and `gcpIamAuth`. - - - - The universal authentication method is one of the easiest ways to get started with Infisical. Universal Auth works anywhere and is not tied to any specific cloud provider. - [Read more about Universal Auth](/documentation/platform/identities/universal-auth). - - Valid fields: - - `identityId`: The identity ID of the machine identity you created. - - `credentialsRef`: The name and namespace of the Kubernetes secret that stores the service token. - - `credentialsRef.secretName`: The name of the Kubernetes secret. - - `credentialsRef.secretNamespace`: The namespace of the Kubernetes secret. - - Example: - - ```yaml - # infisical-push-secret.yaml - spec: - universalAuth: - credentialsRef: - secretName: - secretNamespace: - ``` - - ```yaml - # machine-identity-credentials.yaml - apiVersion: v1 - kind: Secret - metadata: - name: universal-auth-credentials - type: Opaque - stringData: - clientId: - clientSecret: - ``` - - - - The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment. - [Read more about Kubernetes Auth](/documentation/platform/identities/kubernetes-auth). - Valid fields: - - `identityId`: The identity ID of the machine identity you created. - - `serviceAccountRef`: The name and namespace of the service account that will be used to authenticate with Infisical. - - `serviceAccountRef.name`: The name of the service account. - - `serviceAccountRef.namespace`: The namespace of the service account. - - Example: - - ```yaml - spec: - kubernetesAuth: - identityId: - serviceAccountRef: - name: - namespace: - ``` - - - - The AWS IAM machine identity authentication method is used to authenticate with Infisical. - [Read more about AWS IAM Auth](/documentation/platform/identities/aws-auth). - - Valid fields: - - `identityId`: The identity ID of the machine identity you created. - - Example: - - ```yaml - spec: - authentication: - awsIamAuth: - identityId: - ``` - - - - The AWS IAM machine identity authentication method is used to authenticate with Infisical. Azure Auth can only be used from within an Azure environment. - [Read more about Azure Auth](/documentation/platform/identities/azure-auth). - - Valid fields: - - `identityId`: The identity ID of the machine identity you created. - - Example: - - ```yaml - spec: - authentication: - azureAuth: - identityId: - ``` - - - The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments. - [Read more about Azure Auth](/documentation/platform/identities/gcp-auth). - - - Valid fields: - - `identityId`: The identity ID of the machine identity you created. - - `serviceAccountKeyFilePath`: The path to the GCP service account key file. - - Example: - - ```yaml - spec: - gcpIamAuth: - identityId: - serviceAccountKeyFilePath: - ``` - - - The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments. - [Read more about Azure Auth](/documentation/platform/identities/gcp-auth). - - Valid fields: - - `identityId`: The identity ID of the machine identity you created. - - Example: - - ```yaml - spec: - gcpIdTokenAuth: - identityId: - ``` - - - - - - - This block defines the TLS settings to use for connecting to the Infisical - instance. - - Fields: - - This block defines the reference to the CA certificate to use for connecting to the Infisical instance with SSL/TLS. - - Valid fields: - - `secretName`: The name of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS. - - `secretNamespace`: The namespace of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS. - - `key`: The name of the key in the Kubernetes secret which contains the value of the CA certificate to use for connecting to the Infisical instance with SSL/TLS. - - Example: - - ```yaml - tls: - caRef: - secretName: custom-ca-certificate - secretNamespace: default - key: ca.crt - ``` - - - - - -### Applying the InfisicalPushSecret CRD to your cluster - -Once you have configured the `InfisicalPushSecret` CRD with the required fields, you can apply it to your cluster. -After applying, you should notice that the secrets have been pushed to Infisical. - -```bash - kubectl apply -f source-push-secret.yaml # The secret that you're referencing in the InfisicalPushSecret CRD push.secret field - kubectl apply -f example-infisical-push-secret-crd.yaml # The InfisicalPushSecret CRD itself -``` - -## Sync Dynamic Secrets to your cluster - -### Example usage - -The example below demonstrates a sample InfisicalDynamicSecret CRD that creates a dynamic secret lease in Infisical, and syncs the lease to your Kubernetes cluster. - -```yaml dynamic-secret-crd.yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalDynamicSecret -metadata: - name: infisicaldynamicsecret -spec: - hostAPI: https://app.infisical.com/api # Optional, defaults to https://app.infisical.com/api - - dynamicSecret: - secretName: - projectId: - secretsPath: # Root directory is / - environmentSlug: - - # Lease revocation policy defines what should happen to leases created by the operator if the CRD is deleted. - # If set to "Revoke", leases will be revoked when the InfisicalDynamicSecret CRD is deleted. - leaseRevocationPolicy: Revoke - - # Lease TTL defines how long the lease should last for the dynamic secret. - # This value must be less than 1 day, and if a max TTL is defined on the dynamic secret, it must be below the max TTL. - leaseTTL: 1m - - # A reference to the secret that the dynamic secret lease should be stored in. - # If the secret doesn't exist, it will automatically be created. - managedSecretReference: - secretName: - secretNamespace: default # Must be the same namespace as the InfisicalDynamicSecret CRD. - creationPolicy: Orphan - - # Only have one authentication method defined or you are likely to run into authentication issues. - # Remove all except one authentication method. - authentication: - awsIamAuth: - identityId: - azureAuth: - identityId: - gcpIamAuth: - identityId: - serviceAccountKeyFilePath: - gcpIdTokenAuth: - identityId: - kubernetesAuth: - identityId: - serviceAccountRef: - name: - namespace: - universalAuth: - credentialsRef: - secretName: # universal-auth-credentials - secretNamespace: # default -``` - -Apply the InfisicalDynamicSecret CRD to your cluster. -```bash -kubectl apply -f dynamic-secret-crd.yaml -``` - -After applying the InfisicalDynamicSecret CRD, you should notice that the dynamic secret lease has been created in Infisical and synced to your Kubernetes cluster. You can verify that the lease has been created by doing: -```bash -kubectl get secret -o yaml -``` - -After getting the secret, you should should see that the secret has data that contains the lease credentials. -```yaml -apiVersion: v1 -data: - DB_PASSWORD: VHhETjZ4c2xsTXpOSWdPYW5LLlRyNEc2alVKYml6WiQjQS0tNTdodyREM3ZLZWtYSi4hTkdyS0F+TVFsLU9CSA== - DB_USERNAME: cHg4Z0dJTUVBcHdtTW1aYnV3ZWRsekJRRll6cW4wFEE= -kind: Secret -# ..... -``` - -### InfisicalDynamicSecret CRD properties - - - If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to - ` https://your-self-hosted-instace.com/api` - - When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud. - - - If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet. - To achieve this, use the following address for the hostAPI field: - - ``` bash - http://..svc.cluster.local:4000/api - ``` - - Make sure to replace `` and `` with the appropriate values for your backend service and namespace. - - - - - - The `leaseTTL` is a string-formatted duration that defines the time the lease should last for the dynamic secret. - - The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time. - - The following units are supported: - - `s` for seconds (must be at least 5 seconds) - - `m` for minutes - - `h` for hours - - `d` for days - - - The lease duration at most be 1 day (24 hours). And the TTL must be less than the max TTL defined on the dynamic secret. - - - - - The `managedSecretReference` field is used to define the Kubernetes secret where the dynamic secret lease should be stored. The required fields are `secretName` and `secretNamespace`. - - ```yaml - spec: - managedSecretReference: - secretName: - secretNamespace: default - ``` - - - The name of the Kubernetes secret where the dynamic secret lease should be stored. - - - - The namespace of the Kubernetes secret where the dynamic secret lease should be stored. - - - - Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. - This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. - - #### Available options - - `Orphan` (default) - - `Owner` - - - When creation policy is set to `Owner`, the `InfisicalSecret` CRD must be in - the same namespace as where the managed kubernetes secret. - - - This field is optional. - - - - Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets. - - This field is optional. - - - - - - - The field is optional and will default to `None` if not defined. - - The lease revocation policy defines what the operator should do with the leases created by the operator, when the InfisicalDynamicSecret CRD is deleted. - - Valid values are `None` and `Revoke`. - - Behavior of each policy: - - `None`: The operator will not override existing secrets in Infisical. If a secret with the same key already exists, the operator will skip pushing that secret, and the secret will not be managed by the operator. - - `Revoke`: The operator will revoke the leases created by the operator when the InfisicalDynamicSecret CRD is deleted. - - ```yaml - spec: - leaseRevocationPolicy: Revoke - ``` - - - - The `dynamicSecret` field is used to specify which dynamic secret to create leases for. The required fields are `secretName`, `projectId`, `secretsPath`, and `environmentSlug`. - - ```yaml - spec: - dynamicSecret: - secretName: - projectId: - environmentSlug: - secretsPath: - ``` - - - The name of the dynamic secret. - - - - The project ID of where the dynamic secret is stored in Infisical. - - - - The environment slug of where the dynamic secret is stored in Infisical. - - - - The path of where the dynamic secret is stored in Infisical. The root path is `/`. - - - - - - - The `authentication` field dictates which authentication method to use when pushing secrets to Infisical. - The available authentication methods are `universalAuth`, `kubernetesAuth`, `awsIamAuth`, `azureAuth`, `gcpIdTokenAuth`, and `gcpIamAuth`. - - - - The universal authentication method is one of the easiest ways to get started with Infisical. Universal Auth works anywhere and is not tied to any specific cloud provider. - [Read more about Universal Auth](/documentation/platform/identities/universal-auth). - - Valid fields: - - `identityId`: The identity ID of the machine identity you created. - - `credentialsRef`: The name and namespace of the Kubernetes secret that stores the service token. - - `credentialsRef.secretName`: The name of the Kubernetes secret. - - `credentialsRef.secretNamespace`: The namespace of the Kubernetes secret. - - Example: - - ```yaml - # infisical-push-secret.yaml - spec: - universalAuth: - credentialsRef: - secretName: - secretNamespace: - ``` - - ```yaml - # machine-identity-credentials.yaml - apiVersion: v1 - kind: Secret - metadata: - name: universal-auth-credentials - type: Opaque - stringData: - clientId: - clientSecret: - ``` - - - - The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment. - [Read more about Kubernetes Auth](/documentation/platform/identities/kubernetes-auth). - Valid fields: - - `identityId`: The identity ID of the machine identity you created. - - `serviceAccountRef`: The name and namespace of the service account that will be used to authenticate with Infisical. - - `serviceAccountRef.name`: The name of the service account. - - `serviceAccountRef.namespace`: The namespace of the service account. - - Example: - - ```yaml - spec: - kubernetesAuth: - identityId: - serviceAccountRef: - name: - namespace: - ``` - - - - The AWS IAM machine identity authentication method is used to authenticate with Infisical. - [Read more about AWS IAM Auth](/documentation/platform/identities/aws-auth). - - Valid fields: - - `identityId`: The identity ID of the machine identity you created. - - Example: - - ```yaml - spec: - authentication: - awsIamAuth: - identityId: - ``` - - - - The AWS IAM machine identity authentication method is used to authenticate with Infisical. Azure Auth can only be used from within an Azure environment. - [Read more about Azure Auth](/documentation/platform/identities/azure-auth). - - Valid fields: - - `identityId`: The identity ID of the machine identity you created. - - Example: - - ```yaml - spec: - authentication: - azureAuth: - identityId: - ``` - - - The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments. - [Read more about Azure Auth](/documentation/platform/identities/gcp-auth). - - - Valid fields: - - `identityId`: The identity ID of the machine identity you created. - - `serviceAccountKeyFilePath`: The path to the GCP service account key file. - - Example: - - ```yaml - spec: - gcpIamAuth: - identityId: - serviceAccountKeyFilePath: - ``` - - - The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments. - [Read more about Azure Auth](/documentation/platform/identities/gcp-auth). - - Valid fields: - - `identityId`: The identity ID of the machine identity you created. - - Example: - - ```yaml - spec: - gcpIdTokenAuth: - identityId: - ``` - - - - - - - This block defines the TLS settings to use for connecting to the Infisical - instance. - - Fields: - - This block defines the reference to the CA certificate to use for connecting to the Infisical instance with SSL/TLS. - - Valid fields: - - `secretName`: The name of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS. - - `secretNamespace`: The namespace of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS. - - `key`: The name of the key in the Kubernetes secret which contains the value of the CA certificate to use for connecting to the Infisical instance with SSL/TLS. - - Example: - - ```yaml - tls: - caRef: - secretName: custom-ca-certificate - secretNamespace: default - key: ca.crt - ``` - - - - - -### Applying the InfisicalDynamicSecret CRD to your cluster - -Once you have configured the `InfisicalDynamicSecret` CRD with the required fields, you can apply it to your cluster. After applying, you should notice that a lease has been created in Infisical and synced to your Kubernetes cluster. - -```bash -kubectl apply -f dynamic-secret-crd.yaml -``` - -### Auto redeployment - -Deployments using managed secrets don't reload automatically on updates, so they may use outdated secrets unless manually redeployed. -To address this, we added functionality to automatically redeploy your deployment when its managed secret updates. - -#### Enabling auto redeploy - -To enable auto redeployment you simply have to add the following annotation to the deployment that consumes a managed secret - -```yaml -secrets.infisical.com/auto-reload: "true" -``` - - -```yaml -apiVersion: apps/v1 -kind: Deployment -metadata: - name: nginx-deployment - labels: - app: nginx - annotations: - secrets.infisical.com/auto-reload: "true" # <- redeployment annotation -spec: - replicas: 1 - selector: - matchLabels: - app: nginx - template: - metadata: - labels: - app: nginx - spec: - containers: - - name: nginx - image: nginx:1.14.2 - envFrom: - - secretRef: - name: managed-secret # The name of your managed secret, the same that you're using in your InfisicalDynamicSecret CRD (spec.managedSecretReference.secretName) - ports: - - containerPort: 80 -``` - - - #### How it works - When the lease changes, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update. - Then, for each deployment that has this annotation present, a rolling update will be triggered. A redeployment won't happen if the lease is renewed, only if it's recreated. - - - -## Connecting to instances with private/self-signed certificate - -To connect to Infisical instances behind a private/self-signed certificate, you can configure the TLS settings in the CRD -to point to a CA certificate stored in a Kubernetes secret resource. - -```yaml ---- -spec: - hostAPI: https://app.infisical.com/api - tls: - caRef: - secretName: custom-ca-certificate - secretNamespace: default - key: ca.crt ---- -``` - - -## Global configuration - -To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap. -For example, you can configure all `InfisicalSecret` instances to fetch secrets from a single backend API without specifying the `hostAPI` parameter for each instance. - -### Available global properties - -| Property | Description | Default value | -| -------- | --------------------------------------------------------------------------------- | ----------------------------- | -| hostAPI | If `hostAPI` in `InfisicalSecret` instance is left empty, this value will be used | https://app.infisical.com/api | - -### Applying global configurations - -All global configurations must reside in a Kubernetes ConfigMap named `infisical-config` in the namespace `infisical-operator-system`. -To apply global configuration to the operator, copy the following yaml into `infisical-config.yaml` file. - -```yaml infisical-config.yaml -apiVersion: v1 -kind: Namespace -metadata: - name: infisical-operator-system ---- -apiVersion: v1 -kind: ConfigMap -metadata: - name: infisical-config - namespace: infisical-operator-system -data: - hostAPI: https://example.com/api # <-- global hostAPI -``` - -Then apply this change via kubectl by running the following - -```bash -kubectl apply -f infisical-config.yaml -``` - -## Troubleshoot operator - -If the operator is unable to fetch secrets from the API, it will not affect the managed Kubernetes secret. -It will continue attempting to reconnect to the API indefinitely. -The InfisicalSecret resource uses the `status.conditions` field to report its current state and any errors encountered. - -```yaml -$ kubectl get infisicalSecrets -NAME AGE -infisicalsecret-sample 12s - -$ kubectl describe infisicalSecret infisicalsecret-sample -... -Spec: -... -Status: - Conditions: - Last Transition Time: 2022-12-18T04:29:09Z - Message: Infisical controller has located the Infisical token in provided Kubernetes secret - Reason: OK - Status: True - Type: secrets.infisical.com/LoadedInfisicalToken - Last Transition Time: 2022-12-18T04:29:10Z - Message: Failed to update secret because: 400 Bad Request - Reason: Error - Status: False - Type: secrets.infisical.com/ReadyToSyncSecrets -Events: -``` - -## Uninstall Operator - -The managed secret created by the operator will not be deleted when the operator is uninstalled. - - - - Install Infisical Helm repository - ```bash - helm uninstall - ``` - - - ``` - kubectl delete -f https://raw.githubusercontent.com/Infisical/infisical/main/k8-operator/kubectl-install/install-secrets-operator.yaml - ``` - - - -## Useful Articles - -- [Managing secrets in OpenShift with Infisical](https://xphyr.net/post/infisical_ocp/) diff --git a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx new file mode 100644 index 000000000..52df2ceb0 --- /dev/null +++ b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx @@ -0,0 +1,425 @@ +--- +sidebarTitle: "InfisicalDynamicSecret CRD" +title: "Using the InfisicalDynamicSecret CRD" +description: "Learn how to use the InfisicalDynamicSecret CRD to create dynamic secret leases in Infisical and sync them to your Kubernetes cluster." +--- + +## Sync Dynamic Secrets to your cluster + +### Example usage + +The example below demonstrates a sample InfisicalDynamicSecret CRD that creates a dynamic secret lease in Infisical, and syncs the lease to your Kubernetes cluster. + +```yaml dynamic-secret-crd.yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalDynamicSecret +metadata: + name: infisicaldynamicsecret +spec: + hostAPI: https://app.infisical.com/api # Optional, defaults to https://app.infisical.com/api + + dynamicSecret: + secretName: + projectId: + secretsPath: # Root directory is / + environmentSlug: + + # Lease revocation policy defines what should happen to leases created by the operator if the CRD is deleted. + # If set to "Revoke", leases will be revoked when the InfisicalDynamicSecret CRD is deleted. + leaseRevocationPolicy: Revoke + + # Lease TTL defines how long the lease should last for the dynamic secret. + # This value must be less than 1 day, and if a max TTL is defined on the dynamic secret, it must be below the max TTL. + leaseTTL: 1m + + # A reference to the secret that the dynamic secret lease should be stored in. + # If the secret doesn't exist, it will automatically be created. + managedSecretReference: + secretName: + secretNamespace: default # Must be the same namespace as the InfisicalDynamicSecret CRD. + creationPolicy: Orphan + + # Only have one authentication method defined or you are likely to run into authentication issues. + # Remove all except one authentication method. + authentication: + awsIamAuth: + identityId: + azureAuth: + identityId: + gcpIamAuth: + identityId: + serviceAccountKeyFilePath: + gcpIdTokenAuth: + identityId: + kubernetesAuth: + identityId: + serviceAccountRef: + name: + namespace: + universalAuth: + credentialsRef: + secretName: # universal-auth-credentials + secretNamespace: # default +``` + +Apply the InfisicalDynamicSecret CRD to your cluster. +```bash +kubectl apply -f dynamic-secret-crd.yaml +``` + +After applying the InfisicalDynamicSecret CRD, you should notice that the dynamic secret lease has been created in Infisical and synced to your Kubernetes cluster. You can verify that the lease has been created by doing: +```bash +kubectl get secret -o yaml +``` + +After getting the secret, you should should see that the secret has data that contains the lease credentials. +```yaml +apiVersion: v1 +data: + DB_PASSWORD: VHhETjZ4c2xsTXpOSWdPYW5LLlRyNEc2alVKYml6WiQjQS0tNTdodyREM3ZLZWtYSi4hTkdyS0F+TVFsLU9CSA== + DB_USERNAME: cHg4Z0dJTUVBcHdtTW1aYnV3ZWRsekJRRll6cW4wFEE= +kind: Secret +# ..... +``` + +### InfisicalDynamicSecret CRD properties + + + If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to + ` https://your-self-hosted-instace.com/api` + + When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud. + + + If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet. + To achieve this, use the following address for the hostAPI field: + + ``` bash + http://..svc.cluster.local:4000/api + ``` + + Make sure to replace `` and `` with the appropriate values for your backend service and namespace. + + + + + + The `leaseTTL` is a string-formatted duration that defines the time the lease should last for the dynamic secret. + + The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time. + + The following units are supported: + - `s` for seconds (must be at least 5 seconds) + - `m` for minutes + - `h` for hours + - `d` for days + + + The lease duration at most be 1 day (24 hours). And the TTL must be less than the max TTL defined on the dynamic secret. + + + + + The `managedSecretReference` field is used to define the Kubernetes secret where the dynamic secret lease should be stored. The required fields are `secretName` and `secretNamespace`. + + ```yaml + spec: + managedSecretReference: + secretName: + secretNamespace: default + ``` + + + The name of the Kubernetes secret where the dynamic secret lease should be stored. + + + + The namespace of the Kubernetes secret where the dynamic secret lease should be stored. + + + + Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. + This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. + + #### Available options + - `Orphan` (default) + - `Owner` + + + When creation policy is set to `Owner`, the `InfisicalSecret` CRD must be in + the same namespace as where the managed kubernetes secret. + + + This field is optional. + + + + Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets. + + This field is optional. + + + + + + + The field is optional and will default to `None` if not defined. + + The lease revocation policy defines what the operator should do with the leases created by the operator, when the InfisicalDynamicSecret CRD is deleted. + + Valid values are `None` and `Revoke`. + + Behavior of each policy: + - `None`: The operator will not override existing secrets in Infisical. If a secret with the same key already exists, the operator will skip pushing that secret, and the secret will not be managed by the operator. + - `Revoke`: The operator will revoke the leases created by the operator when the InfisicalDynamicSecret CRD is deleted. + + ```yaml + spec: + leaseRevocationPolicy: Revoke + ``` + + + + The `dynamicSecret` field is used to specify which dynamic secret to create leases for. The required fields are `secretName`, `projectId`, `secretsPath`, and `environmentSlug`. + + ```yaml + spec: + dynamicSecret: + secretName: + projectId: + environmentSlug: + secretsPath: + ``` + + + The name of the dynamic secret. + + + + The project ID of where the dynamic secret is stored in Infisical. + + + + The environment slug of where the dynamic secret is stored in Infisical. + + + + The path of where the dynamic secret is stored in Infisical. The root path is `/`. + + + + + + + The `authentication` field dictates which authentication method to use when pushing secrets to Infisical. + The available authentication methods are `universalAuth`, `kubernetesAuth`, `awsIamAuth`, `azureAuth`, `gcpIdTokenAuth`, and `gcpIamAuth`. + + + + The universal authentication method is one of the easiest ways to get started with Infisical. Universal Auth works anywhere and is not tied to any specific cloud provider. + [Read more about Universal Auth](/documentation/platform/identities/universal-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `credentialsRef`: The name and namespace of the Kubernetes secret that stores the service token. + - `credentialsRef.secretName`: The name of the Kubernetes secret. + - `credentialsRef.secretNamespace`: The namespace of the Kubernetes secret. + + Example: + + ```yaml + # infisical-push-secret.yaml + spec: + universalAuth: + credentialsRef: + secretName: + secretNamespace: + ``` + + ```yaml + # machine-identity-credentials.yaml + apiVersion: v1 + kind: Secret + metadata: + name: universal-auth-credentials + type: Opaque + stringData: + clientId: + clientSecret: + ``` + + + + The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment. + [Read more about Kubernetes Auth](/documentation/platform/identities/kubernetes-auth). + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `serviceAccountRef`: The name and namespace of the service account that will be used to authenticate with Infisical. + - `serviceAccountRef.name`: The name of the service account. + - `serviceAccountRef.namespace`: The namespace of the service account. + + Example: + + ```yaml + spec: + kubernetesAuth: + identityId: + serviceAccountRef: + name: + namespace: + ``` + + + + The AWS IAM machine identity authentication method is used to authenticate with Infisical. + [Read more about AWS IAM Auth](/documentation/platform/identities/aws-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + + Example: + + ```yaml + spec: + authentication: + awsIamAuth: + identityId: + ``` + + + + The AWS IAM machine identity authentication method is used to authenticate with Infisical. Azure Auth can only be used from within an Azure environment. + [Read more about Azure Auth](/documentation/platform/identities/azure-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + + Example: + + ```yaml + spec: + authentication: + azureAuth: + identityId: + ``` + + + The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments. + [Read more about Azure Auth](/documentation/platform/identities/gcp-auth). + + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `serviceAccountKeyFilePath`: The path to the GCP service account key file. + + Example: + + ```yaml + spec: + gcpIamAuth: + identityId: + serviceAccountKeyFilePath: + ``` + + + The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments. + [Read more about Azure Auth](/documentation/platform/identities/gcp-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + + Example: + + ```yaml + spec: + gcpIdTokenAuth: + identityId: + ``` + + + + + + + This block defines the TLS settings to use for connecting to the Infisical + instance. + + Fields: + + This block defines the reference to the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + + Valid fields: + - `secretName`: The name of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + - `secretNamespace`: The namespace of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + - `key`: The name of the key in the Kubernetes secret which contains the value of the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + + Example: + + ```yaml + tls: + caRef: + secretName: custom-ca-certificate + secretNamespace: default + key: ca.crt + ``` + + + + + +### Applying the InfisicalDynamicSecret CRD to your cluster + +Once you have configured the `InfisicalDynamicSecret` CRD with the required fields, you can apply it to your cluster. After applying, you should notice that a lease has been created in Infisical and synced to your Kubernetes cluster. + +```bash +kubectl apply -f dynamic-secret-crd.yaml +``` + +### Auto redeployment + +Deployments using managed secrets don't reload automatically on updates, so they may use outdated secrets unless manually redeployed. +To address this, we added functionality to automatically redeploy your deployment when its managed secret updates. + +#### Enabling auto redeploy + +To enable auto redeployment you simply have to add the following annotation to the deployment that consumes a managed secret + +```yaml +secrets.infisical.com/auto-reload: "true" +``` + + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx + annotations: + secrets.infisical.com/auto-reload: "true" # <- redeployment annotation +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + envFrom: + - secretRef: + name: managed-secret # The name of your managed secret, the same that you're using in your InfisicalDynamicSecret CRD (spec.managedSecretReference.secretName) + ports: + - containerPort: 80 +``` + + + #### How it works + When the lease changes, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update. + Then, for each deployment that has this annotation present, a rolling update will be triggered. A redeployment won't happen if the lease is renewed, only if it's recreated. + diff --git a/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx new file mode 100644 index 000000000..eb226e7b8 --- /dev/null +++ b/docs/integrations/platforms/kubernetes/infisical-push-secret-crd.mdx @@ -0,0 +1,400 @@ +--- +sidebarTitle: "InfisicalPushSecret CRD" +title: "Using the InfisicalPushSecret CRD" +description: "Learn how to use the InfisicalPushSecret CRD to push and manage secrets in Infisical." +--- + + +## Push Secrets to Infisical + + +### Example usage + +Below is a sample InfisicalPushSecret CRD that pushes secrets defined in a Kubernetes secret to Infisical. + +After filling out the fields in the InfisicalPushSecret CRD, you can apply it directly to your cluster. + +Before applying the InfisicalPushSecret CRD, you need to create a Kubernetes secret containing the secrets you want to push to Infisical. An example can be seen below the InfisicalPushSecret CRD. + +```yaml infisical-push-secret.yaml + apiVersion: secrets.infisical.com/v1alpha1 + kind: InfisicalPushSecret + metadata: + name: infisical-push-secret-demo + spec: + resyncInterval: 1m + hostAPI: https://app.infisical.com/api + + # Optional, defaults to no replacement. + updatePolicy: Replace # If set to replace, existing secrets inside Infisical will be replaced by the value of the PushSecret on sync. + + # Optional, defaults to no deletion. + deletionPolicy: Delete # If set to delete, the secret(s) inside Infisical managed by the operator, will be deleted if the InfisicalPushSecret CRD is deleted. + + destination: + projectId: + environmentSlug: + secretsPath: + + push: + secret: + secretName: push-secret-demo # Secret CRD + secretNamespace: default + + # Only have one authentication method defined or you are likely to run into authentication issues. + # Remove all except one authentication method. + authentication: + awsIamAuth: + identityId: + azureAuth: + identityId: + gcpIamAuth: + identityId: + serviceAccountKeyFilePath: + gcpIdTokenAuth: + identityId: + kubernetesAuth: + identityId: + serviceAccountRef: + name: + namespace: + universalAuth: + credentialsRef: + secretName: # universal-auth-credentials + secretNamespace: # default +``` + +```yaml source-secret.yaml + apiVersion: v1 + kind: Secret + metadata: + name: push-secret-demo + namespace: default + stringData: # can also be "data", but needs to be base64 encoded + API_KEY: some-api-key + DATABASE_URL: postgres://127.0.0.1:5432 + ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab +``` + +```bash + kubectl apply -f source-secret.yaml +``` + +After applying the soruce-secret.yaml file, you are ready to apply the InfisicalPushSecret CRD. + +```bash + kubectl apply -f infisical-push-secret.yaml +``` + +After applying the InfisicalPushSecret CRD, you should notice that the secrets you have defined in your source-secret.yaml file have been pushed to your specified destination in Infisical. + + +### InfisicalPushSecret CRD properties + + + If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to + ` https://your-self-hosted-instace.com/api` + + When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud. + + + If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet. + To achieve this, use the following address for the hostAPI field: + + ``` bash + http://..svc.cluster.local:4000/api + ``` + + Make sure to replace `` and `` with the appropriate values for your backend service and namespace. + + + + + + + The `resyncInterval` is a string-formatted duration that defines the time between each resync. + + The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time. + + The following units are supported: + - `s` for seconds (must be at least 5 seconds) + - `m` for minutes + - `h` for hours + - `d` for days + - `w` for weeks + + The default value is `1m` (1 minute). + + Valid intervals examples: + ```yaml + resyncInterval: 5s # 10 seconds + resyncInterval: 10s # 10 seconds + resyncInterval: 5m # 5 minutes + resyncInterval: 1h # 1 hour + resyncInterval: 1d # 1 day + ``` + + + + + The field is optional and will default to `None` if not defined. + + The update policy defines how the operator should handle conflicting secrets when pushing secrets to Infisical. + + Valid values are `None` and `Replace`. + + Behavior of each policy: + - `None`: The operator will not override existing secrets in Infisical. If a secret with the same key already exists, the operator will skip pushing that secret, and the secret will not be managed by the operator. + - `Replace`: The operator will replace existing secrets in Infisical with the new secrets. If a secret with the same key already exists, the operator will update the secret with the new value. + + ```yaml + spec: + updatePolicy: Replace + ``` + + + + + This field is optional and will default to `None` if not defined. + + The deletion policy defines what the operator should do in case the InfisicalPushSecret CRD is deleted. + + Valid values are `None` and `Delete`. + + Behavior of each policy: + - `None`: The operator will not delete the secrets in Infisical when the InfisicalPushSecret CRD is deleted. + - `Delete`: The operator will delete the secrets in Infisical that are managed by the operator when the InfisicalPushSecret CRD is deleted. + + ```yaml + spec: + deletionPolicy: Delete + ``` + + + + The `destination` field is used to specify where you want to create the secrets in Infisical. The required fields are `projectId`, `environmentSlug`, and `secretsPath`. + + ```yaml + spec: + destination: + projectId: + environmentSlug: + secretsPath: + ``` + + + The project ID where you want to create the secrets in Infisical. + + + + The environment slug where you want to create the secrets in Infisical. + + + + The path where you want to create the secrets in Infisical. The root path is `/`. + + + + + + The `push` field is used to define what you want to push to Infisical. Currently the operator only supports pushing Kubernetes secrets to Infisical. An example of the `push` field is shown below. + + + + + The `secret` field is used to define the Kubernetes secret you want to push to Infisical. The required fields are `secretName` and `secretNamespace`. + + + + Example usage of the `push.secret` field: + + ```yaml infisical-push-secret.yaml + push: + secret: + secretName: push-secret-demo + secretNamespace: default + ``` + + ```yaml push-secret-demo.yaml + apiVersion: v1 + kind: Secret + metadata: + name: push-secret-demo + namespace: default + # Pass in the secrets you wish to push to Infisical + stringData: + API_KEY: some-api-key + DATABASE_URL: postgres://127.0.0.1:5432 + ENCRYPTION_KEY: fabcc12-a22-facbaa4-11aa568aab + ``` + + + + + + + The `authentication` field dictates which authentication method to use when pushing secrets to Infisical. + The available authentication methods are `universalAuth`, `kubernetesAuth`, `awsIamAuth`, `azureAuth`, `gcpIdTokenAuth`, and `gcpIamAuth`. + + + + The universal authentication method is one of the easiest ways to get started with Infisical. Universal Auth works anywhere and is not tied to any specific cloud provider. + [Read more about Universal Auth](/documentation/platform/identities/universal-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `credentialsRef`: The name and namespace of the Kubernetes secret that stores the service token. + - `credentialsRef.secretName`: The name of the Kubernetes secret. + - `credentialsRef.secretNamespace`: The namespace of the Kubernetes secret. + + Example: + + ```yaml + # infisical-push-secret.yaml + spec: + universalAuth: + credentialsRef: + secretName: + secretNamespace: + ``` + + ```yaml + # machine-identity-credentials.yaml + apiVersion: v1 + kind: Secret + metadata: + name: universal-auth-credentials + type: Opaque + stringData: + clientId: + clientSecret: + ``` + + + + The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment. + [Read more about Kubernetes Auth](/documentation/platform/identities/kubernetes-auth). + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `serviceAccountRef`: The name and namespace of the service account that will be used to authenticate with Infisical. + - `serviceAccountRef.name`: The name of the service account. + - `serviceAccountRef.namespace`: The namespace of the service account. + + Example: + + ```yaml + spec: + kubernetesAuth: + identityId: + serviceAccountRef: + name: + namespace: + ``` + + + + The AWS IAM machine identity authentication method is used to authenticate with Infisical. + [Read more about AWS IAM Auth](/documentation/platform/identities/aws-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + + Example: + + ```yaml + spec: + authentication: + awsIamAuth: + identityId: + ``` + + + + The AWS IAM machine identity authentication method is used to authenticate with Infisical. Azure Auth can only be used from within an Azure environment. + [Read more about Azure Auth](/documentation/platform/identities/azure-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + + Example: + + ```yaml + spec: + authentication: + azureAuth: + identityId: + ``` + + + The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments. + [Read more about Azure Auth](/documentation/platform/identities/gcp-auth). + + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + - `serviceAccountKeyFilePath`: The path to the GCP service account key file. + + Example: + + ```yaml + spec: + gcpIamAuth: + identityId: + serviceAccountKeyFilePath: + ``` + + + The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments. + [Read more about Azure Auth](/documentation/platform/identities/gcp-auth). + + Valid fields: + - `identityId`: The identity ID of the machine identity you created. + + Example: + + ```yaml + spec: + gcpIdTokenAuth: + identityId: + ``` + + + + + + + This block defines the TLS settings to use for connecting to the Infisical + instance. + + Fields: + + This block defines the reference to the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + + Valid fields: + - `secretName`: The name of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + - `secretNamespace`: The namespace of the Kubernetes secret containing the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + - `key`: The name of the key in the Kubernetes secret which contains the value of the CA certificate to use for connecting to the Infisical instance with SSL/TLS. + + Example: + + ```yaml + tls: + caRef: + secretName: custom-ca-certificate + secretNamespace: default + key: ca.crt + ``` + + + + + +### Applying the InfisicalPushSecret CRD to your cluster + +Once you have configured the `InfisicalPushSecret` CRD with the required fields, you can apply it to your cluster. +After applying, you should notice that the secrets have been pushed to Infisical. + +```bash + kubectl apply -f source-push-secret.yaml # The secret that you're referencing in the InfisicalPushSecret CRD push.secret field + kubectl apply -f example-infisical-push-secret-crd.yaml # The InfisicalPushSecret CRD itself +``` \ No newline at end of file diff --git a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx new file mode 100644 index 000000000..4efda567e --- /dev/null +++ b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx @@ -0,0 +1,965 @@ +--- +sidebarTitle: "InfisicalSecret CRD" +title: "Using the InfisicalSecret CRD" +description: "Learn how to use the InfisicalSecret CRD to fetch secrets from Infisical and store them in a Kubernetes secret" +--- + +## Sync Infisical Secrets to your cluster + +Once you have installed the operator to your cluster, you'll need to create a `InfisicalSecret` custom resource definition (CRD). + +```yaml example-infisical-secret-crd.yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample + labels: + label-to-be-passed-to-managed-secret: sample-value + annotations: + example.com/annotation-to-be-passed-to-managed-secret: "sample-value" +spec: + hostAPI: https://app.infisical.com/api + resyncInterval: 10 + authentication: + # Make sure to only have 1 authentication method defined, serviceToken/universalAuth. + # If you have multiple authentication methods defined, it may cause issues. + + # (Deprecated) Service Token Auth + serviceToken: + serviceTokenSecretReference: + secretName: service-token + secretNamespace: default + secretsScope: + envSlug: + secretsPath: + recursive: true + + # Universal Auth + universalAuth: + secretsScope: + projectSlug: new-ob-em + envSlug: dev # "dev", "staging", "prod", etc.. + secretsPath: "/" # Root is "/" + recursive: true # Whether or not to use recursive mode (Fetches all secrets in an environment from a given secret path, and all folders inside the path) / defaults to false + credentialsRef: + secretName: universal-auth-credentials + secretNamespace: default + + # Native Kubernetes Auth + kubernetesAuth: + identityId: + serviceAccountRef: + name: + namespace: + + # secretsScope is identical to the secrets scope in the universalAuth field in this sample. + secretsScope: + projectSlug: your-project-slug + envSlug: prod + secretsPath: "/path" + recursive: true + + # AWS IAM Auth + awsIamAuth: + identityId: + + # secretsScope is identical to the secrets scope in the universalAuth field in this sample. + secretsScope: + projectSlug: your-project-slug + envSlug: prod + secretsPath: "/path" + recursive: true + + # Azure Auth + azureAuth: + identityId: + resource: https://management.azure.com/&client_id=CLIENT_ID # (Optional) This is the Azure resource that you want to access. For example, "https://management.azure.com/". If no value is provided, it will default to "https://management.azure.com/" + + # secretsScope is identical to the secrets scope in the universalAuth field in this sample. + secretsScope: + projectSlug: your-project-slug + envSlug: prod + secretsPath: "/path" + recursive: true + + # GCP ID Token Auth + gcpIdTokenAuth: + identityId: + + # secretsScope is identical to the secrets scope in the universalAuth field in this sample. + secretsScope: + projectSlug: your-project-slug + envSlug: prod + secretsPath: "/path" + recursive: true + + # GCP IAM Auth + gcpIamAuth: + identityId: + + # secretsScope is identical to the secrets scope in the universalAuth field in this sample. + secretsScope: + projectSlug: your-project-slug + envSlug: prod + secretsPath: "/path" + recursive: true + + managedSecretReference: + secretName: managed-secret + secretNamespace: default + creationPolicy: "Orphan" ## Owner | Orphan + # template: + # includeAllSecrets: true + # data: + # CUSTOM_KEY: "{{ .KEY.SecretPath }} {{ .KEY.Value }}" + # secretType: kubernetes.io/dockerconfigjson +``` + +### InfisicalSecret CRD properties + + + If you are fetching secrets from a self-hosted instance of Infisical set the value of `hostAPI` to + ` https://your-self-hosted-instace.com/api` + +When `hostAPI` is not defined the operator fetches secrets from Infisical Cloud. + + + If you have installed your Infisical instance within the same cluster as the Infisical operator, you can optionally access the Infisical backend's service directly without having to route through the public internet. + To achieve this, use the following address for the hostAPI field: + + ``` bash + http://..svc.cluster.local:4000/api + ``` + + Make sure to replace `` and `` with the appropriate values for your backend service and namespace. + + + + + + This property defines the time in seconds between each secret re-sync from + Infisical. Shorter time between re-syncs will require higher rate limits only + available on paid plans. Default re-sync interval is every 1 minute. + + + + This block defines the TLS settings to use for connecting to the Infisical + instance. + + + + This block defines the reference to the CA certificate to use for connecting + to the Infisical instance with SSL/TLS. + + + + The name of the Kubernetes secret containing the CA certificate to use for + connecting to the Infisical instance with SSL/TLS. + + + + The namespace of the Kubernetes secret containing the CA certificate to use + for connecting to the Infisical instance with SSL/TLS. + + + + The name of the key in the Kubernetes secret which contains the value of the + CA certificate to use for connecting to the Infisical instance with SSL/TLS. + + + + This block defines the method that will be used to authenticate with Infisical + so that secrets can be fetched + + + + The universal machine identity authentication method is used to authenticate with Infisical. The client ID and client secret needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores these credentials. + + + + You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about machine identities here](/documentation/platform/identities/universal-auth). + + + Once you have created your machine identity and added it to your project(s), you will need to create a Kubernetes secret containing the identity credentials. + To quickly create a Kubernetes secret containing the identity credentials, you can run the command below. + + Make sure you replace `` with the identity client ID and `` with the identity client secret. + + ``` bash + kubectl create secret generic universal-auth-credentials --from-literal=clientId="" --from-literal=clientSecret="" + ``` + + + + Once the secret is created, add the `secretName` and `secretNamespace` of the secret that was just created under `authentication.universalAuth.credentialsRef` field in the InfisicalSecret resource. + + + + + + Make sure to also populate the `secretsScope` field with the project slug + _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`secretsPath`_ that you want to fetch secrets from. Please see the example + below. + + +## Example + +```yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample-crd +spec: + authentication: + universalAuth: + secretsScope: + projectSlug: # <-- project slug + envSlug: # "dev", "staging", "prod", etc.. + secretsPath: "" # Root is "/" + credentialsRef: + secretName: universal-auth-credentials # <-- name of the Kubernetes secret that stores our machine identity credentials + secretNamespace: default # <-- namespace of the Kubernetes secret that stores our machine identity credentials + ... +``` + + + + + The Kubernetes machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within a Kubernetes environment. + + + + 1.1. Start by creating a service account in your Kubernetes cluster that will be used by Infisical to authenticate with the Kubernetes API Server. + + ```yaml infisical-service-account.yaml + apiVersion: v1 + kind: ServiceAccount + metadata: + name: infisical-auth + namespace: default + + ``` + + ``` + kubectl apply -f infisical-service-account.yaml + ``` + + 1.2. Bind the service account to the `system:auth-delegator` cluster role. As described [here](https://kubernetes.io/docs/reference/access-authn-authz/rbac/#other-component-roles), this role allows delegated authentication and authorization checks, specifically for Infisical to access the [TokenReview API](https://kubernetes.io/docs/reference/kubernetes-api/authentication-resources/token-review-v1/). You can apply the following configuration file: + + ```yaml cluster-role-binding.yaml + apiVersion: rbac.authorization.k8s.io/v1 + kind: ClusterRoleBinding + metadata: + name: role-tokenreview-binding + namespace: default + roleRef: + apiGroup: rbac.authorization.k8s.io + kind: ClusterRole + name: system:auth-delegator + subjects: + - kind: ServiceAccount + name: infisical-auth + namespace: default + ``` + + ``` + kubectl apply -f cluster-role-binding.yaml + ``` + + 1.3. Next, create a long-lived service account JWT token (i.e. the token reviewer JWT token) for the service account using this configuration file for a new `Secret` resource: + + ```yaml service-account-token.yaml + apiVersion: v1 + kind: Secret + type: kubernetes.io/service-account-token + metadata: + name: infisical-auth-token + annotations: + kubernetes.io/service-account.name: "infisical-auth" + ``` + + + ``` + kubectl apply -f service-account-token.yaml + ``` + + 1.4. Link the secret in step 1.3 to the service account in step 1.1: + + ```bash + kubectl patch serviceaccount infisical-auth -p '{"secrets": [{"name": "infisical-auth-token"}]}' -n default + ``` + + 1.5. Finally, retrieve the token reviewer JWT token from the secret. + + ```bash + kubectl get secret infisical-auth-token -n default -o=jsonpath='{.data.token}' | base64 --decode + ``` + + Keep this JWT token handy as you will need it for the **Token Reviewer JWT** field when configuring the Kubernetes Auth authentication method for the identity in step 2. + + + + + To create an identity, head to your Organization Settings > Access Control > Machine Identities and press **Create identity**. + + ![identities organization](/images/platform/identities/identities-org.png) + + When creating an identity, you specify an organization level [role](/documentation/platform/role-based-access-controls) for it to assume; you can configure roles in Organization Settings > Access Control > Organization Roles. + + ![identities organization create](/images/platform/identities/identities-org-create.png) + + Now input a few details for your new identity. Here's some guidance for each field: + + - Name (required): A friendly name for the identity. + - Role (required): A role from the **Organization Roles** tab for the identity to assume. The organization role assigned will determine what organization level resources this identity can have access to. + + Once you've created an identity, you'll be prompted to configure the authentication method for it. Here, select **Kubernetes Auth**. + + + To learn more about each field of the Kubernetes native authentication method, see step 2 of [guide](/documentation/platform/identities/kubernetes-auth#guide). + + + ![identities organization create auth method](/images/platform/identities/identities-org-create-kubernetes-auth-method.png) + + + + + To allow the operator to use the given identity to access secrets, you will need to add the identity to project(s) that you would like to grant it access to. + + To do this, head over to the project you want to add the identity to and go to Project Settings > Access Control > Machine Identities and press **Add identity**. + + Next, select the identity you want to add to the project and the project level role you want to allow it to assume. The project role assigned will determine what project level resources this identity can have access to. + + ![identities project](/images/platform/identities/identities-project.png) + + ![identities project create](/images/platform/identities/identities-project-create.png) + + + + Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. + In the `authentication.kubernetesAuth.identityId` field, add the identity ID of the machine identity you created. + See the example below for more details. + + + Add the service account details from the previous steps under `authentication.kubernetesAuth.serviceAccountRef`. + Here you will need to enter the name and namespace of the service account. + The example below shows a complete InfisicalSecret resource with all required fields defined. + + + + + + Make sure to also populate the `secretsScope` field with the project slug + _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`secretsPath`_ that you want to fetch secrets from. Please see the example + below. + + +## Example + +```yaml example-kubernetes-auth.yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample-crd +spec: + authentication: + kubernetesAuth: + identityId: + serviceAccountRef: + name: + namespace: + + # secretsScope is identical to the secrets scope in the universalAuth field in this sample. + secretsScope: + projectSlug: your-project-slug + envSlug: prod + secretsPath: "/path" + recursive: true + ... +``` + + + + + The AWS IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within an AWS environment like an EC2 or a Lambda function. + + + + You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about AWS machine identities here](/documentation/platform/identities/aws-auth). + + + Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. In the `authentication.awsIamAuth.identityId` field, add the identity ID of the machine identity you created. See the example below for more details. + + + + + + Make sure to also populate the `secretsScope` field with the project slug + _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`secretsPath`_ that you want to fetch secrets from. Please see the example + below. + + +## Example + +```yaml example-aws-iam-auth.yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample-crd +spec: + authentication: + awsIamAuth: + identityId: + + # secretsScope is identical to the secrets scope in the universalAuth field in this sample. + secretsScope: + projectSlug: your-project-slug + envSlug: prod + secretsPath: "/path" + recursive: true + ... +``` + + + + + The Azure machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within an Azure environment. + + + + You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about Azure machine identities here](/documentation/platform/identities/azure-auth). + + + Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. In the `authentication.azureAuth.identityId` field, add the identity ID of the machine identity you created. See the example below for more details. + + + + + + Make sure to also populate the `secretsScope` field with the project slug + _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`secretsPath`_ that you want to fetch secrets from. Please see the example + below. + + +## Example + +```yaml example-azure-auth.yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample-crd +spec: + authentication: + azureAuth: + identityId: + + # secretsScope is identical to the secrets scope in the universalAuth field in this sample. + secretsScope: + projectSlug: your-project-slug + envSlug: prod + secretsPath: "/path" + recursive: true + ... +``` + + + + + The GCP ID Token machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used within GCP environments. + + + + You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about GCP machine identities here](/documentation/platform/identities/gcp-auth). + + + Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. In the `authentication.gcpIdTokenAuth.identityId` field, add the identity ID of the machine identity you created. See the example below for more details. + + + + + + Make sure to also populate the `secretsScope` field with the project slug + _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`secretsPath`_ that you want to fetch secrets from. Please see the example + below. + + +## Example + +```yaml example-gcp-id-token-auth.yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample-crd +spec: + authentication: + gcpIdTokenAuth: + identityId: + + # secretsScope is identical to the secrets scope in the universalAuth field in this sample. + secretsScope: + projectSlug: your-project-slug + envSlug: prod + secretsPath: "/path" + recursive: true + ... +``` + + + + + The GCP IAM machine identity authentication method is used to authenticate with Infisical. The identity ID is stored in a field in the InfisicalSecret resource. This authentication method can only be used both within and outside GCP environments. + + + + You need to create a machine identity, and give it access to the project(s) you want to interact with. You can [read more about GCP machine identities here](/documentation/platform/identities/gcp-auth). + + + Once you have created your machine identity and added it to your project(s), you will need to add the identity ID to your InfisicalSecret resource. In the `authentication.gcpIamAuth.identityId` field, add the identity ID of the machine identity you created. + You'll also need to add the service account key file path to your InfisicalSecret resource. In the `authentication.gcpIamAuth.serviceAccountKeyFilePath` field, add the path to your service account key file path. Please see the example below for more details. + + + + + + Make sure to also populate the `secretsScope` field with the project slug + _`projectSlug`_, environment slug _`envSlug`_, and secrets path + _`secretsPath`_ that you want to fetch secrets from. Please see the example + below. + + +## Example + +```yaml example-gcp-id-token-auth.yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample-crd +spec: + authentication: + gcpIamAuth: + identityId: + serviceAccountKeyFilePath: "/path/to-service-account-key-file-path.json" + + # secretsScope is identical to the secrets scope in the universalAuth field in this sample. + secretsScope: + projectSlug: your-project-slug + envSlug: prod + secretsPath: "/path" + recursive: true + ... +``` + + + + + +The service token required to authenticate with Infisical needs to be stored in a Kubernetes secret. This block defines the reference to the name and namespace of secret that stores this service token. +Follow the instructions below to create and store the service token in a Kubernetes secrets and reference it in your CRD. + +#### 1. Generate service token + +You can generate a [service token](../../documentation/platform/token) for an Infisical project by heading over to the Infisical dashboard then to Project Settings. + +#### 2. Create Kubernetes secret containing service token + +Once you have generated the service token, you will need to create a Kubernetes secret containing the service token you generated. +To quickly create a Kubernetes secret containing the generated service token, you can run the command below. Make sure you replace `` with your service token. + +```bash +kubectl create secret generic service-token --from-literal=infisicalToken="" +``` + +#### 3. Add reference for the Kubernetes secret containing service token + +Once the secret is created, add the name and namespace of the secret that was just created under `authentication.serviceToken.serviceTokenSecretReference` field in the InfisicalSecret resource. + +{" "} + + + Make sure to also populate the `secretsScope` field with the, environment slug + _`envSlug`_, and secrets path _`secretsPath`_ that you want to fetch secrets + from. Please see the example below. + + +## Example + +```yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample-crd +spec: + authentication: + serviceToken: + serviceTokenSecretReference: + secretName: service-token # <-- name of the Kubernetes secret that stores our service token + secretNamespace: option # <-- namespace of the Kubernetes secret that stores our service token + secretsScope: + envSlug: # "dev", "staging", "prod", etc.. + secretsPath: # Root is "/" + ... +``` + + + + +The `managedSecretReference` field is used to define the target location for storing secrets retrieved from an Infisical project. +This field requires specifying both the name and namespace of the Kubernetes secret that will hold these secrets. +The Infisical operator will automatically create the Kubernetes secret with the specified name/namespace and keep it continuously updated. + +Note: The managed secret be should be created in the same namespace as the deployment that will use it. + + + +The name of the managed Kubernetes secret to be created + + +The namespace of the managed Kubernetes secret to be created. + + +Override the default Opaque type for managed secrets with this field. Useful for creating kubernetes.io/dockerconfigjson secrets. + + +Templates enable you to transform data from Infisical before storing it as a Kubernetes Secret. + + +When set to true, this option injects all secrets retrieved from Infisical into your configuration. +Secrets defined in the template will override the automatically injected secrets. + + +Define secret keys and their corresponding templates. +Each data value uses a Golang template with access to all secrets retrieved from the specified scope. + +Secrets are structured as follows: + +```golang +type TemplateSecret struct { + Value string `json:"value"` + SecretPath string `json:"secretPath"` +} +``` + +#### Example template configuration: + +```golang + managedSecretReference: + secretName: managed-secret + secretNamespace: default + template: + includeAllSecrets: true + data: + NEW_KEY: "{{ .KEY1.SecretPath }} {{ .KEY1.Value }}" +``` + +When you run the following command: + +```bash +kubectl get secret managed-secret -o jsonpath='{.data}' +``` + +You'll receive Kubernetes secrets output that includes the NEW_KEY: + +```bash +{... "KEY":"d29ybGQ=","NEW_KEY":"LyBoZWxsbw=="} +``` + +When you set `includeAllSecrets` as `false` the Kubernetes secrets outputs will be: + +```bash +{"NEW_KEY":"LyBoZWxsbw=="} +``` + + + +Creation polices allow you to control whether or not owner references should be added to the managed Kubernetes secret that is generated by the Infisical operator. +This is useful for tools such as ArgoCD, where every resource requires an owner reference; otherwise, it will be pruned automatically. + +#### Available options + +- `Orphan` (default) +- `Owner` + + + When creation policy is set to `Owner`, the `InfisicalSecret` CRD must be in + the same namespace as where the managed kubernetes secret. + + + + +### Propagating labels & annotations + +The operator will transfer all labels & annotations present on the `InfisicalSecret` CRD to the managed Kubernetes secret to be created. +Thus, if a specific label is required on the resulting secret, it can be applied as demonstrated in the following example: + + +```yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample + labels: + label-to-be-passed-to-managed-secret: sample-value + annotations: + example.com/annotation-to-be-passed-to-managed-secret: "sample-value" +spec: + .. + authentication: + ... + managedSecretReference: + ... +``` + +This would result in the following managed secret to be created: + +```yaml +apiVersion: v1 +data: ... +kind: Secret +metadata: + annotations: + example.com/annotation-to-be-passed-to-managed-secret: sample-value + secrets.infisical.com/version: W/"3f1-ZyOSsrCLGSkAhhCkY2USPu2ivRw" + labels: + label-to-be-passed-to-managed-secret: sample-value + name: managed-token + namespace: default +type: Opaque +``` + + + +### Apply the InfisicalSecret CRD to your cluster + +Once you have configured the InfisicalSecret CRD with the required fields, you can apply it to your cluster. +After applying, you should notice that the managed secret has been created in the desired namespace your specified. + +``` +kubectl apply -f example-infisical-secret-crd.yaml +``` + +### Verify managed secret creation + +To verify that the operator has successfully created the managed secret, you can check the secrets in the namespace that was specified. + +```bash +# Verify managed secret is created +kubectl get secrets -n +``` + + + The Infisical secrets will be synced and stored into the managed secret every + 1 minutes. + + +### Using managed secret in your deployment + +Incorporating the managed secret created by the operator into your deployment can be achieved through several methods. +Here, we will highlight three of the most common ways to utilize it. Learn more about Kubernetes secrets [here](https://kubernetes.io/docs/concepts/configuration/secret/) + + + This will take all the secrets from your managed secret and expose them to your container + +````yaml + envFrom: + - secretRef: + name: managed-secret # managed secret name + ``` + + Example usage in a deployment + ```yaml + apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + envFrom: + - secretRef: + name: managed-secret # <- name of managed secret + ports: + - containerPort: 80 +```` + + + + + This will allow you to select individual secrets by key name from your managed secret and expose them to your container + + ```yaml + env: + - name: SECRET_NAME # The environment variable's name which is made available in the container + valueFrom: + secretKeyRef: + name: managed-secret # managed secret name + key: SOME_SECRET_KEY # The name of the key which exists in the managed secret + ``` + +Example usage in a deployment + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: +name: nginx-deployment +labels: +app: nginx +spec: +replicas: 1 +selector: +matchLabels: +app: nginx +template: +metadata: +labels: +app: nginx +spec: +containers: - name: nginx +image: nginx:1.14.2 +env: - name: STRIPE_API_SECRET +valueFrom: +secretKeyRef: +name: managed-secret # <- name of managed secret +key: STRIPE_API_SECRET +ports: - containerPort: 80 + +``` + + + + +This will allow you to create a volume on your container which comprises of files holding the secrets in your managed kubernetes secret +```yaml +volumes: + - name: secrets-volume-name # The name of the volume under which secrets will be stored + secret: + secretName: managed-secret # managed secret name +```` + +You can then mount this volume to the container's filesystem so that your deployment can access the files containing the managed secrets + +```yaml +volumeMounts: + - name: secrets-volume-name + mountPath: /etc/secrets + readOnly: true +``` + +Example usage in a deployment + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + volumeMounts: + - name: secrets-volume-name + mountPath: /etc/secrets + readOnly: true + ports: + - containerPort: 80 + volumes: + - name: secrets-volume-name + secret: + secretName: managed-secret # <- managed secrets +``` + + + +The definition file of the Kubernetes secret for the CA certificate can be structured like the following: + +```yaml +apiVersion: v1 +kind: Secret +metadata: + name: custom-ca-certificate +type: Opaque +stringData: + ca.crt: | + -----BEGIN CERTIFICATE----- + MIIEZzCCA0+gAwIBAgIUDk9+HZcMHppiNy0TvoBg8/aMEqIwDQYJKoZIhvcNAQEL + ... + BQAwDTELMAkGA1UEChMCUEgwHhcNMjQxMDI1MTU0MjAzWhcNMjUxMDI1MjE0MjAz + -----END CERTIFICATE----- +``` + +### Auto redeployment + +Deployments using managed secrets don't reload automatically on updates, so they may use outdated secrets unless manually redeployed. +To address this, we added functionality to automatically redeploy your deployment when its managed secret updates. + +#### Enabling auto redeploy + +To enable auto redeployment you simply have to add the following annotation to the deployment that consumes a managed secret + +```yaml +secrets.infisical.com/auto-reload: "true" +``` + + +```yaml +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx-deployment + labels: + app: nginx + annotations: + secrets.infisical.com/auto-reload: "true" # <- redeployment annotation +spec: + replicas: 1 + selector: + matchLabels: + app: nginx + template: + metadata: + labels: + app: nginx + spec: + containers: + - name: nginx + image: nginx:1.14.2 + envFrom: + - secretRef: + name: managed-secret + ports: + - containerPort: 80 +``` + + + #### How it works + When a secret change occurs, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update. + Then, for each deployment that has this annotation present, a rolling update will be triggered. + \ No newline at end of file diff --git a/docs/integrations/platforms/kubernetes/overview.mdx b/docs/integrations/platforms/kubernetes/overview.mdx new file mode 100644 index 000000000..1b502a6a8 --- /dev/null +++ b/docs/integrations/platforms/kubernetes/overview.mdx @@ -0,0 +1,202 @@ +--- +title: "Kubernetes Operator" +sidebarTitle: "Overview" +description: "How to use Infisical to inject secrets into Kubernetes clusters." +--- + +![title](../../../images/k8-diagram.png) + +The Infisical Secrets Operator is a Kubernetes controller that retrieves secrets from Infisical and stores them in a designated cluster. +It uses an `InfisicalSecret` resource to specify authentication and storage methods. +The operator continuously updates secrets and can also reload dependent deployments automatically. + + + If you are already using the External Secrets operator, you can view the + integration documentation for it + [here](https://external-secrets.io/latest/provider/infisical/). + + +## Install Operator + +The operator can be install via [Helm](https://helm.sh). Helm is a package manager for Kubernetes that allows you to define, install, and upgrade Kubernetes applications. + + +**Install the latest Infisical Helm repository** +```bash +helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' + +helm repo update +``` + +**Install the Helm chart** + +To select a specific version, view the application versions [here](https://hub.docker.com/r/infisical/kubernetes-operator/tags) and chart versions [here](https://cloudsmith.io/~infisical/repos/helm-charts/packages/detail/helm/secrets-operator/#versions) + +```bash +helm install --generate-name infisical-helm-charts/secrets-operator +``` + +```bash +# Example installing app version v0.2.0 and chart version 0.1.4 +helm install --generate-name infisical-helm-charts/secrets-operator --version=0.1.4 --set controllerManager.manager.image.tag=v0.2.0 +``` + +**Namespace-scoped Installation** + +The operator can be configured to watch and manage secrets in a specific namespace instead of having cluster-wide access. This is useful for: + +- **Enhanced Security**: Limit the operator's permissions to only specific namespaces instead of cluster-wide access +- **Multi-tenant Clusters**: Run separate operator instances for different teams or applications +- **Resource Isolation**: Ensure operators in different namespaces don't interfere with each other +- **Development & Testing**: Run development and production operators side by side in isolated namespaces + +**Note**: For multiple namespace-scoped installations, only the first installation should install CRDs. Subsequent installations should set `installCRDs: false` to avoid conflicts. + +```bash +# First namespace installation (with CRDs) +helm install operator-namespace1 infisical-helm-charts/secrets-operator \ + --namespace first-namespace \ + --set scopedNamespace=first-namespace \ + --set scopedRBAC=true + +# Subsequent namespace installations +helm install operator-namespace2 infisical-helm-charts/secrets-operator \ + --namespace another-namespace \ + --set scopedNamespace=another-namespace \ + --set scopedRBAC=true \ + --set installCRDs=false +``` + +When scoped to a namespace, the operator will: + +- Only watch InfisicalSecrets in the specified namespace +- Only create/update Kubernetes secrets in that namespace +- Only access deployments in that namespace + +The default configuration gives cluster-wide access: + +```yaml +installCRDs: true # Install CRDs (set to false for additional namespace installations) +scopedNamespace: "" # Empty for cluster-wide access +scopedRBAC: false # Cluster-wide permissions +``` + +If you want to install operators in multiple namespaces simultaneously: +- Make sure to set `installCRDs: false` for all but one of the installations to avoid conflicts, as CRDs are cluster-wide resources. +- Use unique release names for each installation (e.g., operator-namespace1, operator-namespace2). + + +## Custom Resource Definitions (CRD's) + +Currently the operator supports the following CRD's. We are constantly expanding the functionality of the operator, and this list will be updated as new CRD's are added. + +1. [InfisicalSecret](/integrations/platforms/kubernetes/infisical-secret-crd): Sync secrets from Infisical to a Kubernetes secret. +2. [InfisicalPushSecret](/integrations/platforms/kubernetes/infisical-push-secret-crd): Push secrets from a Kubernetes secret to Infisical. +3. [InfisicalDynamicSecret](/integrations/platforms/kubernetes/infisical-dynamic-secret-crd): Sync dynamic secrets and create leases automatically in Kubernetes. + +## Connecting to instances with private/self-signed certificate + +To connect to Infisical instances behind a private/self-signed certificate, you can configure the TLS settings in the CRD +to point to a CA certificate stored in a Kubernetes secret resource. + +```yaml +--- +spec: + hostAPI: https://app.infisical.com/api + tls: + caRef: + secretName: custom-ca-certificate + secretNamespace: default + key: ca.crt +--- +``` + + +## Global configuration + +To configure global settings that will apply to all instances of `InfisicalSecret`, you can define these configurations in a Kubernetes ConfigMap. +For example, you can configure all `InfisicalSecret` instances to fetch secrets from a single backend API without specifying the `hostAPI` parameter for each instance. + +### Available global properties + +| Property | Description | Default value | +| -------- | --------------------------------------------------------------------------------- | ----------------------------- | +| hostAPI | If `hostAPI` in `InfisicalSecret` instance is left empty, this value will be used | https://app.infisical.com/api | + +### Applying global configurations + +All global configurations must reside in a Kubernetes ConfigMap named `infisical-config` in the namespace `infisical-operator-system`. +To apply global configuration to the operator, copy the following yaml into `infisical-config.yaml` file. + +```yaml infisical-config.yaml +apiVersion: v1 +kind: Namespace +metadata: + name: infisical-operator-system +--- +apiVersion: v1 +kind: ConfigMap +metadata: + name: infisical-config + namespace: infisical-operator-system +data: + hostAPI: https://example.com/api # <-- global hostAPI +``` + +Then apply this change via kubectl by running the following + +```bash +kubectl apply -f infisical-config.yaml +``` + +## Troubleshoot operator + +If the operator is unable to fetch secrets from the API, it will not affect the managed Kubernetes secret. +It will continue attempting to reconnect to the API indefinitely. +The InfisicalSecret resource uses the `status.conditions` field to report its current state and any errors encountered. + +```yaml +$ kubectl get infisicalSecrets +NAME AGE +infisicalsecret-sample 12s + +$ kubectl describe infisicalSecret infisicalsecret-sample +... +Spec: +... +Status: + Conditions: + Last Transition Time: 2022-12-18T04:29:09Z + Message: Infisical controller has located the Infisical token in provided Kubernetes secret + Reason: OK + Status: True + Type: secrets.infisical.com/LoadedInfisicalToken + Last Transition Time: 2022-12-18T04:29:10Z + Message: Failed to update secret because: 400 Bad Request + Reason: Error + Status: False + Type: secrets.infisical.com/ReadyToSyncSecrets +Events: +``` + +## Uninstall Operator + +The managed secret created by the operator will not be deleted when the operator is uninstalled. + + + + Install Infisical Helm repository + ```bash + helm uninstall + ``` + + + ``` + kubectl delete -f https://raw.githubusercontent.com/Infisical/infisical/main/k8-operator/kubectl-install/install-secrets-operator.yaml + ``` + + + +## Useful Articles + +- [Managing secrets in OpenShift with Infisical](https://xphyr.net/post/infisical_ocp/) diff --git a/docs/mint.json b/docs/mint.json index 69ad84f11..79e3168f2 100644 --- a/docs/mint.json +++ b/docs/mint.json @@ -348,7 +348,15 @@ { "group": "Container orchestrators", "pages": [ - "integrations/platforms/kubernetes", + { + "group": "Kubernetes", + "pages": [ + "integrations/platforms/kubernetes/overview", + "integrations/platforms/kubernetes/infisical-secret-crd", + "integrations/platforms/kubernetes/infisical-push-secret-crd", + "integrations/platforms/kubernetes/infisical-dynamic-secret-crd" + ] + }, "integrations/platforms/kubernetes-csi", "integrations/platforms/docker-swarm-with-agent", "integrations/platforms/ecs-with-agent" From b5b91c929f48a07a6bce73ae88ab9f8af63d23dc Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Thu, 9 Jan 2025 12:11:48 -0500 Subject: [PATCH 30/32] fix kubernetes docs --- .../infisical-dynamic-secret-crd.mdx | 22 ++- .../kubernetes/infisical-secret-crd.mdx | 94 +++++++------ .../platforms/kubernetes/overview.mdx | 129 +++++++++--------- 3 files changed, 124 insertions(+), 121 deletions(-) diff --git a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx index 52df2ceb0..f0efb85db 100644 --- a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx @@ -1,14 +1,26 @@ --- sidebarTitle: "InfisicalDynamicSecret CRD" -title: "Using the InfisicalDynamicSecret CRD" -description: "Learn how to use the InfisicalDynamicSecret CRD to create dynamic secret leases in Infisical and sync them to your Kubernetes cluster." +title: "InfisicalDynamicSecret CRD" +description: "Learn how to generate dynamic secret leases in Infisical and sync them to your Kubernetes cluster." --- +## Overview -## Sync Dynamic Secrets to your cluster +The **InfisicalDynamicSecret** CRD allows you to easily create and manage dynamic secret leases in Infisical and automatically syncing them to your Kubernetes cluster as native **Kubernetes Secret** resources. +This means any Pod, Deployment, or other Kubernetes resource can make use of dynamic secrets from Infisical just like any other K8s secret. -### Example usage +This CRD offers the following features: +- **Generate a dynamic secret lease** in Infisical and track its lifecycle. +- **write** the dynamic secret from Infisical to your cluster as native Kubernetes secret. +- **Automatically rotate** the dyanmic secret value before it expires to make sure your cluster always has valid credentials. +- **Optionally trigger redeployments** of any workloads that consume the secret if you enable auto-reload. -The example below demonstrates a sample InfisicalDynamicSecret CRD that creates a dynamic secret lease in Infisical, and syncs the lease to your Kubernetes cluster. +### Prerequisites +- The operator is installed on to your Kubernetes cluster +- You have already configured a dynamic secret in Infisical + +## Configure Dynamic Secret CRD + +The example below shows a sample **InfisicalDynamicSecret** CRD that creates a dynamic secret lease in Infisical, and syncs the lease to your Kubernetes cluster. ```yaml dynamic-secret-crd.yaml apiVersion: secrets.infisical.com/v1alpha1 diff --git a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx index 4efda567e..39154a7c4 100644 --- a/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-secret-crd.mdx @@ -1,11 +1,9 @@ --- sidebarTitle: "InfisicalSecret CRD" -title: "Using the InfisicalSecret CRD" -description: "Learn how to use the InfisicalSecret CRD to fetch secrets from Infisical and store them in a Kubernetes secret" +title: "InfisicalSecret CRD" +description: "Learn how to use the InfisicalSecret CRD to fetch secrets from Infisical and store them as native Kubernetes secret resource" --- -## Sync Infisical Secrets to your cluster - Once you have installed the operator to your cluster, you'll need to create a `InfisicalSecret` custom resource definition (CRD). ```yaml example-infisical-secret-crd.yaml @@ -691,48 +689,6 @@ This is useful for tools such as ArgoCD, where every resource requires an owner -### Propagating labels & annotations - -The operator will transfer all labels & annotations present on the `InfisicalSecret` CRD to the managed Kubernetes secret to be created. -Thus, if a specific label is required on the resulting secret, it can be applied as demonstrated in the following example: - - -```yaml -apiVersion: secrets.infisical.com/v1alpha1 -kind: InfisicalSecret -metadata: - name: infisicalsecret-sample - labels: - label-to-be-passed-to-managed-secret: sample-value - annotations: - example.com/annotation-to-be-passed-to-managed-secret: "sample-value" -spec: - .. - authentication: - ... - managedSecretReference: - ... -``` - -This would result in the following managed secret to be created: - -```yaml -apiVersion: v1 -data: ... -kind: Secret -metadata: - annotations: - example.com/annotation-to-be-passed-to-managed-secret: sample-value - secrets.infisical.com/version: W/"3f1-ZyOSsrCLGSkAhhCkY2USPu2ivRw" - labels: - label-to-be-passed-to-managed-secret: sample-value - name: managed-token - namespace: default -type: Opaque -``` - - - ### Apply the InfisicalSecret CRD to your cluster Once you have configured the InfisicalSecret CRD with the required fields, you can apply it to your cluster. @@ -756,7 +712,7 @@ kubectl get secrets -n 1 minutes. -### Using managed secret in your deployment +## Using managed secret in your deployment Incorporating the managed secret created by the operator into your deployment can be achieved through several methods. Here, we will highlight three of the most common ways to utilize it. Learn more about Kubernetes secrets [here](https://kubernetes.io/docs/concepts/configuration/secret/) @@ -962,4 +918,46 @@ spec: #### How it works When a secret change occurs, the operator will check to see which deployments are using the operator-managed Kubernetes secret that received the update. Then, for each deployment that has this annotation present, a rolling update will be triggered. - \ No newline at end of file + + +## Propagating labels & annotations + +The operator will transfer all labels & annotations present on the `InfisicalSecret` CRD to the managed Kubernetes secret to be created. +Thus, if a specific label is required on the resulting secret, it can be applied as demonstrated in the following example: + + +```yaml +apiVersion: secrets.infisical.com/v1alpha1 +kind: InfisicalSecret +metadata: + name: infisicalsecret-sample + labels: + label-to-be-passed-to-managed-secret: sample-value + annotations: + example.com/annotation-to-be-passed-to-managed-secret: "sample-value" +spec: + .. + authentication: + ... + managedSecretReference: + ... +``` + +This would result in the following managed secret to be created: + +```yaml +apiVersion: v1 +data: ... +kind: Secret +metadata: + annotations: + example.com/annotation-to-be-passed-to-managed-secret: sample-value + secrets.infisical.com/version: W/"3f1-ZyOSsrCLGSkAhhCkY2USPu2ivRw" + labels: + label-to-be-passed-to-managed-secret: sample-value + name: managed-token + namespace: default +type: Opaque +``` + + \ No newline at end of file diff --git a/docs/integrations/platforms/kubernetes/overview.mdx b/docs/integrations/platforms/kubernetes/overview.mdx index 1b502a6a8..c4e3f7c64 100644 --- a/docs/integrations/platforms/kubernetes/overview.mdx +++ b/docs/integrations/platforms/kubernetes/overview.mdx @@ -1,14 +1,18 @@ --- title: "Kubernetes Operator" sidebarTitle: "Overview" -description: "How to use Infisical to inject secrets into Kubernetes clusters." +description: "How to use Infisical to inject, push, and manage secrets within Kubernetes clusters" --- -![title](../../../images/k8-diagram.png) +The Infisical Operator is a collection of Kubernetes controllers that streamline how secrets are managed between Infisical and your Kubernetes cluster. +It provides multiple Custom Resource Definitions (CRDs) which enable you to: -The Infisical Secrets Operator is a Kubernetes controller that retrieves secrets from Infisical and stores them in a designated cluster. -It uses an `InfisicalSecret` resource to specify authentication and storage methods. -The operator continuously updates secrets and can also reload dependent deployments automatically. +- **Sync** secrets from Infisical into Kubernetes (`InfisicalSecret`). +- **Push** new secrets from Kubernetes to Infisical (`InfisicalPushSecret`). +- **Manage** dynamic secrets and automatically create time-bound leases (`InfisicalDynamicSecret`). + +When these CRDs are configured, the Infisical Operator will continuously monitors for changes and performs necessary updates to keep your Kubernetes secrets up to date. +It can also automatically reload dependent Deployments resources whenever relevant secrets are updated. If you are already using the External Secrets operator, you can view the @@ -16,77 +20,75 @@ The operator continuously updates secrets and can also reload dependent deployme [here](https://external-secrets.io/latest/provider/infisical/). -## Install Operator +## Install The operator can be install via [Helm](https://helm.sh). Helm is a package manager for Kubernetes that allows you to define, install, and upgrade Kubernetes applications. - -**Install the latest Infisical Helm repository** +**Install the latest Helm repository** ```bash helm repo add infisical-helm-charts 'https://dl.cloudsmith.io/public/infisical/helm-charts/helm/charts/' - +``` + +```bash helm repo update ``` -**Install the Helm chart** +The operator can be installed either cluster-wide or restricted to a specific namespace. +If you require stronger isolation and stricter access controls, a namespace-scoped installation may make more sense. -To select a specific version, view the application versions [here](https://hub.docker.com/r/infisical/kubernetes-operator/tags) and chart versions [here](https://cloudsmith.io/~infisical/repos/helm-charts/packages/detail/helm/secrets-operator/#versions) + + + ```bash + helm install --generate-name infisical-helm-charts/secrets-operator + ``` + + + The operator can be configured to watch and manage secrets in a specific namespace instead of having cluster-wide access. This is useful for: -```bash -helm install --generate-name infisical-helm-charts/secrets-operator -``` + - **Enhanced Security**: Limit the operator's permissions to only specific namespaces instead of cluster-wide access + - **Multi-tenant Clusters**: Run separate operator instances for different teams or applications + - **Resource Isolation**: Ensure operators in different namespaces don't interfere with each other + - **Development & Testing**: Run development and production operators side by side in isolated namespaces -```bash -# Example installing app version v0.2.0 and chart version 0.1.4 -helm install --generate-name infisical-helm-charts/secrets-operator --version=0.1.4 --set controllerManager.manager.image.tag=v0.2.0 -``` + **Note**: For multiple namespace-scoped installations, only the first installation should install CRDs. Subsequent installations should set `installCRDs: false` to avoid conflicts. -**Namespace-scoped Installation** + ```bash + # First namespace installation (with CRDs) + helm install operator-namespace1 infisical-helm-charts/secrets-operator \ + --namespace first-namespace \ + --set scopedNamespace=first-namespace \ + --set scopedRBAC=true -The operator can be configured to watch and manage secrets in a specific namespace instead of having cluster-wide access. This is useful for: + # Subsequent namespace installations + helm install operator-namespace2 infisical-helm-charts/secrets-operator \ + --namespace another-namespace \ + --set scopedNamespace=another-namespace \ + --set scopedRBAC=true \ + --set installCRDs=false + ``` -- **Enhanced Security**: Limit the operator's permissions to only specific namespaces instead of cluster-wide access -- **Multi-tenant Clusters**: Run separate operator instances for different teams or applications -- **Resource Isolation**: Ensure operators in different namespaces don't interfere with each other -- **Development & Testing**: Run development and production operators side by side in isolated namespaces + When scoped to a namespace, the operator will: -**Note**: For multiple namespace-scoped installations, only the first installation should install CRDs. Subsequent installations should set `installCRDs: false` to avoid conflicts. + - Only watch InfisicalSecrets in the specified namespace + - Only create/update Kubernetes secrets in that namespace + - Only access deployments in that namespace -```bash -# First namespace installation (with CRDs) -helm install operator-namespace1 infisical-helm-charts/secrets-operator \ - --namespace first-namespace \ - --set scopedNamespace=first-namespace \ - --set scopedRBAC=true + The default configuration gives cluster-wide access: -# Subsequent namespace installations -helm install operator-namespace2 infisical-helm-charts/secrets-operator \ - --namespace another-namespace \ - --set scopedNamespace=another-namespace \ - --set scopedRBAC=true \ - --set installCRDs=false -``` + ```yaml + installCRDs: true # Install CRDs (set to false for additional namespace installations) + scopedNamespace: "" # Empty for cluster-wide access + scopedRBAC: false # Cluster-wide permissions + ``` -When scoped to a namespace, the operator will: + If you want to install operators in multiple namespaces simultaneously: + - Make sure to set `installCRDs: false` for all but one of the installations to avoid conflicts, as CRDs are cluster-wide resources. + - Use unique release names for each installation (e.g., operator-namespace1, operator-namespace2). -- Only watch InfisicalSecrets in the specified namespace -- Only create/update Kubernetes secrets in that namespace -- Only access deployments in that namespace + + -The default configuration gives cluster-wide access: - -```yaml -installCRDs: true # Install CRDs (set to false for additional namespace installations) -scopedNamespace: "" # Empty for cluster-wide access -scopedRBAC: false # Cluster-wide permissions -``` - -If you want to install operators in multiple namespaces simultaneously: -- Make sure to set `installCRDs: false` for all but one of the installations to avoid conflicts, as CRDs are cluster-wide resources. -- Use unique release names for each installation (e.g., operator-namespace1, operator-namespace2). - - -## Custom Resource Definitions (CRD's) +## Custom Resource Definitions Currently the operator supports the following CRD's. We are constantly expanding the functionality of the operator, and this list will be updated as new CRD's are added. @@ -94,8 +96,8 @@ Currently the operator supports the following CRD's. We are constantly expanding 2. [InfisicalPushSecret](/integrations/platforms/kubernetes/infisical-push-secret-crd): Push secrets from a Kubernetes secret to Infisical. 3. [InfisicalDynamicSecret](/integrations/platforms/kubernetes/infisical-dynamic-secret-crd): Sync dynamic secrets and create leases automatically in Kubernetes. -## Connecting to instances with private/self-signed certificate - +## General Configuration +### Private/self-signed certificate To connect to Infisical instances behind a private/self-signed certificate, you can configure the TLS settings in the CRD to point to a CA certificate stored in a Kubernetes secret resource. @@ -190,13 +192,4 @@ The managed secret created by the operator will not be deleted when the operator helm uninstall ``` - - ``` - kubectl delete -f https://raw.githubusercontent.com/Infisical/infisical/main/k8-operator/kubectl-install/install-secrets-operator.yaml - ``` - - - -## Useful Articles - -- [Managing secrets in OpenShift with Infisical](https://xphyr.net/post/infisical_ocp/) + \ No newline at end of file From 1639bda3f64db3e4135de9f1be548adcd05263b1 Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Thu, 9 Jan 2025 12:23:46 -0500 Subject: [PATCH 31/32] remove copy-paste and make redeploy docs specific --- .../platforms/kubernetes/infisical-dynamic-secret-crd.mdx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx index f0efb85db..9b0d77e06 100644 --- a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx @@ -387,10 +387,10 @@ Once you have configured the `InfisicalDynamicSecret` CRD with the required fiel kubectl apply -f dynamic-secret-crd.yaml ``` -### Auto redeployment +## Auto redeployment -Deployments using managed secrets don't reload automatically on updates, so they may use outdated secrets unless manually redeployed. -To address this, we added functionality to automatically redeploy your deployment when its managed secret updates. +Deployments referring to Kubernetes secrets containing Infisical dynamic secrets don't automatically reload when the dynamic secret lease expires. This means your deployment may use expired dynamic secrets unless manually redeployed. +To address this, we've added functionality to automatically redeploy your deployment when the associated Kubernetes secret containing your Infisical dynamic secret updates. #### Enabling auto redeploy From 325ce73b9f04b6436229570aaa0541dbce27db6b Mon Sep 17 00:00:00 2001 From: Maidul Islam Date: Thu, 9 Jan 2025 12:36:52 -0500 Subject: [PATCH 32/32] fix typo --- .../platforms/kubernetes/infisical-dynamic-secret-crd.mdx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx index 9b0d77e06..9bdb43e6a 100644 --- a/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx +++ b/docs/integrations/platforms/kubernetes/infisical-dynamic-secret-crd.mdx @@ -5,12 +5,12 @@ description: "Learn how to generate dynamic secret leases in Infisical and sync --- ## Overview -The **InfisicalDynamicSecret** CRD allows you to easily create and manage dynamic secret leases in Infisical and automatically syncing them to your Kubernetes cluster as native **Kubernetes Secret** resources. +The **InfisicalDynamicSecret** CRD allows you to easily create and manage dynamic secret leases in Infisical and automatically sync them to your Kubernetes cluster as native **Kubernetes Secret** resources. This means any Pod, Deployment, or other Kubernetes resource can make use of dynamic secrets from Infisical just like any other K8s secret. This CRD offers the following features: - **Generate a dynamic secret lease** in Infisical and track its lifecycle. -- **write** the dynamic secret from Infisical to your cluster as native Kubernetes secret. +- **Write** the dynamic secret from Infisical to your cluster as native Kubernetes secret. - **Automatically rotate** the dyanmic secret value before it expires to make sure your cluster always has valid credentials. - **Optionally trigger redeployments** of any workloads that consume the secret if you enable auto-reload.