diff --git a/PROJECT b/PROJECT index e9d74c1..3ebe1f2 100644 --- a/PROJECT +++ b/PROJECT @@ -117,4 +117,13 @@ resources: kind: Forgejo path: libre.sh/api/apps/v1alpha1 version: v1alpha1 +- api: + crdVersion: v1 + namespaced: true + controller: true + domain: libre.sh + group: apps + kind: Decidim + path: libre.sh/api/apps/v1alpha1 + version: v1alpha1 version: "3" diff --git a/api/apps/v1alpha1/decidim_types.go b/api/apps/v1alpha1/decidim_types.go new file mode 100644 index 0000000..822a5fb --- /dev/null +++ b/api/apps/v1alpha1/decidim_types.go @@ -0,0 +1,141 @@ +/* +Copyright 2024 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 ( + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + + lshmeta "libre.sh/api/meta/v1alpha1" +) + +// EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! +// NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. + +const DecidimConfigSuffix = "decidim.libre.sh" + +type DecidimLocale struct { + //+kubebuilder:validation:Optional + //+kubebuilder:default="fr" + Default string `json:"default"` + //+kubebuilder:validation:Optional + //+kubebuilder:validation:MinItems=1 + //+kubebuilder:default={"fr"} + Available []string `json:"available"` +} + +type DecidimFileUpload struct { + //+kubebuilder:validation:Optional + //+kubebuilder:default="en" + Default string `json:"default"` + //+kubebuilder:validation:Optional + //+kubebuilder:validation:MinItems=1 + //+kubebuilder:default={"en","fr"} + Available []string `json:"available"` +} + +type DecidimAdmin struct { + //+kubebuilder:validation:Required + Email string `json:"email"` +} + +type DecidimOrganization struct { + //+kubebuilder:validation:Optional + //+kubebuilder:default=1 + ID int `json:"id"` + //+kubebuilder:validation:Optional + Admin DecidimOrganizationAdmin `json:"admin,omitempty"` +} + +type DecidimOrganizationAdmin struct { + //+kubebuilder:validation:Required + Email string `json:"email"` + //+kubebuilder:validation:Required + Name string `json:"name"` + //+kubebuilder:validation:Required + Nickname string `json:"nickname"` +} + +// DecidimSpec defines the desired state of Decidim +type DecidimSpec struct { + lshmeta.Spec `json:",inline"` + //+kubebuilder:validation:Required + Image string `json:"image"` + //+kubebuilder:validation:Optional + Admin DecidimAdmin `json:"admin,omitempty"` + //+kubebuilder:validation:Required + Organization DecidimOrganization `json:"organization"` + //+kubebuilder:validation:Required + //+kubebuilder:validation:MinLength=3 + Host string `json:"host"` + //+kubebuilder:validation:Optional + AdditionalHosts []string `json:"additionalHosts,omitempty"` + //+kubebuilder:validation:Optional + UsersRegistrationMode int `json:"usersRegistrationMode,omitempty"` + //+kubebuilder:validation:Optional + ForceUsersToAuthenticateBeforeAccessOrganization bool `json:"forceUsersToAuthenticateBeforeAccessOrganization,omitempty"` + //+kubebuilder:validation:Optional + AvailableAuthorizations []string `json:"availableAuthorizations,omitempty"` + //+kubebuilder:validation:Optional + FileUploadSettings runtime.RawExtension `json:"fileUploadSettings,omitempty"` + //+kubebuilder:validation:Required + Locale DecidimLocale `json:"locale"` + //+kubebuilder:validation:Optional + //+kubebuilder:default="UTC" + TimeZone string `json:"timeZone,omitempty"` + // OmniAuth DecidimOmniAuth `json:"omniauth,omitempty"` + //+kubebuilder:validation:Optional + EnvFrom []corev1.EnvFromSource `json:"envFrom,omitempty" protobuf:"bytes,19,rep,name=envFrom"` +} + +// DecidimStatus defines the observed state of Decidim +type DecidimStatus struct { + lshmeta.Status `json:",inline"` +} + +//go:generate lsh-gen v1alpha1 Decidim + +//+kubebuilder:object:root=true +//+kubebuilder:subresource:status +//+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp",description="" +//+kubebuilder:printcolumn:name="Ready",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].status",description="" +//+kubebuilder:printcolumn:name="Status",type="string",JSONPath=".status.conditions[?(@.type==\"Ready\")].message",description="" +//+kubebuilder:printcolumn:name="Version",type="string",JSONPath=".status.version",description="" +//+kubebuilder:printcolumn:name="Suspended",type="boolean",JSONPath=".spec.suspend",description="" + +// Decidim is the Schema for the decidims API +type Decidim struct { + metav1.TypeMeta `json:",inline"` + metav1.ObjectMeta `json:"metadata,omitempty"` + + Spec DecidimSpec `json:"spec,omitempty"` + Status DecidimStatus `json:"status,omitempty"` +} + +//+kubebuilder:object:root=true + +// DecidimList contains a list of Decidim +type DecidimList struct { + metav1.TypeMeta `json:",inline"` + metav1.ListMeta `json:"metadata,omitempty"` + Items []Decidim `json:"items"` +} + +func init() { + SchemeBuilder.Register(&Decidim{}, &DecidimList{}) +} diff --git a/api/apps/v1alpha1/zz_generated.decidim_lsh.go b/api/apps/v1alpha1/zz_generated.decidim_lsh.go new file mode 100644 index 0000000..20f3f40 --- /dev/null +++ b/api/apps/v1alpha1/zz_generated.decidim_lsh.go @@ -0,0 +1,35 @@ +// Code generated by lsh-gen; DO NOT EDIT. + +package v1alpha1 + +import ( + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +func (o *Decidim) GetSuspend() bool { + return o.Spec.Suspend +} + +func (o *Decidim) SetSuspend(value bool) { + o.Spec.Suspend = value +} + +func (o *Decidim) GetVersion() string { + return o.Status.Version +} + +func (o *Decidim) SetVersion(value string) { + o.Status.Version = value +} + +func (o *Decidim) GetImage() string { + return o.Spec.Image +} + +func (o *Decidim) GetConditions() []metav1.Condition { + return o.Status.Conditions +} + +func (o *Decidim) SetConditions(conditions []metav1.Condition) { + o.Status.Conditions = conditions +} diff --git a/api/apps/v1alpha1/zz_generated.deepcopy.go b/api/apps/v1alpha1/zz_generated.deepcopy.go index 73e1ce5..f7f6f92 100644 --- a/api/apps/v1alpha1/zz_generated.deepcopy.go +++ b/api/apps/v1alpha1/zz_generated.deepcopy.go @@ -23,9 +23,207 @@ package v1alpha1 import ( "encoding/json" "k8s.io/api/core/v1" - runtime "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime" ) +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *Decidim) DeepCopyInto(out *Decidim) { + *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 Decidim. +func (in *Decidim) DeepCopy() *Decidim { + if in == nil { + return nil + } + out := new(Decidim) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *Decidim) 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 *DecidimAdmin) DeepCopyInto(out *DecidimAdmin) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DecidimAdmin. +func (in *DecidimAdmin) DeepCopy() *DecidimAdmin { + if in == nil { + return nil + } + out := new(DecidimAdmin) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DecidimFileUpload) DeepCopyInto(out *DecidimFileUpload) { + *out = *in + if in.Available != nil { + in, out := &in.Available, &out.Available + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DecidimFileUpload. +func (in *DecidimFileUpload) DeepCopy() *DecidimFileUpload { + if in == nil { + return nil + } + out := new(DecidimFileUpload) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DecidimList) DeepCopyInto(out *DecidimList) { + *out = *in + out.TypeMeta = in.TypeMeta + in.ListMeta.DeepCopyInto(&out.ListMeta) + if in.Items != nil { + in, out := &in.Items, &out.Items + *out = make([]Decidim, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DecidimList. +func (in *DecidimList) DeepCopy() *DecidimList { + if in == nil { + return nil + } + out := new(DecidimList) + in.DeepCopyInto(out) + return out +} + +// DeepCopyObject is an autogenerated deepcopy function, copying the receiver, creating a new runtime.Object. +func (in *DecidimList) 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 *DecidimLocale) DeepCopyInto(out *DecidimLocale) { + *out = *in + if in.Available != nil { + in, out := &in.Available, &out.Available + *out = make([]string, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DecidimLocale. +func (in *DecidimLocale) DeepCopy() *DecidimLocale { + if in == nil { + return nil + } + out := new(DecidimLocale) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DecidimOrganization) DeepCopyInto(out *DecidimOrganization) { + *out = *in + out.Admin = in.Admin +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DecidimOrganization. +func (in *DecidimOrganization) DeepCopy() *DecidimOrganization { + if in == nil { + return nil + } + out := new(DecidimOrganization) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DecidimOrganizationAdmin) DeepCopyInto(out *DecidimOrganizationAdmin) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DecidimOrganizationAdmin. +func (in *DecidimOrganizationAdmin) DeepCopy() *DecidimOrganizationAdmin { + if in == nil { + return nil + } + out := new(DecidimOrganizationAdmin) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DecidimSpec) DeepCopyInto(out *DecidimSpec) { + *out = *in + out.Spec = in.Spec + out.Admin = in.Admin + out.Organization = in.Organization + if in.AdditionalHosts != nil { + in, out := &in.AdditionalHosts, &out.AdditionalHosts + *out = make([]string, len(*in)) + copy(*out, *in) + } + if in.AvailableAuthorizations != nil { + in, out := &in.AvailableAuthorizations, &out.AvailableAuthorizations + *out = make([]string, len(*in)) + copy(*out, *in) + } + in.FileUploadSettings.DeepCopyInto(&out.FileUploadSettings) + in.Locale.DeepCopyInto(&out.Locale) + if in.EnvFrom != nil { + in, out := &in.EnvFrom, &out.EnvFrom + *out = make([]v1.EnvFromSource, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DecidimSpec. +func (in *DecidimSpec) DeepCopy() *DecidimSpec { + if in == nil { + return nil + } + out := new(DecidimSpec) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DecidimStatus) DeepCopyInto(out *DecidimStatus) { + *out = *in + in.Status.DeepCopyInto(&out.Status) +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DecidimStatus. +func (in *DecidimStatus) DeepCopy() *DecidimStatus { + if in == nil { + return nil + } + out := new(DecidimStatus) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Forgejo) DeepCopyInto(out *Forgejo) { *out = *in diff --git a/cmd/main.go b/cmd/main.go index 966c1b6..8730883 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -23,10 +23,12 @@ import ( // Import all Kubernetes client auth plugins (e.g. Azure, GCP, OIDC, etc.) // to ensure that exec-entrypoint and run can make use of them. + "k8s.io/client-go/discovery" _ "k8s.io/client-go/plugin/pkg/client/auth" certmanagerv1 "github.com/cert-manager/cert-manager/pkg/apis/certmanager/v1" monitoringv1 "github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring/v1" + imcv1alpha1 "github.com/stakater/IngressMonitorController/v2/api/v1alpha1" zalandov1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1" "k8s.io/apimachinery/pkg/runtime" utilruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -62,6 +64,7 @@ func init() { utilruntime.Must(zalandov1.AddToScheme(scheme)) utilruntime.Must(monitoringv1.AddToScheme(scheme)) utilruntime.Must(certmanagerv1.AddToScheme(scheme)) + utilruntime.Must(imcv1alpha1.AddToScheme(scheme)) utilruntime.Must(corev1alpha1.AddToScheme(scheme)) utilruntime.Must(postgresv1alpha1.AddToScheme(scheme)) @@ -127,6 +130,13 @@ func main() { os.Exit(1) } + discoveryClient := discovery.NewDiscoveryClientForConfigOrDie(mgr.GetConfig()) + endpointMonitorEnabled, err := discovery.IsResourceEnabled(discoveryClient, imcv1alpha1.GroupVersion.WithResource("endpointmonitors")) + if err != nil { + setupLog.Error(err, "unable to check if resource is enabled") + os.Exit(1) + } + if err = (&corecontroller.PostgresReconciler{ Client: mgr.GetClient(), }).SetupWithManager(mgr); err != nil { @@ -214,6 +224,13 @@ func main() { setupLog.Error(err, "unable to create controller", "controller", "Forgejo") os.Exit(1) } + if err = (&appscontroller.DecidimReconciler{ + Client: mgr.GetClient(), + EndpointMonitorEnabled: endpointMonitorEnabled, + }).SetupWithManager(mgr); err != nil { + setupLog.Error(err, "unable to create controller", "controller", "Decidim") + os.Exit(1) + } //+kubebuilder:scaffold:builder if err := mgr.AddHealthzCheck("healthz", healthz.Ping); err != nil { diff --git a/config/crd/bases/apps.libre.sh_decidims.yaml b/config/crd/bases/apps.libre.sh_decidims.yaml new file mode 100644 index 0000000..4f7ca6b --- /dev/null +++ b/config/crd/bases/apps.libre.sh_decidims.yaml @@ -0,0 +1,239 @@ +--- +apiVersion: apiextensions.k8s.io/v1 +kind: CustomResourceDefinition +metadata: + annotations: + controller-gen.kubebuilder.io/version: v0.13.0 + name: decidims.apps.libre.sh +spec: + group: apps.libre.sh + names: + kind: Decidim + listKind: DecidimList + plural: decidims + singular: decidim + scope: Namespaced + versions: + - additionalPrinterColumns: + - jsonPath: .metadata.creationTimestamp + name: Age + type: date + - jsonPath: .status.conditions[?(@.type=="Ready")].status + name: Ready + type: string + - jsonPath: .status.conditions[?(@.type=="Ready")].message + name: Status + type: string + - jsonPath: .status.version + name: Version + type: string + - jsonPath: .spec.suspend + name: Suspended + type: boolean + name: v1alpha1 + schema: + openAPIV3Schema: + description: Decidim is the Schema for the decidims 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: DecidimSpec defines the desired state of Decidim + properties: + additionalHosts: + items: + type: string + type: array + admin: + properties: + email: + type: string + required: + - email + type: object + availableAuthorizations: + items: + type: string + type: array + envFrom: + description: OmniAuth DecidimOmniAuth `json:"omniauth,omitempty"` + items: + description: EnvFromSource represents the source of a set of ConfigMaps + properties: + configMapRef: + description: The ConfigMap to select from + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the ConfigMap must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + prefix: + description: An optional identifier to prepend to each key in + the ConfigMap. Must be a C_IDENTIFIER. + type: string + secretRef: + description: The Secret to select from + properties: + name: + description: 'Name of the referent. More info: https://kubernetes.io/docs/concepts/overview/working-with-objects/names/#names + TODO: Add other useful fields. apiVersion, kind, uid?' + type: string + optional: + description: Specify whether the Secret must be defined + type: boolean + type: object + x-kubernetes-map-type: atomic + type: object + type: array + fileUploadSettings: + type: object + x-kubernetes-preserve-unknown-fields: true + forceUsersToAuthenticateBeforeAccessOrganization: + type: boolean + host: + minLength: 3 + type: string + image: + type: string + locale: + properties: + available: + default: + - fr + items: + type: string + minItems: 1 + type: array + default: + default: fr + type: string + type: object + organization: + properties: + admin: + properties: + email: + type: string + name: + type: string + nickname: + type: string + required: + - email + - name + - nickname + type: object + id: + default: 1 + type: integer + type: object + suspend: + type: boolean + timeZone: + default: UTC + type: string + usersRegistrationMode: + type: integer + required: + - host + - image + - locale + - organization + type: object + status: + description: DecidimStatus defines the observed state of Decidim + properties: + conditions: + items: + description: "Condition contains details for one aspect of the current + state of this API Resource. --- This struct is intended for direct + use as an array at the field path .status.conditions. For example, + \n type FooStatus struct{ // Represents the observations of a + foo's current state. // Known .status.conditions.type are: \"Available\", + \"Progressing\", and \"Degraded\" // +patchMergeKey=type // +patchStrategy=merge + // +listType=map // +listMapKey=type Conditions []metav1.Condition + `json:\"conditions,omitempty\" patchStrategy:\"merge\" patchMergeKey:\"type\" + protobuf:\"bytes,1,rep,name=conditions\"` \n // other fields }" + properties: + lastTransitionTime: + description: lastTransitionTime is the last time the condition + transitioned from one status to another. This should be when + the underlying condition changed. If that is not known, then + using the time when the API field changed is acceptable. + format: date-time + type: string + message: + description: message is a human readable message indicating + details about the transition. This may be an empty string. + maxLength: 32768 + type: string + observedGeneration: + description: observedGeneration represents the .metadata.generation + that the condition was set based upon. For instance, if .metadata.generation + is currently 12, but the .status.conditions[x].observedGeneration + is 9, the condition is out of date with respect to the current + state of the instance. + format: int64 + minimum: 0 + type: integer + reason: + description: reason contains a programmatic identifier indicating + the reason for the condition's last transition. Producers + of specific condition types may define expected values and + meanings for this field, and whether the values are considered + a guaranteed API. The value should be a CamelCase string. + This field may not be empty. + maxLength: 1024 + minLength: 1 + pattern: ^[A-Za-z]([A-Za-z0-9_,:]*[A-Za-z0-9_])?$ + type: string + status: + description: status of the condition, one of True, False, Unknown. + enum: + - "True" + - "False" + - Unknown + type: string + type: + description: type of condition in CamelCase or in foo.example.com/CamelCase. + --- Many .condition.type values are consistent across resources + like Available, but because arbitrary conditions can be useful + (see .node.status.conditions), the ability to deconflict is + important. The regex it matches is (dns1123SubdomainFmt/)?(qualifiedNameFmt) + maxLength: 316 + pattern: ^([a-z0-9]([-a-z0-9]*[a-z0-9])?(\.[a-z0-9]([-a-z0-9]*[a-z0-9])?)*/)?(([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9])$ + type: string + required: + - lastTransitionTime + - message + - reason + - status + - type + type: object + type: array + x-kubernetes-list-map-keys: + - type + x-kubernetes-list-type: map + version: + type: string + type: object + type: object + served: true + storage: true + subresources: + status: {} diff --git a/config/crd/kustomization.yaml b/config/crd/kustomization.yaml index b73de6b..3d55d8d 100644 --- a/config/crd/kustomization.yaml +++ b/config/crd/kustomization.yaml @@ -14,6 +14,7 @@ resources: - bases/apps.libre.sh_mobilizons.yaml - bases/apps.libre.sh_hedgedocs.yaml - bases/apps.libre.sh_forgejoes.yaml +- bases/apps.libre.sh_decidims.yaml #+kubebuilder:scaffold:crdkustomizeresource patches: @@ -31,6 +32,7 @@ patches: #- path: patches/webhook_in_apps_mobilizons.yaml #- path: patches/webhook_in_apps_hedgedocs.yaml #- path: patches/webhook_in_apps_forgejoes.yaml +#- path: patches/webhook_in_apps_decidims.yaml #+kubebuilder:scaffold:crdkustomizewebhookpatch # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. @@ -47,6 +49,7 @@ patches: #- path: patches/cainjection_in_apps_mobilizons.yaml #- path: patches/cainjection_in_apps_hedgedocs.yaml #- path: patches/cainjection_in_apps_forgejoes.yaml +#- path: patches/cainjection_in_apps_decidims.yaml #+kubebuilder:scaffold:crdkustomizecainjectionpatch # [WEBHOOK] To enable webhook, uncomment the following section diff --git a/config/rbac/apps_decidim_editor_role.yaml b/config/rbac/apps_decidim_editor_role.yaml new file mode 100644 index 0000000..00a0a77 --- /dev/null +++ b/config/rbac/apps_decidim_editor_role.yaml @@ -0,0 +1,31 @@ +# permissions for end users to edit decidims. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: decidim-editor-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: libre-sh + app.kubernetes.io/part-of: libre-sh + app.kubernetes.io/managed-by: kustomize + name: decidim-editor-role +rules: +- apiGroups: + - apps.libre.sh + resources: + - decidims + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps.libre.sh + resources: + - decidims/status + verbs: + - get diff --git a/config/rbac/apps_decidim_viewer_role.yaml b/config/rbac/apps_decidim_viewer_role.yaml new file mode 100644 index 0000000..ae7d9f1 --- /dev/null +++ b/config/rbac/apps_decidim_viewer_role.yaml @@ -0,0 +1,27 @@ +# permissions for end users to view decidims. +apiVersion: rbac.authorization.k8s.io/v1 +kind: ClusterRole +metadata: + labels: + app.kubernetes.io/name: clusterrole + app.kubernetes.io/instance: decidim-viewer-role + app.kubernetes.io/component: rbac + app.kubernetes.io/created-by: libre-sh + app.kubernetes.io/part-of: libre-sh + app.kubernetes.io/managed-by: kustomize + name: decidim-viewer-role +rules: +- apiGroups: + - apps.libre.sh + resources: + - decidims + verbs: + - get + - list + - watch +- apiGroups: + - apps.libre.sh + resources: + - decidims/status + verbs: + - get diff --git a/config/rbac/role.yaml b/config/rbac/role.yaml index bdb0535..f2ef8ca 100644 --- a/config/rbac/role.yaml +++ b/config/rbac/role.yaml @@ -145,6 +145,32 @@ rules: - patch - update - watch +- apiGroups: + - apps.libre.sh + resources: + - decidims + verbs: + - create + - delete + - get + - list + - patch + - update + - watch +- apiGroups: + - apps.libre.sh + resources: + - decidims/finalizers + verbs: + - update +- apiGroups: + - apps.libre.sh + resources: + - decidims/status + verbs: + - get + - patch + - update - apiGroups: - apps.libre.sh resources: @@ -197,6 +223,18 @@ rules: - get - patch - update +- apiGroups: + - autoscaling + resources: + - horizontalpodautoscalers + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - batch resources: @@ -415,6 +453,18 @@ rules: - get - patch - update +- apiGroups: + - endpointmonitor.stakater.com + resources: + - endpointmonitors + verbs: + - create + - delete + - get + - list + - patch + - update + - watch - apiGroups: - keycloak.libre.sh resources: diff --git a/config/samples/apps_v1alpha1_decidim.yaml b/config/samples/apps_v1alpha1_decidim.yaml new file mode 100644 index 0000000..6f80627 --- /dev/null +++ b/config/samples/apps_v1alpha1_decidim.yaml @@ -0,0 +1,12 @@ +apiVersion: apps.libre.sh/v1alpha1 +kind: Decidim +metadata: + labels: + app.kubernetes.io/name: decidim + app.kubernetes.io/instance: decidim-sample + app.kubernetes.io/part-of: libre-sh + app.kubernetes.io/managed-by: kustomize + app.kubernetes.io/created-by: libre-sh + name: decidim-sample +spec: + # TODO(user): Add fields here diff --git a/config/samples/kustomization.yaml b/config/samples/kustomization.yaml index 86c0539..49f4bab 100644 --- a/config/samples/kustomization.yaml +++ b/config/samples/kustomization.yaml @@ -12,4 +12,5 @@ resources: - apps_v1alpha1_mobilizon.yaml - apps_v1alpha1_hedgedoc.yaml - apps_v1alpha1_forgejo.yaml +- apps_v1alpha1_decidim.yaml #+kubebuilder:scaffold:manifestskustomizesamples diff --git a/go.mod b/go.mod index 65690c5..bf2698f 100644 --- a/go.mod +++ b/go.mod @@ -18,7 +18,7 @@ require ( github.com/fluxcd/pkg/runtime v0.42.0 github.com/fluxcd/pkg/ssa v0.35.0 github.com/fluxcd/source-controller/api v1.2.3 - github.com/go-logr/logr v1.3.0 + github.com/go-logr/logr v1.4.1 github.com/google/uuid v1.5.0 github.com/lib/pq v1.10.9 github.com/minio/madmin-go/v3 v3.0.7 @@ -27,6 +27,7 @@ require ( github.com/onsi/gomega v1.30.0 github.com/prometheus-operator/prometheus-operator/pkg/apis/monitoring v0.70.0 github.com/spf13/cobra v1.8.0 + github.com/stakater/IngressMonitorController/v2 v2.1.50 github.com/stretchr/testify v1.8.4 github.com/tidwall/gjson v1.17.0 github.com/tidwall/sjson v1.2.5 @@ -141,7 +142,7 @@ require ( go.starlark.net v0.0.0-20230525235612-a134d8f9ddca // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.25.0 // indirect - golang.org/x/crypto v0.16.0 // indirect + golang.org/x/crypto v0.17.0 // indirect golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect golang.org/x/net v0.19.0 // indirect golang.org/x/oauth2 v0.14.0 // indirect diff --git a/go.sum b/go.sum index e1d1d67..b6bfe3a 100644 --- a/go.sum +++ b/go.sum @@ -106,8 +106,9 @@ github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbS github.com/go-errors/errors v1.4.2 h1:J6MZopCL4uSllY1OfXM374weqZFFItUbrImctkmUxIA= github.com/go-errors/errors v1.4.2/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= +github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ= +github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/zapr v1.2.4 h1:QHVo+6stLbfJmYGkQ7uGHUCu5hnAFAj6mDe6Ea0SeOo= github.com/go-logr/zapr v1.2.4/go.mod h1:FyHWQIzQORZ0QVE1BtVHv3cKtNLuXsbNLtpuhNapBOA= github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY= @@ -325,6 +326,8 @@ github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0= github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho= 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/stakater/IngressMonitorController/v2 v2.1.50 h1:7NN/7edU+8VK7+V7NNVgjv5Y35zhRecQ8aTEcEvWGFg= +github.com/stakater/IngressMonitorController/v2 v2.1.50/go.mod h1:oDNe9M+84mS2IvicLf39dloF9BIt2sAf1JcI0drwpw4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= @@ -384,8 +387,8 @@ golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= +golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= diff --git a/internal/controller/apps/decidim_controller.go b/internal/controller/apps/decidim_controller.go new file mode 100644 index 0000000..c010a41 --- /dev/null +++ b/internal/controller/apps/decidim_controller.go @@ -0,0 +1,264 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 apps + +import ( + "context" + "time" + + "github.com/fluxcd/pkg/apis/meta" + "github.com/fluxcd/pkg/runtime/patch" + appsv1 "k8s.io/api/apps/v1" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + lshr "libre.sh/pkg/controller-runtime" + ctrl "sigs.k8s.io/controller-runtime" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" + + appsv1alpha1 "libre.sh/api/apps/v1alpha1" + lshcore "libre.sh/api/core/v1alpha1" + lshmeta "libre.sh/api/meta/v1alpha1" + internal "libre.sh/internal/decidim" +) + +// DecidimReconciler reconciles a Decidim object +type DecidimReconciler struct { + client.Client + EndpointMonitorEnabled bool +} + +type decidimResources struct { + postgres *lshcore.Postgres + redis *lshcore.Redis + bucket *lshcore.Bucket + secret *corev1.Secret +} + +//+kubebuilder:rbac:groups=apps.libre.sh,resources=decidims,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=apps.libre.sh,resources=decidims/status,verbs=get;update;patch +//+kubebuilder:rbac:groups=apps.libre.sh,resources=decidims/finalizers,verbs=update + +//+kubebuilder:rbac:groups=core.libre.sh,resources=postgres;buckets;redis,verbs=get;list;watch;create;update;patch;delete + +//+kubebuilder:rbac:groups=batch,resources=jobs,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=apps,resources=deployments,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=autoscaling,resources=horizontalpodautoscalers,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=policy,resources=poddisruptionbudgets,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=networking.k8s.io,resources=ingresses,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups="",resources=secrets;configmaps;services,verbs=get;list;watch;create;update;patch;delete +//+kubebuilder:rbac:groups=endpointmonitor.stakater.com,resources=endpointmonitors,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. +// TODO(user): Modify the Reconcile function to compare the state specified by +// the Decidim object against the actual cluster state, and then +// perform operations to make the cluster state reflect the state specified by +// the user. +// +// For more details, check Reconcile and its Result here: +// - https://pkg.go.dev/sigs.k8s.io/controller-runtime@v0.13.0/pkg/reconcile +func (r *DecidimReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) { + log := log.FromContext(ctx) + + var decidim appsv1alpha1.Decidim + patcher, result := lshr.Initialize(ctx, r, req, &decidim) + if result != nil { + return result.Unwrap() + } + + if lshr.IsFinalizing(&decidim) { + return lshr.Finalize(ctx, r, patcher, &decidim, func() error { + var bucket lshcore.Bucket + lshr.SetResourceNamespacedName(&decidim, &bucket) + err := r.Get(ctx, client.ObjectKeyFromObject(&bucket), &bucket) + if err != nil { + return client.IgnoreNotFound(err) + } + return lshr.UnsetControllerReference(ctx, r, &decidim, &bucket) + }) + } + + jm := lshr.NewJobManager(r, &decidim, patcher) + resources := decidimResources{} + + err := r.reconcilePostgres(ctx, &decidim, &resources) + if err != nil { + return ctrl.Result{}, err + } + + err = r.reconcileRedis(ctx, &decidim, &resources) + if err != nil { + return ctrl.Result{}, err + } + + err = r.reconcileBucket(ctx, &decidim, &resources) + if err != nil { + return ctrl.Result{}, err + } + + lshr.SetDependencyCondition(&decidim, resources.postgres, resources.redis, resources.bucket) + if err := lshr.Patch(ctx, r, patcher, &decidim, lshr.PatchOpts{}); err != nil { + return ctrl.Result{}, err + } + if lshr.IsDependencyNotReady(&decidim) || lshr.IsImporting(&decidim) { + log.Info("Waiting for dependencies or importation") + return ctrl.Result{}, nil + + } + + err = r.reconcileSecret(ctx, &decidim, &resources) + if err != nil { + return ctrl.Result{}, err + } + + err = lshr.OnInstall(&decidim, func() error { + log.Info("Installing") + job, err := r.reconcileInstallJob(ctx, &decidim, &resources) + if err != nil { + return err + } + return r.completeHook(ctx, patcher, &decidim, jm, job) + }) + if err != nil { + return ctrl.Result{}, err + } + + err = lshr.OnUpgrade(&decidim, func() error { + log.Info("Upgrading") + job, err := r.reconcileUpgradeJob(ctx, &decidim, &resources) + if err != nil { + return err + } + return r.completeHook(ctx, patcher, &decidim, jm, job) + }) + if err != nil { + return ctrl.Result{}, err + } + + if lshr.IsJobNotComleted(&decidim) { + log.Info("Waiting for lifecycle job") + return ctrl.Result{}, nil + } + + err = r.reconcileMemcachedDeployment(ctx, &decidim) + if err != nil { + return ctrl.Result{}, err + } + + err = r.reconcileMemcachedService(ctx, &decidim) + if err != nil { + return ctrl.Result{}, err + } + + err = r.reconcileMaintenance(ctx, &decidim, patcher, &resources) + if err != nil { + return ctrl.Result{}, err + } + + sidekiqDep, err := r.reconcileSidekiqDeployment(ctx, &decidim, &resources) + if err != nil { + return ctrl.Result{}, err + } + + _, err = r.reconcileHpaSidekiq(ctx, &decidim, sidekiqDep) + if err != nil { + return ctrl.Result{}, err + } + + dep, err := r.reconcileDeployment(ctx, &decidim, &resources) + if err != nil { + return ctrl.Result{}, err + } + + err = r.reconcilePodDisruptionBudget(ctx, &decidim) + if err != nil { + return ctrl.Result{}, err + } + + _, err = r.reconcileHpa(ctx, &decidim, dep) + if err != nil { + return ctrl.Result{}, err + } + + svc, err := r.reconcileService(ctx, &decidim) + if err != nil { + return ctrl.Result{}, err + } + + _, err = r.reconcileIngress(ctx, &decidim, svc) + if err != nil { + return ctrl.Result{}, err + } + + if r.EndpointMonitorEnabled { + err = r.reconcileIMC(ctx, &decidim) + if err != nil { + return ctrl.Result{}, err + } + } + + if !internal.HealthCheck(ctx, svc) { + return ctrl.Result{RequeueAfter: 2 * time.Second}, nil + } + + return ctrl.Result{}, lshr.Complete(ctx, r, patcher, &decidim, lshr.PatchOpts{}) +} + +// todo : move to runtime +func (r *DecidimReconciler) completeHook(ctx context.Context, patcher *patch.SerialPatcher, decidim *appsv1alpha1.Decidim, jm lshr.JobManager, job *batchv1.Job) error { + jm.Add(job) + isJobNotComleted, err := jm.UpdateCondition(ctx) + if err != nil { + return err + } + if !isJobNotComleted { + lshr.SetCurrentVersion(decidim) + delete(decidim.Annotations, "core.libre.sh/lifecycle") + err := r.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground)) + if err != nil { + return err + } + } + return lshr.Patch(ctx, r, patcher, decidim, lshr.PatchOpts{}) +} + +// SetupWithManager sets up the controller with the Manager. +func (r *DecidimReconciler) SetupWithManager(mgr ctrl.Manager) error { + return ctrl.NewControllerManagedBy(mgr). + For(&appsv1alpha1.Decidim{}). + Owns(&lshcore.Postgres{}). + Owns(&lshcore.Bucket{}). + Owns(&lshcore.Redis{}). + Owns(&batchv1.Job{}). + Owns(&appsv1.Deployment{}). + Complete(r) +} + +func (r *DecidimReconciler) Name() string { + return "decidim-controller" +} + +func (r *DecidimReconciler) OwnedConditions() []string { + return []string{ + meta.ReconcilingCondition, + meta.StalledCondition, + lshmeta.DependenciesNotReady, + lshmeta.HookJobNotComleted, + } +} diff --git a/internal/controller/apps/decidim_controller_affinity.go b/internal/controller/apps/decidim_controller_affinity.go new file mode 100644 index 0000000..666f4cc --- /dev/null +++ b/internal/controller/apps/decidim_controller_affinity.go @@ -0,0 +1,65 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 apps + +import ( + corev1 "k8s.io/api/core/v1" +) + +func (r *DecidimReconciler) setAffinity(podSpec *corev1.PodSpec) { + podSpec.Affinity = &corev1.Affinity{ + NodeAffinity: &corev1.NodeAffinity{ + PreferredDuringSchedulingIgnoredDuringExecution: []corev1.PreferredSchedulingTerm{ + { + Weight: 20, + Preference: corev1.NodeSelectorTerm{ + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "node.libre.sh/roles", + Operator: corev1.NodeSelectorOpDoesNotExist, + }, + }, + }, + }, + { + Weight: 100, + Preference: corev1.NodeSelectorTerm{ + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "node.libre.sh/roles", + Operator: corev1.NodeSelectorOpNotIn, + Values: []string{"postgres"}, + }, + }, + }, + }, + { + Weight: 80, + Preference: corev1.NodeSelectorTerm{ + MatchExpressions: []corev1.NodeSelectorRequirement{ + { + Key: "node.libre.sh/roles", + Operator: corev1.NodeSelectorOpNotIn, + Values: []string{"minio"}, + }, + }, + }, + }, + }, + }, + } +} diff --git a/internal/controller/apps/decidim_controller_bucket.go b/internal/controller/apps/decidim_controller_bucket.go new file mode 100644 index 0000000..2d6c8d3 --- /dev/null +++ b/internal/controller/apps/decidim_controller_bucket.go @@ -0,0 +1,40 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 apps + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + appsv1alpha1 "libre.sh/api/apps/v1alpha1" + lshcore "libre.sh/api/core/v1alpha1" + lshr "libre.sh/pkg/controller-runtime" +) + +func (r *DecidimReconciler) reconcileBucket(ctx context.Context, decidim *appsv1alpha1.Decidim, resources *decidimResources) error { + var bucket lshcore.Bucket + resources.bucket = &bucket + lshr.SetResourceNamespacedName(decidim, &bucket) + return lshr.CreateOrPatch(ctx, r, &bucket, func() error { + lshr.ApplyLabels(decidim, &bucket, nil) + bucket.Spec.Suspend = decidim.GetSuspend() + bucket.Spec.Policy = lshcore.BucketPolicy{Preset: lshcore.BucketPrivatePreset} + bucket.Spec.Provider = lshcore.BucketDataProvider + return controllerutil.SetControllerReference(decidim, &bucket, r.Scheme()) + }) +} diff --git a/internal/controller/apps/decidim_controller_config.go b/internal/controller/apps/decidim_controller_config.go new file mode 100644 index 0000000..bfdfcec --- /dev/null +++ b/internal/controller/apps/decidim_controller_config.go @@ -0,0 +1,44 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 apps + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/rand" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + appsv1alpha1 "libre.sh/api/apps/v1alpha1" + lshr "libre.sh/pkg/controller-runtime" +) + +func (r *DecidimReconciler) reconcileSecret(ctx context.Context, decidim *appsv1alpha1.Decidim, resources *decidimResources) error { + var secret corev1.Secret + resources.secret = &secret + secret.Name = lshr.GetResourceName(decidim) + secret.Namespace = decidim.Namespace + return lshr.CreateOrPatch(ctx, r, &secret, func() error { + if secret.Data == nil { + secret.Data = make(map[string][]byte) + } + if len(secret.Data["SECRET_KEY_BASE"]) == 0 { + secret.Data["SECRET_KEY_BASE"] = []byte(rand.String(32)) + } + return controllerutil.SetControllerReference(decidim, &secret, r.Scheme()) + }) +} diff --git a/internal/controller/apps/decidim_controller_deploy.go b/internal/controller/apps/decidim_controller_deploy.go new file mode 100644 index 0000000..1bc2509 --- /dev/null +++ b/internal/controller/apps/decidim_controller_deploy.go @@ -0,0 +1,125 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 apps + +import ( + "context" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + appsv1alpha1 "libre.sh/api/apps/v1alpha1" + lshr "libre.sh/pkg/controller-runtime" +) + +func (r *DecidimReconciler) reconcileDeployment(ctx context.Context, decidim *appsv1alpha1.Decidim, resources *decidimResources) (*appsv1.Deployment, error) { + var deployment appsv1.Deployment + lshr.SetResourceNamespacedName(decidim, &deployment, "app") + err := lshr.CreateOrPatch(ctx, r, &deployment, func() error { + lshr.ApplyLabels(decidim, &deployment, &lshr.LabelOpts{ + Component: "app", + Version: decidim.GetVersion(), + }) + deployment.Spec.Selector = &metav1.LabelSelector{ + MatchLabels: lshr.ExtractLabelSelector(&deployment), + } + deployment.Spec.Template.ObjectMeta.Labels = deployment.Labels + deployment.Spec.Template.Spec.TopologySpreadConstraints = []corev1.TopologySpreadConstraint{ + { + MaxSkew: 1, + TopologyKey: "kubernetes.io/hostname", + WhenUnsatisfiable: corev1.ScheduleAnyway, + LabelSelector: deployment.Spec.Selector, + }, + } + + if deployment.Spec.Replicas == nil { + replicas := int32(2) + deployment.Spec.Replicas = &replicas + } + + r.setAffinity(&deployment.Spec.Template.Spec) + + probeHandler := corev1.ProbeHandler{ + HTTPGet: &corev1.HTTPGetAction{ + Path: "/health_check", + Port: intstr.FromString("http"), + }, + } + appContainer := corev1.Container{ + Image: decidim.Spec.Image, + ImagePullPolicy: corev1.PullAlways, + Name: "app", + Ports: []corev1.ContainerPort{ + { + ContainerPort: 3000, + Name: "http", + Protocol: corev1.ProtocolTCP, + }, + }, + // TODO no endpoint pour readiness and liveness ? + // TODO no securityContext ? + EnvFrom: append(decidim.Spec.EnvFrom, corev1.EnvFromSource{ + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: resources.secret.Name, + }, + }, + }), + StartupProbe: &corev1.Probe{ + ProbeHandler: probeHandler, + InitialDelaySeconds: 10, + FailureThreshold: 11, + PeriodSeconds: 10, + }, + ReadinessProbe: &corev1.Probe{ + ProbeHandler: probeHandler, + PeriodSeconds: 10, + TimeoutSeconds: 30, + FailureThreshold: 3, + }, + LivenessProbe: &corev1.Probe{ + ProbeHandler: probeHandler, + PeriodSeconds: 60, + TimeoutSeconds: 30, + FailureThreshold: 3, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("500Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + }, + } + + envVars := r.getEnvVars(decidim, resources) + + appContainer.Env = envVars + + deployment.Spec.Template.Spec.Containers = []corev1.Container{appContainer /* , webContainer */} + return controllerutil.SetControllerReference(decidim, &deployment, r.Scheme()) + }) + + return &deployment, err +} diff --git a/internal/controller/apps/decidim_controller_deploy_sidekiq.go b/internal/controller/apps/decidim_controller_deploy_sidekiq.go new file mode 100644 index 0000000..72091c5 --- /dev/null +++ b/internal/controller/apps/decidim_controller_deploy_sidekiq.go @@ -0,0 +1,99 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 apps + +import ( + "context" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + appsv1alpha1 "libre.sh/api/apps/v1alpha1" + lshr "libre.sh/pkg/controller-runtime" +) + +func (r *DecidimReconciler) reconcileSidekiqDeployment(ctx context.Context, decidim *appsv1alpha1.Decidim, resources *decidimResources) (*appsv1.Deployment, error) { + const component = "sidekiq" + var deployment appsv1.Deployment + lshr.SetResourceNamespacedName(decidim, &deployment, component) + err := lshr.CreateOrPatch(ctx, r, &deployment, func() error { + lshr.ApplyLabels(decidim, &deployment, &lshr.LabelOpts{ + Component: component, + Version: decidim.GetVersion(), + }) + deployment.Spec.Selector = &metav1.LabelSelector{ + MatchLabels: lshr.ExtractLabelSelector(&deployment), + } + deployment.Spec.Template.ObjectMeta.Labels = deployment.Labels + deployment.Spec.Template.ObjectMeta = metav1.ObjectMeta{ + Labels: deployment.Labels, + } + + r.setAffinity(&deployment.Spec.Template.Spec) + + container := corev1.Container{ + Image: decidim.Spec.Image, + ImagePullPolicy: corev1.PullAlways, + Name: "sidekiq", + Ports: []corev1.ContainerPort{ + { + ContainerPort: 7433, + Name: "api", + Protocol: corev1.ProtocolTCP, + }, + }, + Command: []string{"sidekiq", "-C", "config/sidekiq.yml"}, + Lifecycle: &corev1.Lifecycle{ + PreStop: &corev1.LifecycleHandler{ + Exec: &corev1.ExecAction{ + Command: []string{"./sidekiq_quiet.sh"}, + }, + }, + }, + // TODO no securityContext ? + EnvFrom: append(decidim.Spec.EnvFrom, corev1.EnvFromSource{ + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: resources.secret.Name, + }, + }, + }), + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("500Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("500m"), + corev1.ResourceMemory: resource.MustParse("2Gi"), + }, + }, + } + + envVars := r.getEnvVars(decidim, resources) + + container.Env = envVars + + deployment.Spec.Template.Spec.Containers = []corev1.Container{container} + + return controllerutil.SetControllerReference(decidim, &deployment, r.Scheme()) + }) + return &deployment, err +} diff --git a/internal/controller/apps/decidim_controller_env.go b/internal/controller/apps/decidim_controller_env.go new file mode 100644 index 0000000..c69c01e --- /dev/null +++ b/internal/controller/apps/decidim_controller_env.go @@ -0,0 +1,183 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 apps + +import ( + "fmt" + "strings" + + corev1 "k8s.io/api/core/v1" + + appsv1alpha1 "libre.sh/api/apps/v1alpha1" + lshmeta "libre.sh/api/meta/v1alpha1" + lshr "libre.sh/pkg/controller-runtime" +) + +func (r *DecidimReconciler) getEnvVars(decidim *appsv1alpha1.Decidim, resources *decidimResources) []corev1.EnvVar { + return []corev1.EnvVar{ + /* + + FRANCE_CONNECT_PROFILE_IDENTIFIER + FRANCE_CONNECT_PROFILE_SECRET + FRANCE_CONNECT_UID_IDENTIFIER + FRANCE_CONNECT_UID_SECRET + RAILS_ENV + SECRET_KEY_BASE + SESSION_DAYS_TRIM_THRESHOLD + */ + {Name: "RAILS_ENV", Value: "production"}, + {Name: "APP_NAME", Value: decidim.Name}, + {Name: "WEB_CONCURRENCY", Value: "0"}, + {Name: "CACHE_ASSETS", Value: "true"}, + {Name: "RAILS_LOG_TO_STDOUT", Value: "true"}, + {Name: "RAILS_SERVE_STATIC_FILES", Value: "true"}, + {Name: "NEW_RELIC_ENABLED", Value: "false"}, + {Name: "FORCE_SSL", Value: "1"}, + {Name: "DEFAULT_LOCALE", Value: decidim.Spec.Locale.Default}, + {Name: "AVAILABLE_LOCALES", Value: strings.Join(decidim.Spec.Locale.Available, ",")}, + + {Name: "ENABLE_RACK_ATTACK", Value: "0"}, + {Name: "RACK_ATTACK_FAIL2BAN", Value: "0"}, + + {Name: "ASSET_HOST", Value: decidim.Spec.Host}, + { + Name: "MEMCACHE_SERVERS", + Value: fmt.Sprintf("%s:%d", lshr.GetResourceName(decidim, "memcached"), MEMCACHED_PORT), + }, + { + Name: "POD_IP", + ValueFrom: &corev1.EnvVarSource{ + FieldRef: &corev1.ObjectFieldSelector{ + FieldPath: "status.podIP", + }, + }, + }, + { + Name: "SCALEWAY_ID", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: resources.bucket.SecretName(), + }, + Key: "accessKey", + }, + }, + }, + { + Name: "SCALEWAY_TOKEN", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: resources.bucket.SecretName(), + }, + Key: "secretKey", + }, + }, + }, + { + Name: "OBJECTSTORE_S3_HOST", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: resources.bucket.SecretName(), + }, + Key: "endpoint", + }, + }, + }, + { + Name: "SCALEWAY_BUCKET_NAME", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: resources.bucket.SecretName(), + }, + Key: "bucket", + }, + }, + }, + { + Name: "DATABASE_USERNAME", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: resources.postgres.SecretName(), + }, + Key: lshmeta.SecretUsernameKey, + }, + }, + }, + { + Name: "DATABASE_PASSWORD", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: resources.postgres.SecretName(), + }, + Key: lshmeta.SecretPasswordKey, + }, + }, + }, + { + Name: "DATABASE_HOST", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: resources.postgres.SecretName(), + }, + Key: lshmeta.SecretHostKey, + }, + }, + }, + { + Name: "DATABASE_PORT", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: resources.postgres.SecretName(), + }, + Key: lshmeta.SecretPortKey, + }, + }, + }, + { + Name: "DATABASE_NAME", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: resources.postgres.SecretName(), + }, + Key: "database", + }, + }, + }, + { + Name: "REDIS_URL", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: resources.redis.SecretName(), + }, + Key: "url", + }, + }, + }, + // TODO no port for redis need to be specified ? + // TODO add smtp + // TODO check all secrets/configmap that should be set + } +} diff --git a/internal/controller/apps/decidim_controller_hpa.go b/internal/controller/apps/decidim_controller_hpa.go new file mode 100644 index 0000000..317d40e --- /dev/null +++ b/internal/controller/apps/decidim_controller_hpa.go @@ -0,0 +1,95 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 apps + +import ( + "context" + + appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + appsv1alpha1 "libre.sh/api/apps/v1alpha1" + lshr "libre.sh/pkg/controller-runtime" +) + +func (r *DecidimReconciler) reconcileHpa(ctx context.Context, decidim *appsv1alpha1.Decidim, deployment *appsv1.Deployment) (*autoscalingv2.HorizontalPodAutoscaler, error) { + var hpa autoscalingv2.HorizontalPodAutoscaler + lshr.SetResourceNamespacedName(decidim, &hpa, "app") + err := lshr.CreateOrPatch(ctx, r, &hpa, func() error { + lshr.ApplyLabels(decidim, &hpa, &lshr.LabelOpts{ + Component: "app", + Version: decidim.GetVersion(), + }) + hpa.Spec.ScaleTargetRef = autoscalingv2.CrossVersionObjectReference{ + APIVersion: appsv1.SchemeGroupVersion.String(), + Kind: "Deployment", + Name: deployment.Name, + } + min := int32(2) + hpa.Spec.MinReplicas = &min + hpa.Spec.MaxReplicas = 40 + var averageUtilization int32 = 150 + hpa.Spec.Metrics = []autoscalingv2.MetricSpec{ + { + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: "cpu", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: &averageUtilization, + }, + }, + }, + } + var stabilizationWindowSeconds int32 = 300 + hpa.Spec.Behavior = &autoscalingv2.HorizontalPodAutoscalerBehavior{ + ScaleUp: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: &stabilizationWindowSeconds, + Policies: []autoscalingv2.HPAScalingPolicy{ + { + Type: autoscalingv2.PodsScalingPolicy, + Value: 1, + PeriodSeconds: 15, + }, + { + Type: autoscalingv2.PercentScalingPolicy, + Value: 50, + PeriodSeconds: 15, + }, + }, + }, + ScaleDown: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: &stabilizationWindowSeconds, + Policies: []autoscalingv2.HPAScalingPolicy{ + { + Type: autoscalingv2.PodsScalingPolicy, + Value: 1, + PeriodSeconds: 15, + }, + { + Type: autoscalingv2.PodsScalingPolicy, + Value: 25, + PeriodSeconds: 15, + }, + }, + }, + } + return controllerutil.SetControllerReference(decidim, &hpa, r.Scheme()) + }) + return &hpa, err +} diff --git a/internal/controller/apps/decidim_controller_hpa_sidekiq.go b/internal/controller/apps/decidim_controller_hpa_sidekiq.go new file mode 100644 index 0000000..c462adb --- /dev/null +++ b/internal/controller/apps/decidim_controller_hpa_sidekiq.go @@ -0,0 +1,85 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 apps + +import ( + "context" + + appsv1 "k8s.io/api/apps/v1" + autoscalingv2 "k8s.io/api/autoscaling/v2" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + appsv1alpha1 "libre.sh/api/apps/v1alpha1" + lshr "libre.sh/pkg/controller-runtime" +) + +func (r *DecidimReconciler) reconcileHpaSidekiq(ctx context.Context, decidim *appsv1alpha1.Decidim, deployment *appsv1.Deployment) (*autoscalingv2.HorizontalPodAutoscaler, error) { + var hpa autoscalingv2.HorizontalPodAutoscaler + lshr.SetResourceNamespacedName(decidim, &hpa, "sidekiq") + err := lshr.CreateOrPatch(ctx, r, &hpa, func() error { + lshr.ApplyLabels(decidim, &hpa, &lshr.LabelOpts{ + Component: "sidekiq", + Version: decidim.GetVersion(), + }) + hpa.Spec.ScaleTargetRef = autoscalingv2.CrossVersionObjectReference{ + APIVersion: appsv1.SchemeGroupVersion.String(), + Kind: "Deployment", + Name: deployment.Name, + } + min := int32(1) + hpa.Spec.MinReplicas = &min + hpa.Spec.MaxReplicas = 5 + var averageUtilization int32 = 150 + hpa.Spec.Metrics = []autoscalingv2.MetricSpec{ + { + Type: autoscalingv2.ResourceMetricSourceType, + Resource: &autoscalingv2.ResourceMetricSource{ + Name: "cpu", + Target: autoscalingv2.MetricTarget{ + Type: autoscalingv2.UtilizationMetricType, + AverageUtilization: &averageUtilization, + }, + }, + }, + } + var stabilizationWindowSeconds int32 = 300 + hpa.Spec.Behavior = &autoscalingv2.HorizontalPodAutoscalerBehavior{ + ScaleUp: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: &stabilizationWindowSeconds, + Policies: []autoscalingv2.HPAScalingPolicy{ + { + Type: autoscalingv2.PodsScalingPolicy, + Value: 1, + PeriodSeconds: 30, + }, + }, + }, + ScaleDown: &autoscalingv2.HPAScalingRules{ + StabilizationWindowSeconds: &stabilizationWindowSeconds, + Policies: []autoscalingv2.HPAScalingPolicy{ + { + Type: autoscalingv2.PodsScalingPolicy, + Value: 1, + PeriodSeconds: 15, + }, + }, + }, + } + return controllerutil.SetControllerReference(decidim, &hpa, r.Scheme()) + }) + return &hpa, err +} diff --git a/internal/controller/apps/decidim_controller_ingress.go b/internal/controller/apps/decidim_controller_ingress.go new file mode 100644 index 0000000..8913090 --- /dev/null +++ b/internal/controller/apps/decidim_controller_ingress.go @@ -0,0 +1,104 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 apps + +import ( + "context" + "fmt" + + imcv1alpha1 "github.com/stakater/IngressMonitorController/v2/api/v1alpha1" + corev1 "k8s.io/api/core/v1" + networkingv1 "k8s.io/api/networking/v1" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + appsv1alpha1 "libre.sh/api/apps/v1alpha1" + lshr "libre.sh/pkg/controller-runtime" +) + +func (r *DecidimReconciler) getIngressRule(decidim *appsv1alpha1.Decidim, svc *corev1.Service, host string) networkingv1.IngressRule { + pathType := networkingv1.PathTypePrefix + return networkingv1.IngressRule{ + Host: host, + IngressRuleValue: networkingv1.IngressRuleValue{ + HTTP: &networkingv1.HTTPIngressRuleValue{ + Paths: []networkingv1.HTTPIngressPath{ + { + Path: "/", + PathType: &pathType, + Backend: networkingv1.IngressBackend{ + Service: &networkingv1.IngressServiceBackend{ + Name: svc.Name, + Port: networkingv1.ServiceBackendPort{ + Name: "http", + }, + }, + }, + }, + }, + }, + }, + } +} + +func (r *DecidimReconciler) reconcileIngress(ctx context.Context, decidim *appsv1alpha1.Decidim, svc *corev1.Service) (*networkingv1.Ingress, error) { + var ingress networkingv1.Ingress + lshr.SetResourceNamespacedName(decidim, &ingress) + err := lshr.CreateOrPatch(ctx, r, &ingress, func() error { + ingress.ObjectMeta.Annotations = map[string]string{ + "kubernetes.io/tls-acme": "true", + "nginx.ingress.kubernetes.io/proxy-body-size": "100g", + "nginx.ingress.kubernetes.io/proxy-request-buffering": "off", + } + + ingressTLS := networkingv1.IngressTLS{ + Hosts: []string{decidim.Spec.Host}, + SecretName: fmt.Sprintf("%s-tls", decidim.Name), + } + + ingress.Spec.Rules = []networkingv1.IngressRule{ + r.getIngressRule(decidim, svc, decidim.Spec.Host), + } + + for _, additionalHost := range decidim.Spec.AdditionalHosts { + ingressTLS.Hosts = append(ingressTLS.Hosts, additionalHost) + ingress.Spec.Rules = append(ingress.Spec.Rules, r.getIngressRule(decidim, svc, additionalHost)) + } + + ingress.Spec.TLS = []networkingv1.IngressTLS{ingressTLS} + + return controllerutil.SetControllerReference(decidim, &ingress, r.Scheme()) + }) + return &ingress, err +} + +func (r *DecidimReconciler) reconcileIMC(ctx context.Context, decidim *appsv1alpha1.Decidim) error { + var imc imcv1alpha1.EndpointMonitor + lshr.SetResourceNamespacedName(decidim, &imc) + return lshr.CreateOrPatch(ctx, r, &imc, func() error { + imc.Spec.ForceHTTPS = true + imc.Spec.HealthEndpoint = "/" + imc.Spec.URLFrom = &imcv1alpha1.URLSource{ + IngressRef: &imcv1alpha1.IngressURLSource{ + Name: imc.Name, + }, + } + imc.Spec.UptimeRobotConfig = &imcv1alpha1.UptimeRobotConfig{ + Interval: 60, + } + return controllerutil.SetControllerReference(decidim, &imc, r.Scheme()) + }) +} diff --git a/internal/controller/apps/decidim_controller_job.go b/internal/controller/apps/decidim_controller_job.go new file mode 100644 index 0000000..cfae05e --- /dev/null +++ b/internal/controller/apps/decidim_controller_job.go @@ -0,0 +1,86 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 apps + +import ( + "context" + + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + appsv1alpha1 "libre.sh/api/apps/v1alpha1" + lshr "libre.sh/pkg/controller-runtime" +) + +func (r *DecidimReconciler) mutateJob(decidim *appsv1alpha1.Decidim, resources *decidimResources, job *batchv1.Job, command ...string) { + r.setAffinity(&job.Spec.Template.Spec) + container := corev1.Container{ + Image: decidim.Spec.Image, + ImagePullPolicy: corev1.PullAlways, + Command: command, + Name: "decidim", + + Env: r.getEnvVars(decidim, resources), + EnvFrom: append(decidim.Spec.EnvFrom, corev1.EnvFromSource{ + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: resources.secret.Name, + }, + }, + }), + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("500m"), + corev1.ResourceMemory: resource.MustParse("512Mi"), + }, + }, + } + + backoffLimit := int32(0) + job.Spec.BackoffLimit = &backoffLimit + /* job.Spec.Template.Spec.SecurityContext = &corev1.PodSecurityContext{ + RunAsUser: &UserID, + RunAsGroup: &UserID, + FSGroup: &UserID, + } */ + job.Spec.Template.Spec.Containers = []corev1.Container{container} + job.Spec.Template.Spec.RestartPolicy = corev1.RestartPolicyNever +} + +func (r *DecidimReconciler) reconcileInstallJob(ctx context.Context, decidim *appsv1alpha1.Decidim, resources *decidimResources) (*batchv1.Job, error) { + var job batchv1.Job + job.Name = lshr.GetResourceName(decidim, "install") + job.Namespace = decidim.Namespace + return &job, lshr.CreateOrPatch(ctx, r, &job, func() error { + r.mutateJob(decidim, resources, &job, + "bundle", "exec", "rails", "db:create", "db:migrate") + return controllerutil.SetControllerReference(decidim, &job, r.Scheme()) + }) +} + +func (r *DecidimReconciler) reconcileUpgradeJob(ctx context.Context, decidim *appsv1alpha1.Decidim, resources *decidimResources) (*batchv1.Job, error) { + var job batchv1.Job + job.Name = lshr.GetResourceName(decidim, "upgrade") + job.Namespace = decidim.Namespace + return &job, lshr.CreateOrPatch(ctx, r, &job, func() error { + r.mutateJob(decidim, resources, &job, + "rake", "decidim_app:k8s:upgrade") + return controllerutil.SetControllerReference(decidim, &job, r.Scheme()) + }) +} diff --git a/internal/controller/apps/decidim_controller_maintenance.go b/internal/controller/apps/decidim_controller_maintenance.go new file mode 100644 index 0000000..7e4280a --- /dev/null +++ b/internal/controller/apps/decidim_controller_maintenance.go @@ -0,0 +1,106 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 apps + +import ( + "context" + "fmt" + "strconv" + + "github.com/fluxcd/pkg/runtime/patch" + batchv1 "k8s.io/api/batch/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + "sigs.k8s.io/controller-runtime/pkg/log" + + appsv1alpha1 "libre.sh/api/apps/v1alpha1" + lshr "libre.sh/pkg/controller-runtime" +) + +const maintenanceAnnotation = "decidim.libre.sh/maintenance" + +func (r *DecidimReconciler) reconcileMaintenance(ctx context.Context, decidim *appsv1alpha1.Decidim, patcher *patch.SerialPatcher, resources *decidimResources) error { + log := log.FromContext(ctx) + var job batchv1.Job + lshr.SetResourceNamespacedName(decidim, &job, "maintenance") + + if decidim.Annotations[maintenanceAnnotation] == "" { + err := r.Delete(ctx, &job, client.PropagationPolicy(metav1.DeletePropagationForeground)) + if err == nil { + log.Info("Maintenance job deleted") + } + return client.IgnoreNotFound(err) + } + + minutes, err := strconv.Atoi(decidim.Annotations[maintenanceAnnotation]) + if err != nil { + return fmt.Errorf("%s should be a number of minutes: %w", maintenanceAnnotation, err) + } + + err = lshr.CreateOrPatch(ctx, r, &job, func() error { + if job.CreationTimestamp.IsZero() { + log.Info("Creating maintenance job") + } + lshr.ApplyLabels(decidim, &job, &lshr.LabelOpts{ + Component: "maintenance", + Version: decidim.GetVersion(), + }) + container := corev1.Container{ + Image: decidim.Spec.Image, + ImagePullPolicy: corev1.PullAlways, + Name: "main", + Command: []string{"sh", "-c"}, + Args: []string{fmt.Sprintf("echo waiting %dm && sleep %dm", minutes, minutes)}, + Env: r.getEnvVars(decidim, resources), + EnvFrom: append(decidim.Spec.EnvFrom, corev1.EnvFromSource{ + SecretRef: &corev1.SecretEnvSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: resources.secret.Name, + }, + }, + }), + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("240m"), + corev1.ResourceMemory: resource.MustParse("500Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceMemory: resource.MustParse("6Gi"), + }, + }, + } + job.Spec.Template.Spec.Containers = []corev1.Container{container} + job.Spec.Template.Spec.RestartPolicy = corev1.RestartPolicyOnFailure + gracePeriod := int64(0) + job.Spec.Template.Spec.TerminationGracePeriodSeconds = &gracePeriod + return controllerutil.SetControllerReference(decidim, &job, r.Scheme()) + }) + if err != nil { + return err + } + + if !job.Status.CompletionTime.IsZero() { + log.Info("Maintenance job is complete, marking it for deletion") + delete(decidim.Annotations, maintenanceAnnotation) + return lshr.Patch(ctx, r, patcher, decidim, lshr.PatchOpts{DisableReadyCondition: true}) + } + + return nil +} diff --git a/internal/controller/apps/decidim_controller_memcached.go b/internal/controller/apps/decidim_controller_memcached.go new file mode 100644 index 0000000..555c42c --- /dev/null +++ b/internal/controller/apps/decidim_controller_memcached.go @@ -0,0 +1,98 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 apps + +import ( + "context" + + appsv1 "k8s.io/api/apps/v1" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/resource" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + appsv1alpha1 "libre.sh/api/apps/v1alpha1" + lshr "libre.sh/pkg/controller-runtime" +) + +const MEMCACHED_PORT = 11211 + +func (r *DecidimReconciler) reconcileMemcachedDeployment(ctx context.Context, decidim *appsv1alpha1.Decidim) error { + var deployment appsv1.Deployment + lshr.SetResourceNamespacedName(decidim, &deployment, "memcached") + return lshr.CreateOrPatch(ctx, r, &deployment, func() error { + lshr.ApplyLabels(decidim, &deployment, &lshr.LabelOpts{ + Component: "memcached", + Version: decidim.GetVersion(), + }) + deployment.Spec.Selector = &metav1.LabelSelector{ + MatchLabels: lshr.ExtractLabelSelector(&deployment), + } + deployment.Spec.Template.ObjectMeta.Labels = deployment.Labels + replicas := int32(1) + deployment.Spec.Replicas = &replicas + deployment.Spec.Template.Spec.Containers = []corev1.Container{ + { + Image: "docker.io/memcached:1.6", + Name: "memcached", + Ports: []corev1.ContainerPort{ + { + ContainerPort: MEMCACHED_PORT, + Protocol: corev1.ProtocolTCP, + }, + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("5m"), + corev1.ResourceMemory: resource.MustParse("32Mi"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("200m"), + corev1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + }, + } + + return controllerutil.SetControllerReference(decidim, &deployment, r.Scheme()) + }) + +} + +func (r *DecidimReconciler) reconcileMemcachedService(ctx context.Context, decidim *appsv1alpha1.Decidim) error { + var svc corev1.Service + lshr.SetResourceNamespacedName(decidim, &svc, "memcached") + return lshr.CreateOrPatch(ctx, r, &svc, func() error { + lshr.ApplyLabels(decidim, &svc, &lshr.LabelOpts{ + Component: "memcached", + Version: decidim.GetVersion(), + }) + + svc.Spec.Ports = []corev1.ServicePort{ + { + Port: MEMCACHED_PORT, + Protocol: corev1.ProtocolTCP, + TargetPort: intstr.FromInt(MEMCACHED_PORT), + }, + } + svc.Spec.Type = corev1.ServiceTypeClusterIP + svc.Spec.Selector = lshr.ExtractLabelSelector(&svc) + + return controllerutil.SetControllerReference(decidim, &svc, r.Scheme()) + }) +} diff --git a/internal/controller/apps/decidim_controller_pdb.go b/internal/controller/apps/decidim_controller_pdb.go new file mode 100644 index 0000000..eedb145 --- /dev/null +++ b/internal/controller/apps/decidim_controller_pdb.go @@ -0,0 +1,45 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 apps + +import ( + "context" + + policyv1 "k8s.io/api/policy/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + appsv1alpha1 "libre.sh/api/apps/v1alpha1" + lshr "libre.sh/pkg/controller-runtime" +) + +func (r *DecidimReconciler) reconcilePodDisruptionBudget(ctx context.Context, decidim *appsv1alpha1.Decidim) error { + var pdb policyv1.PodDisruptionBudget + lshr.SetResourceNamespacedName(decidim, &pdb) + return lshr.CreateOrPatch(ctx, r, &pdb, func() error { + lshr.ApplyLabels(decidim, &pdb, nil) + pdb.Spec.Selector = &v1.LabelSelector{ + MatchLabels: lshr.GetLabelSelector(decidim, &lshr.LabelOpts{ + Component: "app", + }), + } + min := intstr.FromInt(1) + pdb.Spec.MinAvailable = &min + return controllerutil.SetControllerReference(decidim, &pdb, r.Scheme()) + }) +} diff --git a/internal/controller/apps/decidim_controller_postgres.go b/internal/controller/apps/decidim_controller_postgres.go new file mode 100644 index 0000000..1c0f20f --- /dev/null +++ b/internal/controller/apps/decidim_controller_postgres.go @@ -0,0 +1,38 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 apps + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + appsv1alpha1 "libre.sh/api/apps/v1alpha1" + corev1alpha1 "libre.sh/api/core/v1alpha1" + lshr "libre.sh/pkg/controller-runtime" +) + +func (r *DecidimReconciler) reconcilePostgres(ctx context.Context, decidim *appsv1alpha1.Decidim, resources *decidimResources) error { + var postgres corev1alpha1.Postgres + resources.postgres = &postgres + lshr.SetResourceNamespacedName(decidim, &postgres) + return lshr.CreateOrPatch(ctx, r, &postgres, func() error { + lshr.ApplyLabels(decidim, &postgres, nil) + postgres.Spec.Database = "decidim" + return controllerutil.SetControllerReference(decidim, &postgres, r.Scheme()) + }) +} diff --git a/internal/controller/apps/decidim_controller_redis.go b/internal/controller/apps/decidim_controller_redis.go new file mode 100644 index 0000000..789119a --- /dev/null +++ b/internal/controller/apps/decidim_controller_redis.go @@ -0,0 +1,40 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 apps + +import ( + "context" + + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + appsv1alpha1 "libre.sh/api/apps/v1alpha1" + lshcore "libre.sh/api/core/v1alpha1" + lshr "libre.sh/pkg/controller-runtime" +) + +func (r *DecidimReconciler) reconcileRedis(ctx context.Context, decidim *appsv1alpha1.Decidim, resources *decidimResources) error { + var redis lshcore.Redis + resources.redis = &redis + lshr.SetResourceNamespacedName(decidim, &redis) + return lshr.CreateOrPatch(ctx, r, &redis, func() error { + redis.Spec.Suspend = decidim.GetSuspend() + redis.Spec.Persistence = lshcore.RedisPersistence{ + Enabled: true, + } + return controllerutil.SetControllerReference(decidim, &redis, r.Scheme()) + }) +} diff --git a/internal/controller/apps/decidim_controller_svc.go b/internal/controller/apps/decidim_controller_svc.go new file mode 100644 index 0000000..465d012 --- /dev/null +++ b/internal/controller/apps/decidim_controller_svc.go @@ -0,0 +1,56 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 apps + +import ( + "context" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/intstr" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" + + appsv1alpha1 "libre.sh/api/apps/v1alpha1" + lshr "libre.sh/pkg/controller-runtime" +) + +func (r *DecidimReconciler) reconcileService(ctx context.Context, decidim *appsv1alpha1.Decidim) (*corev1.Service, error) { + var svc corev1.Service + lshr.SetResourceNamespacedName(decidim, &svc) + err := lshr.CreateOrPatch(ctx, r, &svc, func() error { + lshr.ApplyLabels(decidim, &svc, &lshr.LabelOpts{ + Component: "app", + Version: decidim.GetVersion(), + }) + targetPort := intstr.IntOrString{ + StrVal: "http", + Type: intstr.String, + } + svc.Spec.Ports = []corev1.ServicePort{ + { + Name: "http", + Port: 80, + Protocol: corev1.ProtocolTCP, + TargetPort: targetPort, + }, + } + svc.Spec.Type = corev1.ServiceTypeClusterIP + svc.Spec.Selector = lshr.ExtractLabelSelector(&svc) + + return controllerutil.SetControllerReference(decidim, &svc, r.Scheme()) + }) + return &svc, err +} diff --git a/internal/decidim/health.go b/internal/decidim/health.go new file mode 100644 index 0000000..36f8736 --- /dev/null +++ b/internal/decidim/health.go @@ -0,0 +1,38 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 decidim + +import ( + "context" + "fmt" + "net/http" + + corev1 "k8s.io/api/core/v1" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +func HealthCheck(ctx context.Context, svc *corev1.Service) bool { + log := log.FromContext(ctx) + url := fmt.Sprintf("http://%s.%s.svc.cluster.local/health_check", svc.Name, svc.Namespace) + resp, err := http.Get(url) + if err == nil && resp.StatusCode == http.StatusOK { + log.Info("Health check is ok") + return true + } + log.Info("Waiting for health check to complete") + return false +} diff --git a/internal/decidim/pg.go b/internal/decidim/pg.go new file mode 100644 index 0000000..522f24e --- /dev/null +++ b/internal/decidim/pg.go @@ -0,0 +1,168 @@ +/* +Copyright 2023 IndieHosters. + +Licensed under the EUPL, Version 1.2 or later (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://joinup.ec.europa.eu/collection/eupl/eupl-text-eupl-12 + +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 decidim + +import ( + "context" + "database/sql" + "fmt" + "strings" + "time" + _ "time/tzdata" + + "github.com/lib/pq" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/types" + appsv1alpha1 "libre.sh/api/apps/v1alpha1" + lshcore "libre.sh/api/core/v1alpha1" + lshmeta "libre.sh/api/meta/v1alpha1" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/log" +) + +func OpenPostgreSQL(ctx context.Context, c client.Client, postgres *lshcore.Postgres) (*sql.DB, error) { + var secret corev1.Secret + err := c.Get(ctx, types.NamespacedName{ + Namespace: postgres.Namespace, + Name: postgres.SecretName(), + }, &secret) + if err != nil { + return nil, err + } + return sql.Open("postgres", string(secret.Data[lshmeta.SecretURLKey])) +} + +func SyncOrganization(ctx context.Context, decidim *appsv1alpha1.Decidim, db *sql.DB) error { + rows, err := db.Query(`SELECT count(*) from decidim_organizations WHERE id=$1`, decidim.Spec.Organization.ID) + if err != nil { + return err + } + defer rows.Close() + var count int + if rows.Next() { + err = rows.Scan(&count) + if err != nil { + return err + } + } + log.FromContext(ctx).Info("sql count", "value", count) + kv := getRowKV(decidim) + var args []interface{} + var query string + if count == 0 { + query, args = getInsertQuery(decidim, kv) + } else { + query, args = getUpdateQuery(decidim, kv) + } + _, err = db.Exec(query, args...) + return err +} + +func getTZNow(decidim *appsv1alpha1.Decidim) time.Time { + tz, err := time.LoadLocation(decidim.Spec.TimeZone) + if err != nil { + panic(err) + } + return time.Now().In(tz) +} + +func getRowKV(decidim *appsv1alpha1.Decidim) map[string]interface{} { + return map[string]interface{}{ + "name": decidim.Name, + "host": decidim.Spec.Host, + "default_locale": decidim.Spec.Locale.Default, + "available_locales": pq.Array(decidim.Spec.Locale.Available), + "updated_at": getTZNow(decidim), + // "description": nil, + // "logo": nil, + // "twitter_handler": nil, + // "show_statistics": true, + // "favicon": nil, + // "instagram_handler": nil, + // "facebook_handler": nil, + // "youtube_handler": nil, + // "github_handler": nil, + // "official_img_header": nil, + // "official_img_footer": nil, + // "official_url": nil, + "reference_prefix": "/", + "secondary_hosts": pq.Array(decidim.Spec.AdditionalHosts), + "available_authorizations": pq.Array(decidim.Spec.AvailableAuthorizations), + "file_upload_settings": decidim.Spec.FileUploadSettings.Raw, + // "header_snippets": nil, + // "cta_button_text": nil, + // "cta_button_path": nil, + // "enable_omnipresent_banner": false, + // "omnipresent_banner_title": nil, + // "omnipresent_banner_short_description": nil, + // "omnipresent_banner_url": nil, + // "highlighted_content_banner_enabled": false, + // "highlighted_content_banner_title": nil, + // "highlighted_content_banner_short_description": nil, + // "highlighted_content_banner_action_title": nil, + // "highlighted_content_banner_action_subtitle": nil, + // "highlighted_content_banner_action_url": nil, + // "highlighted_content_banner_image": nil, + // "tos_version": nil, + // "badges_enabled": nil, + // "send_welcome_notification": nil, + // "welcome_notification_subject": nil, + // "welcome_notification_body": nil, + "users_registration_mode": decidim.Spec.UsersRegistrationMode, + // "id_documents_methods": nil, + // "id_documents_explanation_text": nil, + // "user_groups_enabled": nil, + // "smtp_settings": nil, + // "colors": nil, + "force_users_to_authenticate_before_access_organization": decidim.Spec.ForceUsersToAuthenticateBeforeAccessOrganization, + // "omniauth_settings": nil, + // "rich_text_editor_in_public_views": nil, + // "admin_terms_of_use_body": nil, + "time_zone": decidim.Spec.TimeZone, + // "deepl_api_key": nil, + } +} + +func getInsertQuery(decidim *appsv1alpha1.Decidim, kv map[string]interface{}) (string, []interface{}) { + kv["id"] = decidim.Spec.Organization.ID + kv["created_at"] = getTZNow(decidim) + columns := []string{} + values := []string{} + args := []interface{}{} + i := 1 + for k, v := range kv { + columns = append(columns, k) + args = append(args, v) + values = append(values, fmt.Sprintf("$%d", i)) + i++ + } + return fmt.Sprintf("INSERT INTO decidim_organizations (%s) VALUES (%s)", strings.Join(columns, ","), strings.Join(values, ",")), args +} + +func getUpdateQuery(decidim *appsv1alpha1.Decidim, kv map[string]interface{}) (string, []interface{}) { + args := []interface{}{} + sets := []string{} + i := 1 + for k, v := range kv { + set := fmt.Sprintf("%s=$%d", k, i) + sets = append(sets, set) + args = append(args, v) + i++ + } + args = append(args, decidim.Spec.Organization.ID) + return fmt.Sprintf("UPDATE decidim_organizations SET %s WHERE id=$%d", strings.Join(sets, ","), i), args +}