Merge pull request #3540 from Infisical/daniel/generators

feat(k8s): generator support
This commit is contained in:
Daniel Hougaard
2025-05-07 22:10:22 +04:00
committed by GitHub
20 changed files with 1143 additions and 79 deletions

View File

@@ -34,7 +34,7 @@ Before applying the InfisicalPushSecret CRD, you need to create a Kubernetes sec
metadata:
name: infisical-push-secret-demo
spec:
resyncInterval: 1m
resyncInterval: 1m # Remove this field to disable automatic reconciliation of the InfisicalPushSecret CRD.
hostAPI: https://app.infisical.com/api
# Optional, defaults to no replacement.
@@ -124,7 +124,9 @@ After applying the InfisicalPushSecret CRD, you should notice that the secrets y
<Accordion title="resyncInterval">
The `resyncInterval` is a string-formatted duration that defines the time between each resync.
The `resyncInterval` is a string-formatted duration that defines the time between each resync. The field is optional, and will default to no automatic resync if not defined.
If you don't want to automatically reconcile the InfisicalPushSecret CRD on an interval, you can remove the `resyncInterval` field entirely from your InfisicalPushSecret CRD.
The format of the field is `[duration][unit]` where `duration` is a number and `unit` is a string representing the unit of time.
@@ -459,6 +461,126 @@ Using Go templates, you can format, combine, and create new key-value pairs of s
Please refer to the [templating functions documentation](/integrations/platforms/kubernetes/overview#available-helper-functions) for more information.
</Accordion>
## Using generators to push secrets
Generators are a feature of the Infisical secrets operator that allows you to generate secrets on-reconcile and push them to Infisical. This is useful for secret rotation purposes, and fully operator-managed secrets.
A generator is a custom resource that is installed on the cluster that defines the logic for generating a secret.
Generators don't keep track of the secrets they generate, which means that on each reconciliation, a new value will be created and pushed.
For this reason you may want to disable automatic reconciliation of the InfisicalPushSecret CRD. You can do this by removing `resyncInterval` from the InfisicalPushSecret CRD.
**Supported generators**:
- `Password`: Generates a random password of string format.
- `UUID`: Generates a random v4 UUID.
To use a generator, you must specify at least one generator in the `push.generators[]` field. An example of a generator usage can be seen here:
<Accordion title="push.generators[]">
Define a generator in the `push.generators[]` field.
<Accordion title="push.generators[].destinationSecretName">
The name of the secret that will be created in Infisical.
</Accordion>
<Accordion title="push.generators[].generatorRef">
The reference to the generator resource.
Valid fields:
- `kind`: The kind of the generator resource, must match the generator kind.
- `name`: The name of the generator resource.
</Accordion>
</Accordion>
```yaml
push:
secret:
secretName: push-secret-source-secret
secretNamespace: dev
generators:
- destinationSecretName: password-generator # Name of the secret that will be created in Infisical
generatorRef:
kind: Password|UUID # Kind of the resource, must match the generator kind.
name: custom-generator # Name of the generator resource
```
<Tabs>
<Tab title="Password Generator">
The Password generator is a custom resource that is installed on the cluster that defines the logic for generating a password.
<Accordion title="Spec definition">
- `kind`: The kind of the generator resource, must match the generator kind. For the Password generator, the kind is `Password`.
- `generator.passwordSpec`: The spec of the password generator.
<Accordion title="generator.passwordSpec">
- `length`: The length of the password.
- `digits`: The number of digits in the password.
- `symbols`: The number of symbols in the password.
- `symbolCharacters`: The characters to use for the symbols in the password.
- `noUpper`: Whether to include uppercase letters in the password.
- `allowRepeat`: Whether to allow repeating characters in the password.
</Accordion>
</Accordion>
```yaml password-cluster-generator.yaml
apiVersion: secrets.infisical.com/v1alpha1
kind: ClusterGenerator
metadata:
name: password-generator
spec:
kind: Password
generator:
passwordSpec:
length: 10
digits: 5
symbols: 5
symbolCharacters: "-_$@"
noUpper: false
allowRepeat: true
```
Example InfisicalPushSecret CRD using the Password generator:
```yaml infisical-push-secret-crd.yaml
push:
generators:
- destinationSecretName: password-generator-test
generatorRef:
kind: Password
name: password-generator
```
</Tab>
<Tab title="UUID Generator">
The UUID generator is a custom resource that is installed on the cluster that defines the logic for generating a UUID.
<Accordion title="Spec definition">
- `kind`: The kind of the generator resource, must match the generator kind. For the UUID generator, the kind is `UUID`.
- `generator.uuidSpec`: The spec of the UUID generator. For UUID's, this can be left empty.
</Accordion>
```yaml uuid-cluster-generator.yaml
apiVersion: secrets.infisical.com/v1alpha1
kind: ClusterGenerator
metadata:
name: uuid-generator
spec:
kind: UUID
generator:
uuidSpec:
```
Example InfisicalPushSecret CRD using the UUID generator:
```yaml infisical-push-secret-crd.yaml
push:
generators:
- destinationSecretName: uuid-generator-test
generatorRef:
kind: UUID
name: uuid-generator
```
</Tab>
</Tabs>
## 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.

View File

@@ -0,0 +1,152 @@
/*
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"
)
// GeneratorKind represents a kind of generator.
// +kubebuilder:validation:Enum=Password;UUID
type GeneratorKind string
const (
GeneratorKindPassword GeneratorKind = "Password"
GeneratorKindUUID GeneratorKind = "UUID"
)
type ClusterGeneratorSpec struct {
// Kind the kind of this generator.
Kind GeneratorKind `json:"kind"`
// Generator the spec for this generator, must match the kind.
Generator GeneratorSpec `json:"generator,omitempty"`
}
type GeneratorSpec struct {
// +kubebuilder:validation:Optional
PasswordSpec *PasswordSpec `json:"passwordSpec,omitempty"`
// +kubebuilder:validation:Optional
UUIDSpec *UUIDSpec `json:"uuidSpec,omitempty"`
}
// ClusterGenerator represents a cluster-wide generator
// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Cluster
type ClusterGenerator struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec ClusterGeneratorSpec `json:"spec,omitempty"`
}
// +kubebuilder:object:root=true
// ClusterGeneratorList contains a list of ClusterGenerator resources.
type ClusterGeneratorList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []ClusterGenerator `json:"items"`
}
// ! UUID Generator
// UUIDSpec controls the behavior of the uuid generator.
type UUIDSpec struct{}
// UUID generates a version 4 UUID (e56657e3-764f-11ef-a397-65231a88c216).
// +kubebuilder:object:root=true
// +kubebuilder:subresource:status
type UUID struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec UUIDSpec `json:"spec,omitempty"`
}
// +kubebuilder:object:root=true
// UUIDList contains a list of UUID resources.
type UUIDList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []UUID `json:"items"`
}
// ! Password Generator
// PasswordSpec controls the behavior of the password generator.
type PasswordSpec struct {
// Length of the password to be generated.
// Defaults to 24
// +kubebuilder:validation:Optional
// +kubebuilder:default=24
Length int `json:"length"`
// digits specifies the number of digits in the generated
// password. If omitted it defaults to 25% of the length of the password
Digits *int `json:"digits,omitempty"`
// symbols specifies the number of symbol characters in the generated
// password. If omitted it defaults to 25% of the length of the password
Symbols *int `json:"symbols,omitempty"`
// symbolCharacters specifies the special characters that should be used
// in the generated password.
SymbolCharacters *string `json:"symbolCharacters,omitempty"`
// Set noUpper to disable uppercase characters
// +kubebuilder:validation:Optional
// +kubebuilder:default=false
NoUpper bool `json:"noUpper"`
// set allowRepeat to true to allow repeating characters.
// +kubebuilder:validation:Optional
// +kubebuilder:default=false
AllowRepeat bool `json:"allowRepeat"`
}
// Password generates a random password based on the
// configuration parameters in spec.
// You can specify the length, characterset and other attributes.
// +kubebuilder:object:root=true
// +kubebuilder:storageversion
// +kubebuilder:subresource:status
// +kubebuilder:resource:scope=Namespaced
type Password struct {
metav1.TypeMeta `json:",inline"`
metav1.ObjectMeta `json:"metadata,omitempty"`
Spec PasswordSpec `json:"spec,omitempty"`
}
// +kubebuilder:object:root=true
// PasswordList contains a list of Password resources.
type PasswordList struct {
metav1.TypeMeta `json:",inline"`
metav1.ListMeta `json:"metadata,omitempty"`
Items []Password `json:"items"`
}
func init() {
SchemeBuilder.Register(&Password{}, &PasswordList{})
SchemeBuilder.Register(&UUID{}, &UUIDList{})
SchemeBuilder.Register(&ClusterGenerator{}, &ClusterGeneratorList{})
}

View File

@@ -29,9 +29,28 @@ type InfisicalPushSecretSecretSource struct {
Template *SecretTemplate `json:"template,omitempty"`
}
type SecretPush struct {
type GeneratorRef struct {
// Specify the Kind of the generator resource
// +kubebuilder:validation:Enum=Password;UUID
// +kubebuilder:validation:Required
Secret InfisicalPushSecretSecretSource `json:"secret"`
Kind GeneratorKind `json:"kind"`
// +kubebuilder:validation:Required
Name string `json:"name"`
}
type SecretPushGenerator struct {
// +kubebuilder:validation:Required
DestinationSecretName string `json:"destinationSecretName"`
// +kubebuilder:validation:Required
GeneratorRef GeneratorRef `json:"generatorRef"`
}
type SecretPush struct {
// +kubebuilder:validation:Optional
Secret *InfisicalPushSecretSecretSource `json:"secret,omitempty"`
// +kubebuilder:validation:Optional
Generators []SecretPushGenerator `json:"generators,omitempty"`
}
// InfisicalPushSecretSpec defines the desired state of InfisicalPushSecret
@@ -52,7 +71,8 @@ type InfisicalPushSecretSpec struct {
// +kubebuilder:validation:Required
Push SecretPush `json:"push"`
ResyncInterval string `json:"resyncInterval"`
// +kubebuilder:validation:Optional
ResyncInterval *string `json:"resyncInterval,omitempty"`
// Infisical host to pull secrets from
// +kubebuilder:validation:Optional

View File

@@ -96,6 +96,80 @@ 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 *ClusterGenerator) DeepCopyInto(out *ClusterGenerator) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGenerator.
func (in *ClusterGenerator) DeepCopy() *ClusterGenerator {
if in == nil {
return nil
}
out := new(ClusterGenerator)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ClusterGenerator) 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 *ClusterGeneratorList) DeepCopyInto(out *ClusterGeneratorList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]ClusterGenerator, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGeneratorList.
func (in *ClusterGeneratorList) DeepCopy() *ClusterGeneratorList {
if in == nil {
return nil
}
out := new(ClusterGeneratorList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *ClusterGeneratorList) 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 *ClusterGeneratorSpec) DeepCopyInto(out *ClusterGeneratorSpec) {
*out = *in
in.Generator.DeepCopyInto(&out.Generator)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new ClusterGeneratorSpec.
func (in *ClusterGeneratorSpec) DeepCopy() *ClusterGeneratorSpec {
if in == nil {
return nil
}
out := new(ClusterGeneratorSpec)
in.DeepCopyInto(out)
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
@@ -143,6 +217,46 @@ 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 *GeneratorRef) DeepCopyInto(out *GeneratorRef) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratorRef.
func (in *GeneratorRef) DeepCopy() *GeneratorRef {
if in == nil {
return nil
}
out := new(GeneratorRef)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *GeneratorSpec) DeepCopyInto(out *GeneratorSpec) {
*out = *in
if in.PasswordSpec != nil {
in, out := &in.PasswordSpec, &out.PasswordSpec
*out = new(PasswordSpec)
(*in).DeepCopyInto(*out)
}
if in.UUIDSpec != nil {
in, out := &in.UUIDSpec, &out.UUIDSpec
*out = new(UUIDSpec)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratorSpec.
func (in *GeneratorSpec) DeepCopy() *GeneratorSpec {
if in == nil {
return nil
}
out := new(GeneratorSpec)
in.DeepCopyInto(out)
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
@@ -483,6 +597,11 @@ func (in *InfisicalPushSecretSpec) DeepCopyInto(out *InfisicalPushSecretSpec) {
out.Destination = in.Destination
in.Authentication.DeepCopyInto(&out.Authentication)
in.Push.DeepCopyInto(&out.Push)
if in.ResyncInterval != nil {
in, out := &in.ResyncInterval, &out.ResyncInterval
*out = new(string)
**out = **in
}
out.TLS = in.TLS
}
@@ -746,10 +865,107 @@ func (in *ManagedKubeSecretConfig) DeepCopy() *ManagedKubeSecretConfig {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *Password) DeepCopyInto(out *Password) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
in.Spec.DeepCopyInto(&out.Spec)
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Password.
func (in *Password) DeepCopy() *Password {
if in == nil {
return nil
}
out := new(Password)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *Password) 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 *PasswordList) DeepCopyInto(out *PasswordList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]Password, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PasswordList.
func (in *PasswordList) DeepCopy() *PasswordList {
if in == nil {
return nil
}
out := new(PasswordList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *PasswordList) 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 *PasswordSpec) DeepCopyInto(out *PasswordSpec) {
*out = *in
if in.Digits != nil {
in, out := &in.Digits, &out.Digits
*out = new(int)
**out = **in
}
if in.Symbols != nil {
in, out := &in.Symbols, &out.Symbols
*out = new(int)
**out = **in
}
if in.SymbolCharacters != nil {
in, out := &in.SymbolCharacters, &out.SymbolCharacters
*out = new(string)
**out = **in
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new PasswordSpec.
func (in *PasswordSpec) DeepCopy() *PasswordSpec {
if in == nil {
return nil
}
out := new(PasswordSpec)
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
in.Secret.DeepCopyInto(&out.Secret)
if in.Secret != nil {
in, out := &in.Secret, &out.Secret
*out = new(InfisicalPushSecretSecretSource)
(*in).DeepCopyInto(*out)
}
if in.Generators != nil {
in, out := &in.Generators, &out.Generators
*out = make([]SecretPushGenerator, len(*in))
copy(*out, *in)
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretPush.
@@ -762,6 +978,22 @@ 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 *SecretPushGenerator) DeepCopyInto(out *SecretPushGenerator) {
*out = *in
out.GeneratorRef = in.GeneratorRef
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SecretPushGenerator.
func (in *SecretPushGenerator) DeepCopy() *SecretPushGenerator {
if in == nil {
return nil
}
out := new(SecretPushGenerator)
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
@@ -848,6 +1080,79 @@ func (in *TLSConfig) DeepCopy() *TLSConfig {
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UUID) DeepCopyInto(out *UUID) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ObjectMeta.DeepCopyInto(&out.ObjectMeta)
out.Spec = in.Spec
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UUID.
func (in *UUID) DeepCopy() *UUID {
if in == nil {
return nil
}
out := new(UUID)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *UUID) 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 *UUIDList) DeepCopyInto(out *UUIDList) {
*out = *in
out.TypeMeta = in.TypeMeta
in.ListMeta.DeepCopyInto(&out.ListMeta)
if in.Items != nil {
in, out := &in.Items, &out.Items
*out = make([]UUID, len(*in))
for i := range *in {
(*in)[i].DeepCopyInto(&(*out)[i])
}
}
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UUIDList.
func (in *UUIDList) DeepCopy() *UUIDList {
if in == nil {
return nil
}
out := new(UUIDList)
in.DeepCopyInto(out)
return out
}
// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object.
func (in *UUIDList) 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 *UUIDSpec) DeepCopyInto(out *UUIDSpec) {
*out = *in
}
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new UUIDSpec.
func (in *UUIDSpec) DeepCopy() *UUIDSpec {
if in == nil {
return nil
}
out := new(UUIDSpec)
in.DeepCopyInto(out)
return out
}
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
func (in *UniversalAuthDetails) DeepCopyInto(out *UniversalAuthDetails) {
*out = *in

View File

@@ -0,0 +1,90 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.10.0
creationTimestamp: null
name: clustergenerators.secrets.infisical.com
spec:
group: secrets.infisical.com
names:
kind: ClusterGenerator
listKind: ClusterGeneratorList
plural: clustergenerators
singular: clustergenerator
scope: Cluster
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: ClusterGenerator represents a cluster-wide generator
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:
properties:
generator:
description: Generator the spec for this generator, must match the
kind.
properties:
passwordSpec:
description: PasswordSpec controls the behavior of the password
generator.
properties:
allowRepeat:
default: false
description: set allowRepeat to true to allow repeating characters.
type: boolean
digits:
description: digits specifies the number of digits in the
generated password. If omitted it defaults to 25% of the
length of the password
type: integer
length:
default: 24
description: Length of the password to be generated. Defaults
to 24
type: integer
noUpper:
default: false
description: Set noUpper to disable uppercase characters
type: boolean
symbolCharacters:
description: symbolCharacters specifies the special characters
that should be used in the generated password.
type: string
symbols:
description: symbols specifies the number of symbol characters
in the generated password. If omitted it defaults to 25%
of the length of the password
type: integer
type: object
uuidSpec:
description: UUIDSpec controls the behavior of the uuid generator.
type: object
type: object
kind:
description: Kind the kind of this generator.
enum:
- Password
- UUID
type: string
required:
- kind
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -142,6 +142,34 @@ spec:
type: string
push:
properties:
generators:
items:
properties:
destinationSecretName:
type: string
generatorRef:
properties:
kind:
allOf:
- enum:
- Password
- UUID
- enum:
- Password
- UUID
description: Specify the Kind of the generator resource
type: string
name:
type: string
required:
- kind
- name
type: object
required:
- destinationSecretName
- generatorRef
type: object
type: array
secret:
properties:
secretName:
@@ -168,8 +196,6 @@ spec:
- secretName
- secretNamespace
type: object
required:
- secret
type: object
resyncInterval:
type: string
@@ -200,7 +226,6 @@ spec:
required:
- destination
- push
- resyncInterval
type: object
status:
description: InfisicalPushSecretStatus defines the observed state of InfisicalPushSecret

View File

@@ -0,0 +1,69 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.10.0
creationTimestamp: null
name: passwords.secrets.infisical.com
spec:
group: secrets.infisical.com
names:
kind: Password
listKind: PasswordList
plural: passwords
singular: password
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: Password generates a random password based on the configuration
parameters in spec. You can specify the length, characterset and other attributes.
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: PasswordSpec controls the behavior of the password generator.
properties:
allowRepeat:
default: false
description: set allowRepeat to true to allow repeating characters.
type: boolean
digits:
description: digits specifies the number of digits in the generated
password. If omitted it defaults to 25% of the length of the password
type: integer
length:
default: 24
description: Length of the password to be generated. Defaults to 24
type: integer
noUpper:
default: false
description: Set noUpper to disable uppercase characters
type: boolean
symbolCharacters:
description: symbolCharacters specifies the special characters that
should be used in the generated password.
type: string
symbols:
description: symbols specifies the number of symbol characters in
the generated password. If omitted it defaults to 25% of the length
of the password
type: integer
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -0,0 +1,42 @@
---
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
annotations:
controller-gen.kubebuilder.io/version: v0.10.0
creationTimestamp: null
name: uuids.secrets.infisical.com
spec:
group: secrets.infisical.com
names:
kind: UUID
listKind: UUIDList
plural: uuids
singular: uuid
scope: Namespaced
versions:
- name: v1alpha1
schema:
openAPIV3Schema:
description: UUID generates a version 4 UUID (e56657e3-764f-11ef-a397-65231a88c216).
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: UUIDSpec controls the behavior of the uuid generator.
type: object
type: object
served: true
storage: true
subresources:
status: {}

View File

@@ -5,6 +5,7 @@ resources:
- bases/secrets.infisical.com_infisicalsecrets.yaml
- bases/secrets.infisical.com_infisicalpushsecrets.yaml
- bases/secrets.infisical.com_infisicaldynamicsecrets.yaml
- bases/secrets.infisical.com_clustergenerators.yaml
#+kubebuilder:scaffold:crdkustomizeresource
patchesStrategicMerge:

View File

@@ -74,6 +74,18 @@ rules:
- tokenreviews
verbs:
- create
- apiGroups:
- secrets.infisical.com
resources:
- clustergenerators
verbs:
- create
- delete
- get
- list
- patch
- update
- watch
- apiGroups:
- secrets.infisical.com
resources:

View File

@@ -0,0 +1,14 @@
apiVersion: secrets.infisical.com/v1alpha1
kind: ClusterGenerator
metadata:
name: password-generator
spec:
kind: Password
generator:
passwordSpec:
length: 10
digits: 5
symbols: 5
symbolCharacters: "-_$@"
noUpper: false
allowRepeat: true

View File

@@ -393,7 +393,7 @@ func (r *InfisicalDynamicSecretReconciler) ReconcileInfisicalDynamicSecret(ctx c
// Max TTL
if infisicalDynamicSecret.Status.MaxTTL != "" {
maxTTLDuration, err := util.ConvertIntervalToDuration(infisicalDynamicSecret.Status.MaxTTL)
maxTTLDuration, err := util.ConvertIntervalToDuration(&infisicalDynamicSecret.Status.MaxTTL)
if err != nil {
return defaultNextReconcile, fmt.Errorf("unable to parse MaxTTL duration: %w", err)
}

View File

@@ -51,7 +51,7 @@ func (r *InfisicalPushSecretReconciler) GetLogger(req ctrl.Request) logr.Logger
//+kubebuilder:rbac:groups="",resources=pods,verbs=get;list
//+kubebuilder:rbac:groups="authentication.k8s.io",resources=tokenreviews,verbs=create
//+kubebuilder:rbac:groups="",resources=serviceaccounts/token,verbs=create
// +kubebuilder:rbac:groups=secrets.infisical.com,resources=clustergenerators,verbs=get;list;watch;create;update;patch;delete
// 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:
@@ -108,23 +108,30 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl.
return ctrl.Result{}, nil
}
if infisicalPushSecretCRD.Spec.ResyncInterval != "" {
if infisicalPushSecretCRD.Spec.Push.Secret == nil && infisicalPushSecretCRD.Spec.Push.Generators == nil {
logger.Info("No secret or generators found, skipping reconciliation. Please define ")
return ctrl.Result{}, nil
}
duration, err := util.ConvertIntervalToDuration(infisicalPushSecretCRD.Spec.ResyncInterval)
duration, err := util.ConvertIntervalToDuration(infisicalPushSecretCRD.Spec.ResyncInterval)
if err != nil {
if err != nil {
// if resyncInterval is nil, we don't want to reconcile automatically
if infisicalPushSecretCRD.Spec.ResyncInterval != nil {
logger.Error(err, fmt.Sprintf("unable to convert resync interval to duration. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
} else {
logger.Error(err, "unable to convert resync interval to duration")
return ctrl.Result{}, err
}
}
requeueTime = duration
requeueTime = duration
if requeueTime != 0 {
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
@@ -137,10 +144,15 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl.
// 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 requeueTime != 0 {
logger.Error(err, fmt.Sprintf("unable to fetch infisical-config. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
} else {
logger.Error(err, "unable to fetch infisical-config")
return ctrl.Result{}, err
}
}
if infisicalPushSecretCRD.Spec.HostAPI == "" {
@@ -152,10 +164,15 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl.
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{
RequeueAfter: requeueTime,
}, nil
if requeueTime != 0 {
logger.Error(err, fmt.Sprintf("unable to fetch CA certificate. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
} else {
logger.Error(err, "unable to fetch CA certificate")
return ctrl.Result{}, err
}
}
logger.Info("Using custom CA certificate...")
@@ -167,17 +184,27 @@ func (r *InfisicalPushSecretReconciler) Reconcile(ctx context.Context, req ctrl.
r.SetReconcileStatusCondition(ctx, &infisicalPushSecretCRD, 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
if requeueTime != 0 {
logger.Error(err, fmt.Sprintf("unable to reconcile Infisical Push Secret. Will requeue after [requeueTime=%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
} else {
logger.Error(err, "unable to reconcile Infisical Push Secret")
return ctrl.Result{}, err
}
}
// Sync again after the specified time
logger.Info(fmt.Sprintf("Operator will requeue after [%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
if requeueTime != 0 {
logger.Info(fmt.Sprintf("Operator will requeue after [%v]", requeueTime))
return ctrl.Result{
RequeueAfter: requeueTime,
}, nil
} else {
logger.Info("Operator will reconcile on next spec change")
return ctrl.Result{}, nil
}
}
func (r *InfisicalPushSecretReconciler) SetupWithManager(mgr ctrl.Manager) error {
@@ -228,27 +255,67 @@ func (r *InfisicalPushSecretReconciler) SetupWithManager(mgr ctrl.Manager) error
)).
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
}),
handler.EnqueueRequestsFromMapFunc(r.findPushSecretsForSecret),
).
Watches(
&source.Kind{Type: &secretsv1alpha1.ClusterGenerator{}},
handler.EnqueueRequestsFromMapFunc(r.findPushSecretsForClusterGenerator),
).
Complete(r)
}
func (r *InfisicalPushSecretReconciler) findPushSecretsForClusterGenerator(o client.Object) []reconcile.Request {
ctx := context.Background()
pushSecrets := &secretsv1alpha1.InfisicalPushSecretList{}
if err := r.List(ctx, pushSecrets); err != nil {
return []reconcile.Request{}
}
clusterGenerator, ok := o.(*secretsv1alpha1.ClusterGenerator)
if !ok {
return []reconcile.Request{}
}
requests := []reconcile.Request{}
for _, pushSecret := range pushSecrets.Items {
if pushSecret.Spec.Push.Generators != nil {
for _, generator := range pushSecret.Spec.Push.Generators {
if generator.GeneratorRef.Name == clusterGenerator.GetName() {
requests = append(requests, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: pushSecret.GetName(),
Namespace: pushSecret.GetNamespace(),
},
})
break
}
}
}
}
return requests
}
func (r *InfisicalPushSecretReconciler) findPushSecretsForSecret(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 != nil &&
pushSecret.Spec.Push.Secret.SecretName == o.GetName() &&
pushSecret.Spec.Push.Secret.SecretNamespace == o.GetNamespace() {
requests = append(requests, reconcile.Request{
NamespacedName: types.NamespacedName{
Name: pushSecret.GetName(),
Namespace: pushSecret.GetNamespace(),
},
})
}
}
return requests
}

View File

@@ -19,6 +19,7 @@ import (
"k8s.io/apimachinery/pkg/types"
"sigs.k8s.io/controller-runtime/pkg/client"
generatorUtil "github.com/Infisical/infisical/k8-operator/packages/generator"
infisicalSdk "github.com/infisical/go-sdk"
k8Errors "k8s.io/apimachinery/pkg/api/errors"
)
@@ -106,6 +107,52 @@ func (r *InfisicalPushSecretReconciler) updateResourceVariables(infisicalPushSec
infisicalPushSecretResourceVariablesMap[string(infisicalPushSecret.UID)] = resourceVariables
}
func (r *InfisicalPushSecretReconciler) processGenerators(ctx context.Context, infisicalPushSecret v1alpha1.InfisicalPushSecret) (map[string]string, error) {
processedSecrets := make(map[string]string)
if len(infisicalPushSecret.Spec.Push.Generators) == 0 {
return processedSecrets, nil
}
for _, generator := range infisicalPushSecret.Spec.Push.Generators {
generatorRef := generator.GeneratorRef
clusterGenerator := &v1alpha1.ClusterGenerator{}
err := r.Client.Get(ctx, types.NamespacedName{Name: generatorRef.Name}, clusterGenerator)
if err != nil {
return nil, fmt.Errorf("unable to get ClusterGenerator resource [err=%s]", err)
}
if generatorRef.Kind == v1alpha1.GeneratorKindPassword {
// get the custom ClusterGenerator resource from the cluster
if clusterGenerator.Spec.Generator.PasswordSpec == nil {
return nil, fmt.Errorf("password spec is not defined in the ClusterGenerator resource")
}
password, err := generatorUtil.GeneratorPassword(*clusterGenerator.Spec.Generator.PasswordSpec)
if err != nil {
return nil, fmt.Errorf("unable to generate password [err=%s]", err)
}
processedSecrets[generator.DestinationSecretName] = password
}
if generatorRef.Kind == v1alpha1.GeneratorKindUUID {
uuid, err := generatorUtil.GeneratorUUID()
if err != nil {
return nil, fmt.Errorf("unable to generate UUID [err=%s]", err)
}
processedSecrets[generator.DestinationSecretName] = uuid
}
}
return processedSecrets, nil
}
func (r *InfisicalPushSecretReconciler) processTemplatedSecrets(infisicalPushSecret v1alpha1.InfisicalPushSecret, kubePushSecret *corev1.Secret, destination v1alpha1.InfisicalPushSecretDestination) (map[string]string, error) {
processedSecrets := make(map[string]string)
@@ -172,18 +219,31 @@ func (r *InfisicalPushSecretReconciler) ReconcileInfisicalPushSecret(ctx context
})
}
kubePushSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
Namespace: infisicalPushSecret.Spec.Push.Secret.SecretNamespace,
Name: infisicalPushSecret.Spec.Push.Secret.SecretName,
})
processedSecrets := make(map[string]string)
if err != nil {
return fmt.Errorf("unable to fetch kube secret [err=%s]", err)
if infisicalPushSecret.Spec.Push.Secret != nil {
kubePushSecret, err := util.GetKubeSecretByNamespacedName(ctx, r.Client, types.NamespacedName{
Namespace: infisicalPushSecret.Spec.Push.Secret.SecretNamespace,
Name: infisicalPushSecret.Spec.Push.Secret.SecretName,
})
if err != nil {
return fmt.Errorf("unable to fetch kube secret [err=%s]", err)
}
processedSecrets, err = r.processTemplatedSecrets(infisicalPushSecret, kubePushSecret, infisicalPushSecret.Spec.Destination)
if err != nil {
return fmt.Errorf("unable to process templated secrets [err=%s]", err)
}
}
processedSecrets, err := r.processTemplatedSecrets(infisicalPushSecret, kubePushSecret, infisicalPushSecret.Spec.Destination)
generatorSecrets, err := r.processGenerators(ctx, infisicalPushSecret)
if err != nil {
return fmt.Errorf("unable to process templated secrets [err=%s]", err)
return fmt.Errorf("unable to process generators [err=%s]", err)
}
for key, value := range generatorSecrets {
processedSecrets[key] = value
}
destination := infisicalPushSecret.Spec.Destination

View File

@@ -4,10 +4,12 @@ go 1.21
require (
github.com/Masterminds/sprig/v3 v3.3.0
github.com/aws/smithy-go v1.20.3
github.com/infisical/go-sdk v0.4.4
github.com/lestrrat-go/jwx/v2 v2.1.4
github.com/onsi/ginkgo/v2 v2.6.0
github.com/onsi/gomega v1.24.1
github.com/sethvargo/go-password v0.3.1
k8s.io/apimachinery v0.26.1
k8s.io/client-go v0.26.1
sigs.k8s.io/controller-runtime v0.14.4
@@ -34,7 +36,6 @@ require (
github.com/aws/aws-sdk-go-v2/service/sso v1.22.1 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.2 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.30.1 // indirect
github.com/aws/smithy-go v1.20.3 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
@@ -85,7 +86,7 @@ require (
github.com/google/gnostic v0.5.7-v3refs // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/google/uuid v1.6.0
github.com/imdario/mergo v0.3.12 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect

View File

@@ -338,6 +338,8 @@ github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZV
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/segmentio/asm v1.2.0 h1:9BQrFxC+YOHJlTlHGkTrFWf59nbL3XnCoFLTwDCI7ys=
github.com/segmentio/asm v1.2.0/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sethvargo/go-password v0.3.1 h1:WqrLTjo7X6AcVYfC6R7GtSyuUQR9hGyAj/f1PYQZCJU=
github.com/sethvargo/go-password v0.3.1/go.mod h1:rXofC1zT54N7R8K/h1WDUdkf9BOx5OptoxrMBcrXzvs=
github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k=
github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME=
github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo=

View File

@@ -0,0 +1 @@
package generator

View File

@@ -0,0 +1,76 @@
package generator
import (
"github.com/Infisical/infisical/k8-operator/api/v1alpha1"
"github.com/sethvargo/go-password/password"
)
const (
defaultLength = 24
defaultSymbolChars = "~!@#$%^&*()_+`-={}|[]\\:\"<>?,./"
digitFactor = 0.25
symbolFactor = 0.25
)
func generateSafePassword(
passLen int,
symbols int,
symbolCharacters string,
digits int,
noUpper bool,
allowRepeat bool,
) (string, error) {
gen, err := password.NewGenerator(&password.GeneratorInput{
Symbols: symbolCharacters,
})
if err != nil {
return "", err
}
return gen.Generate(
passLen,
digits,
symbols,
noUpper,
allowRepeat,
)
}
func GeneratorPassword(spec v1alpha1.PasswordSpec) (string, error) {
symbolCharacters := defaultSymbolChars
if spec.SymbolCharacters != nil && *spec.SymbolCharacters != "" {
symbolCharacters = *spec.SymbolCharacters
}
passwordLength := defaultLength
if spec.Length != 0 {
passwordLength = spec.Length
}
digits := int(float32(passwordLength) * digitFactor)
if spec.Digits != nil {
digits = *spec.Digits
}
symbols := int(float32(passwordLength) * symbolFactor)
if spec.Symbols != nil {
symbols = *spec.Symbols
}
pass, err := generateSafePassword(
passwordLength,
symbols,
symbolCharacters,
digits,
spec.NoUpper,
spec.AllowRepeat,
)
if err != nil {
return "", err
}
return pass, nil
}

View File

@@ -0,0 +1,10 @@
package generator
import (
"github.com/google/uuid"
)
func GeneratorUUID() (string, error) {
uuid := uuid.New().String()
return uuid, nil
}

View File

@@ -7,14 +7,19 @@ import (
"time"
)
func ConvertIntervalToDuration(resyncInterval string) (time.Duration, error) {
length := len(resyncInterval)
func ConvertIntervalToDuration(resyncInterval *string) (time.Duration, error) {
if resyncInterval == nil || *resyncInterval == "" {
return 0, nil
}
length := len(*resyncInterval)
if length < 2 {
return 0, fmt.Errorf("invalid format")
}
unit := resyncInterval[length-1:]
numberPart := resyncInterval[:length-1]
unit := (*resyncInterval)[length-1:]
numberPart := (*resyncInterval)[:length-1]
number, err := strconv.Atoi(numberPart)
if err != nil {
@@ -40,16 +45,6 @@ func ConvertIntervalToDuration(resyncInterval string) (time.Duration, error) {
}
}
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