diff --git a/charts/controller/crds/deployments.plural.sh_generatedsecrets.yaml b/charts/controller/crds/deployments.plural.sh_generatedsecrets.yaml new file mode 100644 index 0000000000..eb6aadcd7d --- /dev/null +++ b/charts/controller/crds/deployments.plural.sh_generatedsecrets.yaml @@ -0,0 +1,146 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.3 + name: generatedsecrets.deployments.plural.sh +spec: + group: deployments.plural.sh + names: + kind: GeneratedSecret + listKind: GeneratedSecretList + plural: generatedsecrets + singular: generatedsecret + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: GeneratedSecret is the Schema for the generatedsecrets 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: GeneratedSecretSpec defines the desired state of GeneratedSecret + properties: + destinations: + description: Destinations describe name/namespace for the secrets. + items: + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + type: array + template: + additionalProperties: + type: string + description: Template secret data in string form. + type: object + type: object + status: + properties: + conditions: + description: Represents the observations of a PrAutomation's current + state. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + 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. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + id: + description: ID of the resource in the Console API. + type: string + renderedTemplateSecretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + sha: + description: SHA of last applied configuration. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/go/controller/api/v1alpha1/generatedsecret_types.go b/go/controller/api/v1alpha1/generatedsecret_types.go new file mode 100644 index 0000000000..bc121612be --- /dev/null +++ b/go/controller/api/v1alpha1/generatedsecret_types.go @@ -0,0 +1,61 @@ +package v1alpha1 + +import ( + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/meta" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// GeneratedSecretSpec defines the desired state of GeneratedSecret +type GeneratedSecretSpec struct { + // Template secret data in string form. + Template map[string]string `json:"template,omitempty"` + // Destinations describe name/namespace for the secrets. + Destinations []GeneratedSecretDestination `json:"destinations,omitempty"` +} + +type GeneratedSecretDestination struct { + Name string `json:"name"` + Namespace string `json:"namespace,omitempty"` +} + +type GeneratedSecretStatus struct { + Status `json:",inline"` + RenderedTemplateSecretRef *corev1.LocalObjectReference `json:"renderedTemplateSecretRef,omitempty"` +} + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:resource:scope=Namespaced + +// GeneratedSecret is the Schema for the generatedsecrets API +type GeneratedSecret struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec GeneratedSecretSpec `json:"spec,omitempty"` + Status GeneratedSecretStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// GeneratedSecretList contains a list of GeneratedSecret +type GeneratedSecretList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []GeneratedSecret `json:"items"` +} + +func (in *GeneratedSecret) SetCondition(condition metav1.Condition) { + meta.SetStatusCondition(&in.Status.Conditions, condition) +} + +func (in *GeneratedSecret) GetSecretName() string { + return fmt.Sprintf("gs-%s", in.Name) +} + +func init() { + SchemeBuilder.Register(&GeneratedSecret{}, &GeneratedSecretList{}) +} diff --git a/go/controller/api/v1alpha1/zz_generated.deepcopy.go b/go/controller/api/v1alpha1/zz_generated.deepcopy.go index 22ff9c4e9f..79f02097b1 100644 --- a/go/controller/api/v1alpha1/zz_generated.deepcopy.go +++ b/go/controller/api/v1alpha1/zz_generated.deepcopy.go @@ -1462,6 +1462,128 @@ func (in *GateSpec) DeepCopy() *GateSpec { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GeneratedSecret) DeepCopyInto(out *GeneratedSecret) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ObjectMeta.DeepCopyInto(&out.ObjectMeta) + in.Spec.DeepCopyInto(&out.Spec) + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratedSecret. +func (in *GeneratedSecret) DeepCopy() *GeneratedSecret { + if in == nil { + return nil + } + out := new(GeneratedSecret) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GeneratedSecret) 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 *GeneratedSecretDestination) DeepCopyInto(out *GeneratedSecretDestination) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratedSecretDestination. +func (in *GeneratedSecretDestination) DeepCopy() *GeneratedSecretDestination { + if in == nil { + return nil + } + out := new(GeneratedSecretDestination) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GeneratedSecretList) DeepCopyInto(out *GeneratedSecretList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]GeneratedSecret, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratedSecretList. +func (in *GeneratedSecretList) DeepCopy() *GeneratedSecretList { + if in == nil { + return nil + } + out := new(GeneratedSecretList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *GeneratedSecretList) 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 *GeneratedSecretSpec) DeepCopyInto(out *GeneratedSecretSpec) { + *out = *in + if in.Template != nil { + in, out := &in.Template, &out.Template + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } + if in.Destinations != nil { + in, out := &in.Destinations, &out.Destinations + *out = make([]GeneratedSecretDestination, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratedSecretSpec. +func (in *GeneratedSecretSpec) DeepCopy() *GeneratedSecretSpec { + if in == nil { + return nil + } + out := new(GeneratedSecretSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *GeneratedSecretStatus) DeepCopyInto(out *GeneratedSecretStatus) { + *out = *in + in.Status.DeepCopyInto(&out.Status) + if in.RenderedTemplateSecretRef != nil { + in, out := &in.RenderedTemplateSecretRef, &out.RenderedTemplateSecretRef + *out = new(v1.LocalObjectReference) + **out = **in + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new GeneratedSecretStatus. +func (in *GeneratedSecretStatus) DeepCopy() *GeneratedSecretStatus { + if in == nil { + return nil + } + out := new(GeneratedSecretStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *GitRef) DeepCopyInto(out *GitRef) { *out = *in diff --git a/go/controller/config/crd/bases/deployments.plural.sh_generatedsecrets.yaml b/go/controller/config/crd/bases/deployments.plural.sh_generatedsecrets.yaml new file mode 100644 index 0000000000..eb6aadcd7d --- /dev/null +++ b/go/controller/config/crd/bases/deployments.plural.sh_generatedsecrets.yaml @@ -0,0 +1,146 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.3 + name: generatedsecrets.deployments.plural.sh +spec: + group: deployments.plural.sh + names: + kind: GeneratedSecret + listKind: GeneratedSecretList + plural: generatedsecrets + singular: generatedsecret + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: GeneratedSecret is the Schema for the generatedsecrets 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: GeneratedSecretSpec defines the desired state of GeneratedSecret + properties: + destinations: + description: Destinations describe name/namespace for the secrets. + items: + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + type: array + template: + additionalProperties: + type: string + description: Template secret data in string form. + type: object + type: object + status: + properties: + conditions: + description: Represents the observations of a PrAutomation's current + state. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + 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. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + id: + description: ID of the resource in the Console API. + type: string + renderedTemplateSecretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + sha: + description: SHA of last applied configuration. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/go/controller/config/rbac/generatedsecret_editor_role.yaml b/go/controller/config/rbac/generatedsecret_editor_role.yaml new file mode 100644 index 0000000000..e7b7a7368a --- /dev/null +++ b/go/controller/config/rbac/generatedsecret_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit generatedsecrets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: generatedsecret-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: controller + app.kubernetes.io/part-of: controller + app.kubernetes.io/managed-by: kustomize + name: generatedsecret-editor-role +rules: +- apiGroups: + - deployments.plural.sh + resources: + - generatedsecrets + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - deployments.plural.sh + resources: + - generatedsecrets/status + verbs: + - get diff --git a/go/controller/config/rbac/generatedsecret_viewer_role.yaml b/go/controller/config/rbac/generatedsecret_viewer_role.yaml new file mode 100644 index 0000000000..f6b43467a6 --- /dev/null +++ b/go/controller/config/rbac/generatedsecret_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view generatedsecrets. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: generatedsecret-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: controller + app.kubernetes.io/part-of: controller + app.kubernetes.io/managed-by: kustomize + name: generatedsecret-viewer-role +rules: +- apiGroups: + - deployments.plural.sh + resources: + - generatedsecrets + verbs: + - get + - list + - watch +- apiGroups: + - deployments.plural.sh + resources: + - generatedsecrets/status + verbs: + - get diff --git a/go/controller/config/rbac/role.yaml b/go/controller/config/rbac/role.yaml index 857c8fa1f8..f37a25a0a7 100644 --- a/go/controller/config/rbac/role.yaml +++ b/go/controller/config/rbac/role.yaml @@ -32,6 +32,7 @@ rules: - clusters - customstackruns - deploymentsettings + - generatedsecrets - gitrepositories - globalservices - helmrepositories @@ -70,6 +71,7 @@ rules: - clusters/finalizers - customstackruns/finalizers - deploymentsettings/finalizers + - generatedsecrets/finalizers - gitrepositories/finalizers - globalservices/finalizers - helmrepositories/finalizers @@ -102,6 +104,7 @@ rules: - clusters/status - customstackruns/status - deploymentsettings/status + - generatedsecrets/status - gitrepositories/status - globalservices/status - helmrepositories/status diff --git a/go/controller/config/samples/deployments_v1alpha1_generatedsecret.yaml b/go/controller/config/samples/deployments_v1alpha1_generatedsecret.yaml new file mode 100644 index 0000000000..3d3d0de7dd --- /dev/null +++ b/go/controller/config/samples/deployments_v1alpha1_generatedsecret.yaml @@ -0,0 +1,16 @@ +apiVersion: deployments.plural.sh/v1alpha1 +kind: GeneratedSecret +metadata: + labels: + app.kubernetes.io/name: generatedsecret + app.kubernetes.io/instance: generatedsecret-sample + app.kubernetes.io/part-of: controller + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: controller + name: generatedsecret-sample +spec: + template: + password: "{{ 10 | randAlphaNum }}" + destinations: + - name: test + namespace: default diff --git a/go/controller/docs/api.md b/go/controller/docs/api.md index 460dedbc00..e6e62161a7 100644 --- a/go/controller/docs/api.md +++ b/go/controller/docs/api.md @@ -15,6 +15,7 @@ Package v1alpha1 contains API Schema definitions for the deployments v1alpha1 AP - [ClusterRestoreTrigger](#clusterrestoretrigger) - [CustomStackRun](#customstackrun) - [DeploymentSettings](#deploymentsettings) +- [GeneratedSecret](#generatedsecret) - [GitRepository](#gitrepository) - [GlobalService](#globalservice) - [HelmRepository](#helmrepository) @@ -768,6 +769,60 @@ _Appears in:_ | `job` _[JobSpec](#jobspec)_ | | | Optional: {}
| +#### GeneratedSecret + + + +GeneratedSecret is the Schema for the generatedsecrets API + + + + + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `apiVersion` _string_ | `deployments.plural.sh/v1alpha1` | | | +| `kind` _string_ | `GeneratedSecret` | | | +| `metadata` _[ObjectMeta](https://kubernetes.io/docs/reference/generated/kubernetes-api/v1.29/#objectmeta-v1-meta)_ | Refer to Kubernetes API documentation for fields of `metadata`. | | | +| `spec` _[GeneratedSecretSpec](#generatedsecretspec)_ | | | | + + +#### GeneratedSecretDestination + + + + + + + +_Appears in:_ +- [GeneratedSecretSpec](#generatedsecretspec) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `name` _string_ | | | | +| `namespace` _string_ | | | | + + +#### GeneratedSecretSpec + + + +GeneratedSecretSpec defines the desired state of GeneratedSecret + + + +_Appears in:_ +- [GeneratedSecret](#generatedsecret) + +| Field | Description | Default | Validation | +| --- | --- | --- | --- | +| `template` _object (keys:string, values:string)_ | Template secret data in string form. | | | +| `destinations` _[GeneratedSecretDestination](#generatedsecretdestination) array_ | Destinations describe name/namespace for the secrets. | | | + + + + #### GitHealth _Underlying type:_ _string_ @@ -2662,6 +2717,7 @@ _Appears in:_ _Appears in:_ - [ClusterStatus](#clusterstatus) +- [GeneratedSecretStatus](#generatedsecretstatus) - [GitRepositoryStatus](#gitrepositorystatus) - [ServiceStatus](#servicestatus) diff --git a/go/controller/go.mod b/go/controller/go.mod index 220d03b4ba..571cd48bce 100644 --- a/go/controller/go.mod +++ b/go/controller/go.mod @@ -9,8 +9,8 @@ require ( github.com/onsi/gomega v1.33.1 github.com/orcaman/concurrent-map/v2 v2.0.1 github.com/pluralsh/console/go/client v0.0.0-00010101000000-000000000000 - github.com/pluralsh/polly v0.1.10 - github.com/samber/lo v1.46.0 + github.com/pluralsh/polly v0.1.11 + github.com/samber/lo v1.47.0 github.com/spf13/pflag v1.0.5 github.com/stretchr/testify v1.9.0 go.uber.org/zap v1.27.0 @@ -25,8 +25,11 @@ require ( require k8s.io/client-go v0.30.0 require ( + dario.cat/mergo v1.0.1 // indirect github.com/99designs/gqlgen v0.17.49 // indirect - github.com/Masterminds/semver/v3 v3.2.1 // indirect + github.com/Masterminds/goutils v1.1.1 // indirect + github.com/Masterminds/semver/v3 v3.3.0 // indirect + github.com/Masterminds/sprig/v3 v3.3.0 // indirect github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect @@ -51,15 +54,19 @@ require ( github.com/google/gofuzz v1.2.0 // indirect github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af // indirect github.com/google/uuid v1.6.0 // indirect - github.com/huandu/xstrings v1.4.0 // indirect + github.com/huandu/xstrings v1.5.0 // indirect github.com/imdario/mergo v0.3.16 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/mailru/easyjson v0.7.7 // indirect github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/osteele/liquid v1.4.0 // indirect + github.com/osteele/tuesday v1.0.3 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/prometheus/client_golang v1.18.0 // indirect @@ -67,16 +74,19 @@ require ( github.com/prometheus/common v0.45.0 // indirect github.com/prometheus/procfs v0.12.0 // indirect github.com/rogpeppe/go-internal v1.12.0 // indirect + github.com/shopspring/decimal v1.4.0 // indirect github.com/sosodev/duration v1.3.1 // indirect + github.com/spf13/cast v1.7.0 // indirect github.com/stretchr/objx v0.5.2 // indirect github.com/vektah/gqlparser/v2 v2.5.16 // indirect go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.27.0 // indirect golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect golang.org/x/net v0.26.0 // indirect golang.org/x/oauth2 v0.18.0 // indirect - golang.org/x/sys v0.21.0 // indirect - golang.org/x/term v0.21.0 // indirect - golang.org/x/text v0.16.0 // indirect + golang.org/x/sys v0.25.0 // indirect + golang.org/x/term v0.24.0 // indirect + golang.org/x/text v0.18.0 // indirect golang.org/x/time v0.5.0 // indirect golang.org/x/tools v0.22.0 // indirect gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect diff --git a/go/controller/go.sum b/go/controller/go.sum index 6b6e7ede8b..1387fcdf41 100644 --- a/go/controller/go.sum +++ b/go/controller/go.sum @@ -1,13 +1,15 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= github.com/99designs/gqlgen v0.17.49 h1:b3hNGexHd33fBSAd4NDT/c3NCcQzcAVkknhN9ym36YQ= github.com/99designs/gqlgen v0.17.49/go.mod h1:tC8YFVZMed81x7UJ7ORUwXF4Kn6SXuucFqQBhN8+BU0= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Masterminds/goutils v1.1.1 h1:5nUrii3FMTL5diU80unEVvNevw1nH4+ZV4DSLVJLSYI= github.com/Masterminds/goutils v1.1.1/go.mod h1:8cTjp+g8YejhMuvIA5y2vz3BpJxksy863GQaJW2MFNU= -github.com/Masterminds/semver/v3 v3.2.1 h1:RN9w6+7QoMeJVGyfmbcgs28Br8cvmnucEXnY0rYXWg0= -github.com/Masterminds/semver/v3 v3.2.1/go.mod h1:qvl/7zhW3nngYb5+80sSMF+FG2BjYrf8m9wsX0PNOMQ= -github.com/Masterminds/sprig/v3 v3.2.3 h1:eL2fZNezLomi0uOLqjQoN6BfsDD+fyLtgbJMAj9n6YA= -github.com/Masterminds/sprig/v3 v3.2.3/go.mod h1:rXcFaZ2zZbLRJv/xSysmlgIM1u11eBaRMhvYXJNkGuM= +github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= +github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= +github.com/Masterminds/sprig/v3 v3.3.0 h1:mQh0Yrg1XPo6vjYXgtf5OtijNAKJRNcTdOOGZe3tPhs= +github.com/Masterminds/sprig/v3 v3.3.0/go.mod h1:Zy1iXRYNqNLUolqCpL4uhk6SHUMAOSCzdgBfDb35Lz0= github.com/Yamashou/gqlgenc v0.23.2 h1:WPxYPrwc6W4Z1eY4qKxoH3nb5PC4jAMWqQA0G8toQMI= github.com/Yamashou/gqlgenc v0.23.2/go.mod h1:oMc4EQBQeDwLIODvgcvpaSp6rO+KMf47FuOhplv5D3A= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= @@ -41,6 +43,8 @@ github.com/evanphx/json-patch v5.7.0+incompatible h1:vgGkfT/9f8zE6tvSCe74nfpAVDQ github.com/evanphx/json-patch v5.7.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.9.0 h1:kcBlZQbplgElYIlo/n1hJbls2z/1awpXxpRi0/FOJfg= github.com/evanphx/json-patch/v5 v5.9.0/go.mod h1:VNkHZ/282BpEyt/tObQO8s5CMPmYYq14uClGH4abBuQ= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= github.com/go-logr/logr v0.1.0/go.mod h1:ixOQHD9gLJUVQQ2ZOR7zLEifBX6tGkNJF4QyIY7sIas= @@ -80,8 +84,8 @@ github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af h1:kmjWCqn2qkEml422C2 github.com/google/pprof v0.0.0-20240525223248-4bfdf5a9a2af/go.mod h1:K1liHPHnj73Fdn/EKuT8nrFqBihUSKXoLYU0BuatOYo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/huandu/xstrings v1.4.0 h1:D17IlohoQq4UcpqD7fDk80P7l+lwAmlFaBHgOipl2FU= -github.com/huandu/xstrings v1.4.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= +github.com/huandu/xstrings v1.5.0 h1:2ag3IFq9ZDANvthTwTiqSSZLjDc+BedvHPAp5tJy2TI= +github.com/huandu/xstrings v1.5.0/go.mod h1:y5/lhBue+AyNmUVz9RLU9xbLR0o4KIIExikq4ovT0aE= github.com/imdario/mergo v0.3.16 h1:wwQJbIsHYGMUyLSPrEq1CT16AhnhNJQ51+4fdHUnCl4= github.com/imdario/mergo v0.3.16/go.mod h1:WBLT9ZmE3lPoWsEzCh9LPo3TiwVN+ZKEjmz+hD27ysY= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= @@ -120,10 +124,14 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/orcaman/concurrent-map/v2 v2.0.1 h1:jOJ5Pg2w1oeB6PeDurIYf6k9PQ+aTITr/6lP/L/zp6c= github.com/orcaman/concurrent-map/v2 v2.0.1/go.mod h1:9Eq3TG2oBe5FirmYWQfYO5iH1q0Jv47PLaNK++uCdOM= +github.com/osteele/liquid v1.4.0 h1:WS6lT3MFWUAxNbveF22tMLluOWNghGnKCZHLn7NbJGs= +github.com/osteele/liquid v1.4.0/go.mod h1:VmzQQHa5v4E0GvGzqccfAfLgMwRk2V+s1QbxYx9dGak= +github.com/osteele/tuesday v1.0.3 h1:SrCmo6sWwSgnvs1bivmXLvD7Ko9+aJvvkmDjB5G4FTU= +github.com/osteele/tuesday v1.0.3/go.mod h1:pREKpE+L03UFuR+hiznj3q7j3qB1rUZ4XfKejwWFF2M= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pluralsh/polly v0.1.10 h1:8Or7SPy7OCbQiAU2Fu7vMUsyZ88WgF66Wwjx9aVus9c= -github.com/pluralsh/polly v0.1.10/go.mod h1:W9IBX3e3xEjJuRjAQRfFJpH+UkNjddVY5YjMhyisQqQ= +github.com/pluralsh/polly v0.1.11 h1:xSpgVr+VY4GLHUM9BeWSXTbCHX/s4xWOqJOkVMbiDgM= +github.com/pluralsh/polly v0.1.11/go.mod h1:o+oi+FXNThipKNyZsqCemqwY0JdkvhEjQqi8aTw8l4M= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= @@ -137,16 +145,16 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= -github.com/samber/lo v1.46.0 h1:w8G+oaCPgz1PoCJztqymCFaKwXt+5cCXn51uPxExFfQ= -github.com/samber/lo v1.46.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= +github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc= +github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4= github.com/sosodev/duration v1.3.1/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= -github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0= -github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cast v1.7.0 h1:ntdiHjuueXFgm5nzDRdOS4yfT43P5Fnud6DH50rz/7w= +github.com/spf13/cast v1.7.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stoewer/go-strcase v1.2.0 h1:Z2iHWqGXH00XYgqDmNgQbIBxf3wrNq0F3feEy0ainaU= @@ -175,8 +183,8 @@ go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= -golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= +golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= +golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM= golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= @@ -193,20 +201,20 @@ golang.org/x/oauth2 v0.18.0/go.mod h1:Wf7knwG0MPoWIMMBgFlEaSUDaKskp0dCfrlJRJXbBi golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= -golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ= +golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= -golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= -golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= +golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34= +golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.24.0 h1:Mh5cbb+Zk2hqqXNO7S1iTjEphVL+jb8ZWaqh/g+JWkM= +golang.org/x/term v0.24.0/go.mod h1:lOBK/LVxemqiMij05LGJ0tzNr8xlmwBRJ81PX6wVLH8= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= -golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= +golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224= +golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= diff --git a/go/controller/internal/controller/generatedsecret_controller.go b/go/controller/internal/controller/generatedsecret_controller.go new file mode 100644 index 0000000000..2770b3b7dc --- /dev/null +++ b/go/controller/internal/controller/generatedsecret_controller.go @@ -0,0 +1,200 @@ +package controller + +import ( + "context" + "reflect" + + "github.com/pluralsh/console/go/controller/api/v1alpha1" + "github.com/pluralsh/console/go/controller/internal/utils" + "github.com/pluralsh/polly/template" + "github.com/samber/lo" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "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/log" + "sigs.k8s.io/controller-runtime/pkg/predicate" +) + +// GeneratedSecretReconciler reconciles a GeneratedSecret object +type GeneratedSecretReconciler struct { + client.Client + Scheme *runtime.Scheme +} + +//+kubebuilder:rbac:groups=deployments.plural.sh,resources=generatedsecrets,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=deployments.plural.sh,resources=generatedsecrets/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=deployments.plural.sh,resources=generatedsecrets/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. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.16.3/pkg/reconcile +func (r *GeneratedSecretReconciler) Reconcile(ctx context.Context, req ctrl.Request) (_ ctrl.Result, reterr error) { + logger := log.FromContext(ctx) + generatedSecret := &v1alpha1.GeneratedSecret{} + + if err := r.Get(ctx, req.NamespacedName, generatedSecret); err != nil { + return ctrl.Result{}, client.IgnoreNotFound(err) + } + utils.MarkCondition(generatedSecret.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.ReadyConditionReason, "") + scope, err := NewDefaultScope(ctx, r.Client, generatedSecret) + if err != nil { + logger.Error(err, "failed to create scope") + utils.MarkCondition(generatedSecret.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.SynchronizedConditionReasonError, err.Error()) + return ctrl.Result{}, err + } + + // Always patch object when exiting this function, so we can persist any object changes. + defer func() { + if err := scope.PatchObject(); err != nil && reterr == nil { + reterr = err + } + }() + + if !generatedSecret.GetDeletionTimestamp().IsZero() { + return r.handleDelete(ctx, generatedSecret) + } + + data, err := r.persistData(ctx, generatedSecret, generatedSecret.Spec.Template) + if err != nil { + utils.MarkCondition(generatedSecret.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.SynchronizedConditionReasonError, err.Error()) + return ctrl.Result{}, err + } + + for _, destination := range generatedSecret.Spec.Destinations { + destSecretRef := &corev1.SecretReference{Name: destination.Name, Namespace: generatedSecret.Namespace} + destSecret, err := utils.GetSecret(ctx, r.Client, destSecretRef) + // create if it doesn't exist + if err != nil { + if !errors.IsNotFound(err) { + utils.MarkCondition(generatedSecret.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.SynchronizedConditionReasonError, err.Error()) + return ctrl.Result{}, err + } + if err := r.ensureNamespace(ctx, generatedSecret.Namespace); err != nil { + utils.MarkCondition(generatedSecret.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.SynchronizedConditionReasonError, err.Error()) + return ctrl.Result{}, err + } + if err := r.createSecret(ctx, destination.Namespace, destination.Name, data); err != nil { + utils.MarkCondition(generatedSecret.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.SynchronizedConditionReasonError, err.Error()) + return ctrl.Result{}, err + } + if err := r.tryAddSecretControllerRef(ctx, generatedSecret, destSecretRef); err != nil { + utils.MarkCondition(generatedSecret.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.SynchronizedConditionReasonError, err.Error()) + return ctrl.Result{}, err + } + continue + } + // update destination secret if it's different then persisted data + if !reflect.DeepEqual(data, destSecret.Data) { + destSecret.Data = data + if err := r.Update(ctx, destSecret); err != nil { + utils.MarkCondition(generatedSecret.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.SynchronizedConditionReasonError, err.Error()) + return ctrl.Result{}, err + } + } + } + + generatedSecret.Status.RenderedTemplateSecretRef = &corev1.LocalObjectReference{Name: generatedSecret.GetSecretName()} + utils.MarkCondition(generatedSecret.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionTrue, v1alpha1.ReadyConditionReason, "") + return ctrl.Result{}, nil +} + +func (r *GeneratedSecretReconciler) persistData(ctx context.Context, gs *v1alpha1.GeneratedSecret, tmp map[string]string) (map[string][]byte, error) { + data, err := templateData(tmp) + if err != nil { + return nil, err + } + secretRef := &corev1.SecretReference{Name: gs.GetSecretName(), Namespace: gs.Namespace} + templatedSecret, err := utils.GetSecret(ctx, r.Client, secretRef) + if err != nil { + if !errors.IsNotFound(err) { + utils.MarkCondition(gs.SetCondition, v1alpha1.ReadyConditionType, v1.ConditionFalse, v1alpha1.SynchronizedConditionReasonError, err.Error()) + return nil, err + } + if err := r.createSecret(ctx, gs.Namespace, gs.GetSecretName(), data); err != nil { + return nil, err + } + + return data, r.tryAddSecretControllerRef(ctx, gs, secretRef) + } + persistedData := templatedSecret.Data + // merges maps from left to right. + data = lo.Assign(data, persistedData) + if !reflect.DeepEqual(persistedData, data) { + templatedSecret.Data = data + if err := r.Update(ctx, templatedSecret); err != nil { + return nil, err + } + } + + return data, nil +} + +func (r *GeneratedSecretReconciler) ensureNamespace(ctx context.Context, namespace string) error { + if namespace == "" { + return nil + } + if err := r.Get(ctx, client.ObjectKey{Name: namespace}, &corev1.Namespace{}); err != nil { + if !errors.IsNotFound(err) { + return err + } + return r.Create(ctx, &corev1.Namespace{ + ObjectMeta: v1.ObjectMeta{ + Name: namespace, + }, + }) + } + return nil +} + +func (r *GeneratedSecretReconciler) createSecret(ctx context.Context, namespace, name string, data map[string][]byte) error { + secret := &corev1.Secret{ + ObjectMeta: v1.ObjectMeta{ + Name: name, + Namespace: namespace, + }, + Data: data, + } + + if err := r.Create(ctx, secret); err != nil { + return err + } + return nil +} + +func (r *GeneratedSecretReconciler) tryAddSecretControllerRef(ctx context.Context, gs *v1alpha1.GeneratedSecret, secretRef *corev1.SecretReference) error { + secret, err := utils.GetSecret(ctx, r.Client, secretRef) + if err != nil { + return err + } + return utils.TryAddControllerRef(ctx, r.Client, gs, secret, r.Scheme) +} + +func templateData(tmp map[string]string) (map[string][]byte, error) { + data := make(map[string][]byte) + for k, v := range tmp { + out, err := template.RenderLiquid([]byte(v), map[string]interface{}{}) + if err != nil { + return nil, err + } + data[k] = out + } + return data, nil +} + +func (r *GeneratedSecretReconciler) handleDelete(ctx context.Context, secret *v1alpha1.GeneratedSecret) (ctrl.Result, error) { + return ctrl.Result{}, nil +} + +// SetupWithManager sets up the controller with the Manager. +func (r *GeneratedSecretReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&v1alpha1.GeneratedSecret{}). + Owns(&corev1.Secret{}, builder.WithPredicates(predicate.ResourceVersionChangedPredicate{})). + Complete(r) +} diff --git a/go/controller/internal/controller/generatedsecret_controller_test.go b/go/controller/internal/controller/generatedsecret_controller_test.go new file mode 100644 index 0000000000..c4d07d3503 --- /dev/null +++ b/go/controller/internal/controller/generatedsecret_controller_test.go @@ -0,0 +1,90 @@ +package controller_test + +import ( + "context" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "github.com/pluralsh/console/go/controller/api/v1alpha1" + "github.com/pluralsh/console/go/controller/internal/controller" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/reconcile" +) + +var _ = Describe("GeneratedSecret Controller", Ordered, func() { + Context("When reconciling a resource", func() { + const ( + generatedSecretName = "test" + namespace = "default" + ) + + ctx := context.Background() + + namespacedName := types.NamespacedName{Name: generatedSecretName, Namespace: namespace} + + gs := &v1alpha1.GeneratedSecret{} + + BeforeAll(func() { + By("Creating GeneratedSecret") + err := k8sClient.Get(ctx, namespacedName, gs) + if err != nil && errors.IsNotFound(err) { + resource := &v1alpha1.GeneratedSecret{ + ObjectMeta: metav1.ObjectMeta{ + Name: generatedSecretName, + Namespace: namespace, + }, + Spec: v1alpha1.GeneratedSecretSpec{ + Template: map[string]string{ + "b64": "{{ 'one two three' | b64enc }}", + "name": "John Doe", + "password": "{{ 10 | randAlphaNum }}", + }, + Destinations: []v1alpha1.GeneratedSecretDestination{ + { + Name: "secret1", + Namespace: namespace, + }, + }, + }, + } + Expect(k8sClient.Create(ctx, resource)).To(Succeed()) + } + }) + + AfterAll(func() { + By("Cleanup resources") + gs := &v1alpha1.GeneratedSecret{} + Expect(k8sClient.Get(ctx, namespacedName, gs)).NotTo(HaveOccurred()) + Expect(k8sClient.Delete(ctx, gs)).To(Succeed()) + }) + + It("Reconcile test", func() { + reconciler := &controller.GeneratedSecretReconciler{ + Client: k8sClient, + Scheme: k8sClient.Scheme(), + } + + // create new persisted secret + _, err := reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: namespacedName}) + Expect(err).NotTo(HaveOccurred()) + s1 := &corev1.Secret{} + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: "secret1", Namespace: namespace}, s1)).To(Succeed()) + Expect(s1.Data["b64"]).To(Equal([]byte("b25lIHR3byB0aHJlZQ=="))) + Expect(s1.Data["name"]).To(Equal([]byte("John Doe"))) + password := s1.Data["password"] + Expect(len(password)).To(Equal(10)) + + // read from persisted secret + _, err = reconciler.Reconcile(ctx, reconcile.Request{NamespacedName: namespacedName}) + Expect(err).NotTo(HaveOccurred()) + // should have exactly the same data + Expect(k8sClient.Get(ctx, client.ObjectKey{Name: "secret1", Namespace: namespace}, s1)).To(Succeed()) + Expect(s1.Data["password"]).To(Equal(password)) + }) + + }) +}) diff --git a/go/controller/internal/types/reconciler.go b/go/controller/internal/types/reconciler.go index 6408796486..c273e9a318 100644 --- a/go/controller/internal/types/reconciler.go +++ b/go/controller/internal/types/reconciler.go @@ -41,6 +41,7 @@ const ( ObserverReconciler Reconciler = "observerprovider" CatalogReconciler Reconciler = "catalogprovider" OIDCProviderReconciler Reconciler = "oidcprovider" + GeneratedSecretReconciler Reconciler = "generatedsecret" ) // ToReconciler maps reconciler string to a Reconciler type. @@ -98,6 +99,8 @@ func ToReconciler(reconciler string) (Reconciler, error) { fallthrough case OIDCProviderReconciler: fallthrough + case GeneratedSecretReconciler: + fallthrough case ProviderReconciler: return Reconciler(reconciler), nil } @@ -309,6 +312,11 @@ func (sc Reconciler) ToController(mgr ctrl.Manager, consoleClient client.Console Scheme: mgr.GetScheme(), UserGroupCache: userGroupCache, }, nil + case GeneratedSecretReconciler: + return &controller.GeneratedSecretReconciler{ + Client: mgr.GetClient(), + Scheme: mgr.GetScheme(), + }, nil default: return nil, fmt.Errorf("reconciler %q is not supported", sc) } @@ -349,6 +357,7 @@ func Reconcilers() ReconcilerList { StackDefinitionReconciler, CatalogReconciler, OIDCProviderReconciler, + GeneratedSecretReconciler, } } diff --git a/plural/helm/console/crds/deployments.plural.sh_generatedsecrets.yaml b/plural/helm/console/crds/deployments.plural.sh_generatedsecrets.yaml new file mode 100644 index 0000000000..eb6aadcd7d --- /dev/null +++ b/plural/helm/console/crds/deployments.plural.sh_generatedsecrets.yaml @@ -0,0 +1,146 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.16.3 + name: generatedsecrets.deployments.plural.sh +spec: + group: deployments.plural.sh + names: + kind: GeneratedSecret + listKind: GeneratedSecretList + plural: generatedsecrets + singular: generatedsecret + scope: Namespaced + versions: + - name: v1alpha1 + schema: + openAPIV3Schema: + description: GeneratedSecret is the Schema for the generatedsecrets 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: GeneratedSecretSpec defines the desired state of GeneratedSecret + properties: + destinations: + description: Destinations describe name/namespace for the secrets. + items: + properties: + name: + type: string + namespace: + type: string + required: + - name + type: object + type: array + template: + additionalProperties: + type: string + description: Template secret data in string form. + type: object + type: object + status: + properties: + conditions: + description: Represents the observations of a PrAutomation's current + state. + items: + description: Condition contains details for one aspect of the current + state of this API Resource. + 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. + 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 + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + id: + description: ID of the resource in the Console API. + type: string + renderedTemplateSecretRef: + description: |- + LocalObjectReference contains enough information to let you locate the + referenced object inside the same namespace. + properties: + name: + description: |- + Name of the referent. + More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + type: string + type: object + x-kubernetes-map-type: atomic + sha: + description: SHA of last applied configuration. + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {}