├── .dockerignore ├── .gitignore ├── .golangci.yml ├── Backlog ├── Dockerfile ├── LICENCE ├── Makefile ├── PROJECT ├── README.md ├── Upgrade-Kubebuilder ├── api └── v1 │ ├── groupversion_info.go │ ├── kubegres_types.go │ └── zz_generated.deepcopy.go ├── cmd ├── main.go └── yamlcopy │ └── WriteYamlTemplatesInGoFile.go ├── config ├── crd │ ├── bases │ │ └── kubegres.reactive-tech.io_kubegres.yaml │ ├── kustomization.yaml │ └── kustomizeconfig.yaml ├── default │ ├── kustomization.yaml │ ├── manager_metrics_patch.yaml │ └── metrics_service.yaml ├── localresource │ ├── custom-namespace │ │ ├── backup-pvc.yaml │ │ ├── configMap.yaml │ │ ├── kubegres.yaml │ │ ├── kustomization.yaml │ │ ├── namespace.yaml │ │ └── secret.yaml │ └── default-namespace │ │ ├── backup-pvc.yaml │ │ ├── configMap.yaml │ │ ├── kubegres.yaml │ │ ├── kustomization.yaml │ │ └── secret.yaml ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── network-policy │ ├── allow-metrics-traffic.yaml │ └── kustomization.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── rbac │ ├── kubegres_editor_role.yaml │ ├── kubegres_viewer_role.yaml │ ├── kustomization.yaml │ ├── leader_election_role.yaml │ ├── leader_election_role_binding.yaml │ ├── metrics_auth_role.yaml │ ├── metrics_auth_role_binding.yaml │ ├── metrics_reader_role.yaml │ ├── role.yaml │ ├── role_binding.yaml │ └── service_account.yaml └── samples │ ├── kubegres_v1_kubegres.yaml │ └── kustomization.yaml ├── go.mod ├── go.sum ├── hack └── boilerplate.go.txt ├── internal ├── controller │ ├── ctx │ │ ├── CreateOwnerKeyIndexation.go │ │ ├── KubegresContext.go │ │ ├── log │ │ │ ├── InterfaceArrayUtil.go │ │ │ └── LogWrapper.go │ │ ├── resources │ │ │ └── ResourcesContext.go │ │ └── status │ │ │ └── KubegresStatusWrapper.go │ ├── kubegres_controller.go │ ├── operation │ │ ├── BlockingOperation.go │ │ ├── BlockingOperationConfig.go │ │ ├── BlockingOperationError.go │ │ └── log │ │ │ └── BlockingOperationLogger.go │ ├── spec │ │ ├── checker │ │ │ └── SpecChecker.go │ │ ├── defaultspec │ │ │ ├── DefaultStorageClass.go │ │ │ └── UndefinedSpecValuesChecker.go │ │ ├── enforcer │ │ │ ├── comparator │ │ │ │ └── PodSpecComparator.go │ │ │ ├── resources_count_spec │ │ │ │ ├── BackUpCronJobCountSpecEnforcer.go │ │ │ │ ├── BaseConfigMapCountSpecEnforcer.go │ │ │ │ ├── ResourcesCountSpecEnforcer.go │ │ │ │ ├── ServicesCountSpecEnforcer.go │ │ │ │ ├── StatefulSetCountSpecEnforcer.go │ │ │ │ └── statefulset │ │ │ │ │ ├── PrimaryDbCountSpecEnforcer.go │ │ │ │ │ ├── ReplicaDbCountSpecEnforcer.go │ │ │ │ │ └── failover │ │ │ │ │ └── PrimaryToReplicaFailOver.go │ │ │ └── statefulset_spec │ │ │ │ ├── AffinitySpecEnforcer.go │ │ │ │ ├── AllStatefulSetsSpecEnforcer.go │ │ │ │ ├── ContainerSecurityContextSpecEnforcer.go │ │ │ │ ├── CustomConfigSpecEnforcer.go │ │ │ │ ├── ImageSpecEnforcer.go │ │ │ │ ├── LivenessProbeSpecEnforcer.go │ │ │ │ ├── PortSpecEnforcer.go │ │ │ │ ├── ReadinessProbeSpecEnforcer.go │ │ │ │ ├── ResourcesSpecEnforcer.go │ │ │ │ ├── SecurityContextSpecEnforcer.go │ │ │ │ ├── ServiceAccountNameSpecEnforcer.go │ │ │ │ ├── StatefulSetSpecDifferences.go │ │ │ │ ├── StatefulSetsSpecEnforcer.go │ │ │ │ ├── StorageClassSizeSpecEnforcer.go │ │ │ │ ├── TolerationsSpecEnforcer.go │ │ │ │ └── VolumeSpecEnforcer.go │ │ └── template │ │ │ ├── CustomConfigSpecHelper.go │ │ │ ├── ResourceTemplateLoader.go │ │ │ ├── ResourcesCreatorFromTemplate.go │ │ │ └── yaml │ │ │ ├── BackUpCronJobTemplate.yaml │ │ │ ├── BaseConfigMapTemplate.yaml │ │ │ ├── PrimaryServiceTemplate.yaml │ │ │ ├── PrimaryStatefulSetTemplate.yaml │ │ │ ├── ReplicaServiceTemplate.yaml │ │ │ ├── ReplicaStatefulSetTemplate.yaml │ │ │ └── Templates.go │ └── states │ │ ├── BackUpStates.go │ │ ├── ConfigStates.go │ │ ├── DbStorageClassStates.go │ │ ├── ResourcesStates.go │ │ ├── ServicesStates.go │ │ ├── log │ │ └── ResourcesStatesLogger.go │ │ └── statefulset │ │ ├── PodsStates.go │ │ ├── StatefulSetWrapper.go │ │ ├── StatefulSetWrappersSorting.go │ │ └── StatefulSetsStates.go └── test │ ├── custom_annotation_test.go │ ├── custom_namespace_test.go │ ├── data_is_replicated_test.go │ ├── many_diff_kubegres_instances_test.go │ ├── postgres_conf_wal_level_logical_test.go │ ├── primary_failure_and_recovery_test.go │ ├── replica_failure_and_recovery_test.go │ ├── resourceConfigs │ ├── ConfigForTest.go │ ├── LoadTestYaml.go │ ├── backupPvc.yaml │ ├── customConfig │ │ ├── configMap_empty.yaml │ │ ├── configMap_with_all_configs.yaml │ │ ├── configMap_with_backup_database_script.yaml │ │ ├── configMap_with_pg_hba_conf.yaml │ │ ├── configMap_with_postgres_conf.yaml │ │ ├── configMap_with_postgres_conf_and_wal_level_to_logical.yaml │ │ └── configMap_with_primary_init_script.yaml │ ├── kubegres.yaml │ ├── primaryService.yaml │ ├── replicaService.yaml │ ├── secret.yaml │ └── serviceAccount.yaml │ ├── spec_affinity_test.go │ ├── spec_backup_test.go │ ├── spec_containerSecurityContext_test.go │ ├── spec_customConfig_test.go │ ├── spec_databaseSize_test.go │ ├── spec_databaseStorageClassName_test.go │ ├── spec_databaseVolumeMount_test.go │ ├── spec_envVariables_test.go │ ├── spec_failover_is_disabled_test.go │ ├── spec_image_test.go │ ├── spec_livenessProbe_test.go │ ├── spec_pod_manually_promoted_test.go │ ├── spec_port_test.go │ ├── spec_readinessProbe_test.go │ ├── spec_replica_test.go │ ├── spec_resource_test.go │ ├── spec_securityContext_test.go │ ├── spec_serviceAccountName_test.go │ ├── spec_tolerations_test.go │ ├── spec_volumeAndVolumeMount_test.go │ ├── spec_volumeClaimTemplates_test.go │ ├── suite_test.go │ └── util │ ├── DbConnectionTestUtil.go │ ├── MockEventRecorderTestUtil.go │ ├── MockLogger.go │ ├── TestResourceCreator.go │ ├── TestResourceModifier.go │ ├── TestResourceRetriever.go │ ├── kindcluster │ ├── KindTestClusterUtil.go │ ├── createCluster.sh │ └── kind-cluster-config.yaml │ └── testcases │ └── DbQueryTestCases.go └── kubegres.yaml /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | docker-build 2 | 3 | # Binaries for programs and plugins 4 | *.exe 5 | *.exe~ 6 | *.dll 7 | *.so 8 | *.dylib 9 | bin/* 10 | testbin/* 11 | Dockerfile.cross 12 | 13 | # Test binary, build with `go test -c` 14 | *.test 15 | 16 | # Output of the go coverage tool, specifically when used with LiteIDE 17 | *.out 18 | 19 | # Kubernetes Generated files - skip generated files, except for vendored files 20 | 21 | !vendor/**/zz_generated.* 22 | 23 | test-output.txt 24 | 25 | # editor and IDE paraphernalia 26 | .idea 27 | .vscode 28 | *.swp 29 | *.swo 30 | *~ 31 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | run: 2 | timeout: 5m 3 | allow-parallel-runners: true 4 | 5 | issues: 6 | # don't skip warning about doc comments 7 | # don't exclude the default set of lint 8 | exclude-use-default: false 9 | # restore some of the defaults 10 | # (fill in the rest as needed) 11 | exclude-rules: 12 | - path: "api/*" 13 | linters: 14 | - lll 15 | - path: "internal/*" 16 | linters: 17 | - dupl 18 | - lll 19 | linters: 20 | disable-all: true 21 | enable: 22 | - dupl 23 | - errcheck 24 | - copyloopvar 25 | - ginkgolinter 26 | - goconst 27 | - gocyclo 28 | - gofmt 29 | - goimports 30 | - gosimple 31 | - govet 32 | - ineffassign 33 | - lll 34 | - misspell 35 | - nakedret 36 | - prealloc 37 | - revive 38 | - staticcheck 39 | - typecheck 40 | - unconvert 41 | - unparam 42 | - unused 43 | 44 | linters-settings: 45 | revive: 46 | rules: 47 | - name: comment-spacings 48 | -------------------------------------------------------------------------------- /Backlog: -------------------------------------------------------------------------------- 1 | Features in order of priority: 2 | 3 | - being able to configure how sensitive is hte automatic recovery process 4 | by setting a time value in seconds 5 | 6 | - #12 : Reuse PVC (see below) and Primary becomes a Replica 7 | 8 | As part of the available options for the field "failover.pvc", there would be: 9 | - "retain": the default option currently with Kubegres where PVC are kept but not reused for safety and investigation reasons 10 | - "delete": the PVC will be deleted 11 | - "reuse": if the state of the PVC is healthy, it will be reused by the newly created Replica pod. I think that matches with your suggestion? 12 | 13 | - #20 : Delete PVC (is it the same as #12?) 14 | 15 | - add a test in spec_readinessProbe_test and spec_livenessProbe_test so that 16 | we test the default behaviour when no option is set from the YAML 17 | 18 | - #30: Add one or many sidecar container options in Kubegres YAML 19 | 20 | - #51: add documentation about how to recover backup 21 | - add use cases documentation, for example how to expand storage manually and how to upgrade Postgres major version. 22 | - check how to setup log archiving in case of replica does not found a data 23 | 24 | - #46: Define Service Type for Primary and Replica 25 | 26 | - #7 : Allow major version upgrade using pg_upgrade 27 | 28 | - #35 : Restore database from a PV backup 29 | 30 | - #82 : Allow using a unique image for the backup job 31 | 32 | - #10 : Deploy Kubegres with a HELM chart 33 | 34 | - #?: PG bouncer 35 | 36 | - #? : Add a field to allow restarting StatefulSets and Pods via the YAML of "Kind: Kubegres"? 37 | 38 | Blocked: 39 | #49 : Expand Storage (waiting on the Kubernetes feature: https://github.com/kubernetes/enhancements/pull/2842) 40 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.23 as builder 3 | ARG TARGETOS 4 | ARG TARGETARCH 5 | 6 | WORKDIR /workspace 7 | # Copy the Go Modules manifests 8 | COPY go.mod go.mod 9 | COPY go.sum go.sum 10 | # cache deps before building and copying source so that we don't need to re-download as much 11 | # and so that source changes don't invalidate our downloaded layer 12 | RUN go mod download 13 | 14 | # Copy the go source 15 | COPY cmd/main.go cmd/main.go 16 | COPY api/ api/ 17 | COPY internal/controller/ internal/controller/ 18 | 19 | # Build 20 | # the GOARCH has not a default value to allow the binary be built according to the host where the command 21 | # was called. For example, if we call make docker-build in a local env which has the Apple Silicon M1 SO 22 | # the docker BUILDPLATFORM arg will be linux/arm64 when for Apple x86 it will be linux/amd64. Therefore, 23 | # by leaving it empty we can ensure that the container and binary shipped on it will have the same platform. 24 | RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH} go build -a -o manager cmd/main.go 25 | 26 | # Use distroless as minimal base image to package the manager binary 27 | # Refer to https://github.com/GoogleContainerTools/distroless for more details 28 | FROM gcr.io/distroless/static:nonroot 29 | WORKDIR / 30 | COPY --from=builder /workspace/manager . 31 | USER 65532:65532 32 | 33 | ENTRYPOINT ["/manager"] 34 | -------------------------------------------------------------------------------- /PROJECT: -------------------------------------------------------------------------------- 1 | # Code generated by tool. DO NOT EDIT. 2 | # This file is used to track the info used to scaffold your project 3 | # and allow the plugins properly work. 4 | # More info: https://book.kubebuilder.io/reference/project-config.html 5 | domain: reactive-tech.io 6 | layout: 7 | - go.kubebuilder.io/v4 8 | projectName: kubegres 9 | repo: reactive-tech.io/kubegres 10 | resources: 11 | - api: 12 | crdVersion: v1 13 | namespaced: true 14 | controller: true 15 | domain: reactive-tech.io 16 | group: kubegres 17 | kind: Kubegres 18 | path: reactive-tech.io/kubegres/api/v1 19 | version: v1 20 | version: "3" 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [Kubegres](https://www.kubegres.io/) is a Kubernetes operator allowing to deploy one or many clusters of PostgreSql pods with data 2 | replication and failover enabled out-of-the box. It brings simplicity when using PostgreSql considering how complex managing 3 | stateful-set's life-cycle and data replication could be with Kubernetes. 4 | 5 | **Features** 6 | 7 | * It can manage one or many clusters of Postgres instances. 8 | Each cluster of Postgres instances is created using a YAML of "kind: Kubegres". Each cluster is self-contained and is 9 | identified by its unique name and namespace. 10 | 11 | * It creates a cluster of PostgreSql servers with [Streaming Replication](https://wiki.postgresql.org/wiki/Streaming_Replication) enabled: it creates a Primary PostgreSql pod and a 12 | number of Replica PostgreSql pods and replicates primary's database in real-time to Replica pods. 13 | 14 | * It manages fail-over: if a Primary PostgreSql crashes, it automatically promotes a Replica PostgreSql as a Primary. 15 | 16 | * It has a data backup option allowing to dump PostgreSql data regularly in a given volume. 17 | 18 | * It provides a very simple YAML with properties specialised for PostgreSql. 19 | 20 | * It is resilient, has over [99 automatized tests](https://github.com/reactive-tech/kubegres/tree/main/internal/test) cases and 21 | has been running in production. 22 | 23 | 24 | **How does Kubegres differentiate itself?** 25 | 26 | Kubegres is fully integrated with Kubernetes' lifecycle as it runs as an operator written in Go. 27 | It is minimalist in terms of codebase compared to other open-source Postgres operators. It has the minimal and 28 | yet robust required features to manage a cluster of PostgreSql on Kubernetes. We aim keeping this project small and simple. 29 | 30 | Among many reasons, there are [5 main ones why we recommend Kubegres](https://www.kubegres.io/#kubegres_compared). 31 | 32 | **Getting started** 33 | 34 | If you would like to install Kubegres, please read the page [Getting started](http://www.kubegres.io/doc/getting-started.html). 35 | 36 | **Sponsor** 37 | 38 | Kubegres is sponsored by [Etikbee](https://www.etikbee.com) 39 | which is using Kubegres in production with over 25 clusters of Postgres. 40 | Etikbee is a UK based marketplace which promotes reuse by allowing merchants 41 | to list their products for rent, for sale and advertise services such as product repair. 42 | 43 | **Contribute** 44 | 45 | If you would like to contribute to Kubegres, please read the page [How to contribute](http://www.kubegres.io/contribute/). 46 | 47 | **More details about the project** 48 | 49 | [Kubegres](https://www.kubegres.io/) was developed by [Reactive Tech Limited](https://www.reactive-tech.io/) and Alex 50 | Arica as the lead developer. Reactive Tech offers [support services](https://www.kubegres.io/support/) for Kubegres, 51 | Kubernetes and PostgreSql. 52 | 53 | It was developed with the framework [Kubebuilder](https://book.kubebuilder.io/) version 3, an SDK for building Kubernetes 54 | APIs using CRDs. Kubebuilder is maintained by the official Kubernetes API Machinery Special Interest Group (SIG). 55 | 56 | **Support** 57 | 58 | [Reactive Tech Limited](https://www.reactive-tech.io/) offers support for organisations using Kubegres. And we prioritise 59 | new features requested by organisations paying supports as long the new features would benefit the Open Source community. 60 | We start working on the implementation of new features within 24h of the request from organisations paying supports. 61 | More details in the [support page](https://www.kubegres.io/support/). 62 | 63 | **Interesting links** 64 | * A webinar about Kubegres was hosted by PostgresConf on 25 May 2021. [Watch the recorded video.](https://postgresconf.org/conferences/2021_Postgres_Conference_Webinars/program/proposals/creating-a-resilient-postgresql-cluster-with-kubegres) 65 | * The availability of Kubegres was published on [PostgreSql's official website](https://www.postgresql.org/about/news/kubegres-is-available-as-open-source-2197/). 66 | * Google talked about Kubegres in their [Kubernetes Podcast #146](https://kubernetespodcast.com/episode/146-kubernetes-1.21/). 67 | -------------------------------------------------------------------------------- /Upgrade-Kubebuilder: -------------------------------------------------------------------------------- 1 | kubebuilder init --domain reactive-tech.io --repo reactive-tech.io/kubegres 2 | kubebuilder create api --group kubegres --version v1 --kind Kubegres 3 | make manifests -------------------------------------------------------------------------------- /api/v1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | // Package v1 contains API Schema definitions for the kubegres v1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=kubegres.reactive-tech.io 20 | package v1 21 | 22 | import ( 23 | "k8s.io/apimachinery/pkg/runtime/schema" 24 | "sigs.k8s.io/controller-runtime/pkg/scheme" 25 | ) 26 | 27 | var ( 28 | // GroupVersion is group version used to register these objects 29 | GroupVersion = schema.GroupVersion{Group: "kubegres.reactive-tech.io", Version: "v1"} 30 | 31 | // SchemeBuilder is used to add go types to the GroupVersionKind scheme 32 | SchemeBuilder = &scheme.Builder{GroupVersion: GroupVersion} 33 | 34 | // AddToScheme adds the types in this group-version to the given scheme. 35 | AddToScheme = SchemeBuilder.AddToScheme 36 | ) 37 | -------------------------------------------------------------------------------- /api/v1/kubegres_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package v1 18 | 19 | import ( 20 | v1 "k8s.io/api/core/v1" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // ----------------------- SPEC ------------------------------------------- 25 | 26 | type KubegresDatabase struct { 27 | Size string `json:"size,omitempty"` 28 | VolumeMount string `json:"volumeMount,omitempty"` 29 | StorageClassName *string `json:"storageClassName,omitempty"` 30 | } 31 | 32 | type KubegresBackUp struct { 33 | Schedule string `json:"schedule,omitempty"` 34 | VolumeMount string `json:"volumeMount,omitempty"` 35 | PvcName string `json:"pvcName,omitempty"` 36 | } 37 | 38 | type KubegresFailover struct { 39 | IsDisabled bool `json:"isDisabled,omitempty"` 40 | PromotePod string `json:"promotePod,omitempty"` 41 | } 42 | 43 | type KubegresScheduler struct { 44 | Affinity *v1.Affinity `json:"affinity,omitempty"` 45 | Tolerations []v1.Toleration `json:"tolerations,omitempty"` 46 | } 47 | 48 | type VolumeClaimTemplate struct { 49 | Name string `json:"name,omitempty"` 50 | Spec v1.PersistentVolumeClaimSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"` 51 | } 52 | 53 | type Volume struct { 54 | VolumeMounts []v1.VolumeMount `json:"volumeMounts,omitempty"` 55 | Volumes []v1.Volume `json:"volumes,omitempty"` 56 | VolumeClaimTemplates []VolumeClaimTemplate `json:"volumeClaimTemplates,omitempty"` 57 | } 58 | 59 | type Probe struct { 60 | LivenessProbe *v1.Probe `json:"livenessProbe,omitempty"` 61 | ReadinessProbe *v1.Probe `json:"readinessProbe,omitempty"` 62 | } 63 | 64 | type KubegresSpec struct { 65 | Replicas *int32 `json:"replicas,omitempty"` 66 | Image string `json:"image,omitempty"` 67 | Port int32 `json:"port,omitempty"` 68 | ImagePullSecrets []v1.LocalObjectReference `json:"imagePullSecrets,omitempty"` 69 | CustomConfig string `json:"customConfig,omitempty"` 70 | Database KubegresDatabase `json:"database,omitempty"` 71 | Failover KubegresFailover `json:"failover,omitempty"` 72 | Backup KubegresBackUp `json:"backup,omitempty"` 73 | Env []v1.EnvVar `json:"env,omitempty"` 74 | Scheduler KubegresScheduler `json:"scheduler,omitempty"` 75 | Resources v1.ResourceRequirements `json:"resources,omitempty"` 76 | Volume Volume `json:"volume,omitempty"` 77 | SecurityContext *v1.PodSecurityContext `json:"securityContext,omitempty"` 78 | ContainerSecurityContext *v1.SecurityContext `json:"containerSecurityContext,omitempty"` 79 | Probe Probe `json:"probe,omitempty"` 80 | ServiceAccountName string `json:"serviceAccountName,omitempty"` 81 | } 82 | 83 | // ----------------------- STATUS ----------------------------------------- 84 | 85 | type KubegresStatefulSetOperation struct { 86 | InstanceIndex int32 `json:"instanceIndex,omitempty"` 87 | Name string `json:"name,omitempty"` 88 | } 89 | 90 | type KubegresStatefulSetSpecUpdateOperation struct { 91 | SpecDifferences string `json:"specDifferences,omitempty"` 92 | } 93 | 94 | type KubegresBlockingOperation struct { 95 | OperationId string `json:"operationId,omitempty"` 96 | StepId string `json:"stepId,omitempty"` 97 | TimeOutEpocInSeconds int64 `json:"timeOutEpocInSeconds,omitempty"` 98 | HasTimedOut bool `json:"hasTimedOut,omitempty"` 99 | 100 | // Custom operation fields 101 | StatefulSetOperation KubegresStatefulSetOperation `json:"statefulSetOperation,omitempty"` 102 | StatefulSetSpecUpdateOperation KubegresStatefulSetSpecUpdateOperation `json:"statefulSetSpecUpdateOperation,omitempty"` 103 | } 104 | 105 | type KubegresStatus struct { 106 | LastCreatedInstanceIndex int32 `json:"lastCreatedInstanceIndex,omitempty"` 107 | BlockingOperation KubegresBlockingOperation `json:"blockingOperation,omitempty"` 108 | PreviousBlockingOperation KubegresBlockingOperation `json:"previousBlockingOperation,omitempty"` 109 | EnforcedReplicas int32 `json:"enforcedReplicas,omitempty"` 110 | } 111 | 112 | // ----------------------- RESOURCE --------------------------------------- 113 | 114 | //+kubebuilder:object:root=true 115 | //+kubebuilder:subresource:status 116 | 117 | // Kubegres is the Schema for the kubegres API 118 | type Kubegres struct { 119 | metav1.TypeMeta `json:",inline"` 120 | metav1.ObjectMeta `json:"metadata,omitempty"` 121 | 122 | Spec KubegresSpec `json:"spec,omitempty"` 123 | Status KubegresStatus `json:"status,omitempty"` 124 | } 125 | 126 | //+kubebuilder:object:root=true 127 | 128 | // KubegresList contains a list of Kubegres 129 | type KubegresList struct { 130 | metav1.TypeMeta `json:",inline"` 131 | metav1.ListMeta `json:"metadata,omitempty"` 132 | Items []Kubegres `json:"items"` 133 | } 134 | 135 | func init() { 136 | SchemeBuilder.Register(&Kubegres{}, &KubegresList{}) 137 | } 138 | -------------------------------------------------------------------------------- /cmd/yamlcopy/WriteYamlTemplatesInGoFile.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | /* 22 | This script is executed every time this project is compiled. 23 | See cmd/main.go where there is a comment triggering this script: "//go:generate go run cmd/yamlcopy/WriteYamlTemplatesInGoFile.go" 24 | 25 | It copies each YAML file inside the package "internal/controller/template/yaml" 26 | and set them as constants in the file "internal/controller/template/yaml/Templates.go". 27 | This mechanism allows developers to work with YAML files when defining templates for resources managed by this operator. 28 | It is more convenient to work directly with YAML files than GO API classes when developing. 29 | */ 30 | 31 | package main 32 | 33 | import ( 34 | "fmt" 35 | "io" 36 | "os" 37 | "strings" 38 | ) 39 | 40 | const ( 41 | yamlTemplateDir = "../internal/controller/spec/template/yaml" 42 | destinationGoFile = yamlTemplateDir + "/Templates.go" 43 | ) 44 | 45 | func main() { 46 | 47 | entries, _ := os.ReadDir(yamlTemplateDir) 48 | out, _ := os.Create(destinationGoFile) 49 | 50 | out.Write([]byte("package yaml " + 51 | "\n\n // This file is auto generated by 'WriteYamlTemplatesInGoFile.go'. " + 52 | "\n // Any manual modification to this file will be lost during next compilation. " + 53 | "\n\nconst (\n")) 54 | 55 | fmt.Println("Setting constants in the file: '" + destinationGoFile + "', by copying the YAML contents of the following files:") 56 | 57 | for _, entry := range entries { 58 | fileInfo, _ := entry.Info() 59 | if strings.HasSuffix(fileInfo.Name(), ".yaml") { 60 | 61 | templateYamlFilePath := yamlTemplateDir + "/" + fileInfo.Name() 62 | fmt.Println("- '" + templateYamlFilePath + "'") 63 | 64 | out.Write([]byte(strings.TrimSuffix(fileInfo.Name(), ".yaml") + " = `")) 65 | f, _ := os.Open(templateYamlFilePath) 66 | io.Copy(out, f) 67 | out.Write([]byte("`\n")) 68 | } 69 | 70 | } 71 | out.Write([]byte(")\n")) 72 | } 73 | -------------------------------------------------------------------------------- /config/crd/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # This kustomization.yaml is not intended to be run by itself, 2 | # since it depends on service name and namespace that are out of this kustomize package. 3 | # It should be run by config/default 4 | resources: 5 | - bases/kubegres.reactive-tech.io_kubegres.yaml 6 | # +kubebuilder:scaffold:crdkustomizeresource 7 | 8 | patches: 9 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 10 | # patches here are for enabling the conversion webhook for each CRD 11 | # +kubebuilder:scaffold:crdkustomizewebhookpatch 12 | 13 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 14 | # patches here are for enabling the CA injection for each CRD 15 | # +kubebuilder:scaffold:crdkustomizecainjectionpatch 16 | 17 | # [WEBHOOK] To enable webhook, uncomment the following section 18 | # the following config is for teaching kustomize how to do kustomization for CRDs. 19 | #configurations: 20 | #- kustomizeconfig.yaml 21 | -------------------------------------------------------------------------------- /config/crd/kustomizeconfig.yaml: -------------------------------------------------------------------------------- 1 | # This file is for teaching kustomize how to substitute name and namespace reference in CRD 2 | nameReference: 3 | - kind: Service 4 | version: v1 5 | fieldSpecs: 6 | - kind: CustomResourceDefinition 7 | version: v1 8 | group: apiextensions.k8s.io 9 | path: spec/conversion/webhook/clientConfig/service/name 10 | 11 | namespace: 12 | - kind: CustomResourceDefinition 13 | version: v1 14 | group: apiextensions.k8s.io 15 | path: spec/conversion/webhook/clientConfig/service/namespace 16 | create: false 17 | 18 | varReference: 19 | - path: metadata/annotations 20 | -------------------------------------------------------------------------------- /config/default/manager_metrics_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch adds the args to allow exposing the metrics endpoint using HTTPS 2 | - op: add 3 | path: /spec/template/spec/containers/0/args/0 4 | value: --metrics-bind-address=:8443 5 | -------------------------------------------------------------------------------- /config/default/metrics_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: kubegres 7 | app.kubernetes.io/managed-by: kustomize 8 | name: controller-manager-metrics-service 9 | namespace: system 10 | spec: 11 | ports: 12 | - name: https 13 | port: 8443 14 | protocol: TCP 15 | targetPort: 8443 16 | selector: 17 | control-plane: controller-manager 18 | -------------------------------------------------------------------------------- /config/localresource/custom-namespace/backup-pvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: backup 5 | namespace: custom 6 | spec: 7 | storageClassName: "standard" 8 | accessModes: 9 | - ReadWriteOnce 10 | resources: 11 | requests: 12 | storage: 10Mi 13 | -------------------------------------------------------------------------------- /config/localresource/custom-namespace/configMap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: mypostgres-conf 5 | namespace: custom 6 | 7 | data: 8 | 9 | # This script is run once, the 1st time a Primary Kubegres container is created. 10 | # It is run in the Primary container only. 11 | # 12 | # Even if you do not use this script, please do not remove it because it is referenced in the StatefulSets. 13 | # 14 | # You can add in this script any instructions that you would like to run once 15 | # such as SQL statements to create specific database(s), user(s), grant accesses, etc... 16 | # See the commented example below. 17 | # 18 | # 19 | # This script will be located in the container folder "/docker-entrypoint-initdb.d" 20 | # That folder is referenced in the Postgres Docker page: https://hub.docker.com/_/postgres 21 | primary_init_script.sh: | 22 | #!/bin/bash 23 | set -e 24 | 25 | dt=$(date '+%d/%m/%Y %H:%M:%S'); 26 | echo "$dt - Running init script the 1st time Primary Kubegres container is created..."; 27 | 28 | customDatabaseName="my_app" 29 | customUserName="my_username" 30 | 31 | echo "$dt - Running: psql -v ON_ERROR_STOP=1 --username $POSTGRES_USER --dbname $POSTGRES_DB ..."; 32 | 33 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL 34 | CREATE USER $customUserName WITH PASSWORD '$POSTGRES_MYAPP_PASSWORD'; 35 | CREATE DATABASE $customDatabaseName; 36 | \connect $customDatabaseName; 37 | GRANT ALL ON SCHEMA public TO $customUserName; 38 | EOSQL 39 | 40 | psql -v ON_ERROR_STOP=1 --username "$customUserName" --dbname "$customDatabaseName" <<-EOSQL 41 | CREATE TABLE account(user_id serial PRIMARY KEY, username VARCHAR (50) NOT NULL); 42 | INSERT INTO account VALUES (1, 'username1'); 43 | INSERT INTO account VALUES (2, 'username2'); 44 | EOSQL 45 | 46 | echo "$dt - Init script is completed"; 47 | -------------------------------------------------------------------------------- /config/localresource/custom-namespace/kubegres.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kubegres.reactive-tech.io/v1 2 | kind: Kubegres 3 | metadata: 4 | name: mypostgres 5 | namespace: custom 6 | spec: 7 | 8 | replicas: 3 9 | image: postgres:17.2 10 | port: 5432 11 | 12 | #imagePullSecrets: 13 | # - name: my-private-docker-repo-secret 14 | 15 | database: 16 | size: 200Mi 17 | #storageClassName: standard 18 | #volumeMount: /var/lib/postgres/data 19 | 20 | customConfig: mypostgres-conf 21 | 22 | #failover: 23 | # isDisabled: true 24 | # promotePod: "mypostgres-3-0" 25 | 26 | #backup: 27 | # schedule: "*/1 * * * *" 28 | # pvcName: backup 29 | # volumeMount: /tmp/mypostgres 30 | 31 | #containerSecurityContext: 32 | # allowPrivilegeEscalation: false 33 | # capabilities: 34 | # drop: 35 | # - ALL 36 | # seccompProfile: 37 | # type: RuntimeDefault 38 | # runAsNonRoot: true 39 | # readOnlyRootFilesystem: false 40 | # privileged: false 41 | 42 | #securityContext: 43 | # runAsUser: 1001 44 | # runAsGroup: 1001 45 | # fsGroup: 1001 46 | # runAsNonRoot: true 47 | 48 | env: 49 | - name: POSTGRES_PASSWORD 50 | valueFrom: 51 | secretKeyRef: 52 | name: mypostgres-secret 53 | key: superUserPassword 54 | 55 | - name: POSTGRES_REPLICATION_PASSWORD 56 | valueFrom: 57 | secretKeyRef: 58 | name: mypostgres-secret 59 | key: replicationUserPassword 60 | 61 | - name: POSTGRES_MYAPP_PASSWORD 62 | valueFrom: 63 | secretKeyRef: 64 | name: mypostgres-secret 65 | key: myAppUserPassword 66 | -------------------------------------------------------------------------------- /config/localresource/custom-namespace/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - backup-pvc.yaml 6 | - namespace.yaml 7 | - secret.yaml 8 | - configMap.yaml 9 | - kubegres.yaml 10 | -------------------------------------------------------------------------------- /config/localresource/custom-namespace/namespace.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | name: custom 5 | #labels: 6 | # pod-security.kubernetes.io/enforce: restricted 7 | # pod-security.kubernetes.io/enforce-version: latest 8 | -------------------------------------------------------------------------------- /config/localresource/custom-namespace/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: mypostgres-secret 5 | namespace: custom 6 | type: Opaque 7 | stringData: 8 | superUserPassword: test 9 | replicationUserPassword: test 10 | myAppUserPassword: test 11 | -------------------------------------------------------------------------------- /config/localresource/default-namespace/backup-pvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: backup 5 | namespace: default 6 | spec: 7 | storageClassName: "standard" 8 | accessModes: 9 | - ReadWriteOnce 10 | resources: 11 | requests: 12 | storage: 10Mi 13 | -------------------------------------------------------------------------------- /config/localresource/default-namespace/configMap.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: mypostgres-conf 5 | namespace: default 6 | 7 | data: 8 | 9 | # This script is run once, the 1st time a Primary Kubegres container is created. 10 | # It is run in the Primary container only. 11 | # 12 | # Even if you do not use this script, please do not remove it because it is referenced in the StatefulSets. 13 | # 14 | # You can add in this script any instructions that you would like to run once 15 | # such as SQL statements to create specific database(s), user(s), grant accesses, etc... 16 | # See the commented example below. 17 | # 18 | # 19 | # This script will be located in the container folder "/docker-entrypoint-initdb.d" 20 | # That folder is referenced in the Postgres Docker page: https://hub.docker.com/_/postgres 21 | primary_init_script.sh: | 22 | #!/bin/bash 23 | set -e 24 | 25 | dt=$(date '+%d/%m/%Y %H:%M:%S'); 26 | echo "$dt - Running init script the 1st time Primary Kubegres container is created..."; 27 | 28 | customDatabaseName="my_app" 29 | customUserName="my_username" 30 | 31 | echo "$dt - Running: psql -v ON_ERROR_STOP=1 --username $POSTGRES_USER --dbname $POSTGRES_DB ..."; 32 | 33 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL 34 | CREATE USER $customUserName WITH PASSWORD '$POSTGRES_MYAPP_PASSWORD'; 35 | CREATE DATABASE $customDatabaseName; 36 | \connect $customDatabaseName; 37 | GRANT ALL ON SCHEMA public TO $customUserName; 38 | EOSQL 39 | 40 | psql -v ON_ERROR_STOP=1 --username "$customUserName" --dbname "$customDatabaseName" <<-EOSQL 41 | CREATE TABLE account(user_id serial PRIMARY KEY, username VARCHAR (50) NOT NULL); 42 | INSERT INTO account VALUES (1, 'username1'); 43 | INSERT INTO account VALUES (2, 'username2'); 44 | EOSQL 45 | 46 | echo "$dt - Init script is completed"; 47 | -------------------------------------------------------------------------------- /config/localresource/default-namespace/kubegres.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kubegres.reactive-tech.io/v1 2 | kind: Kubegres 3 | metadata: 4 | name: mypostgres 5 | namespace: default 6 | spec: 7 | 8 | replicas: 3 9 | image: postgres:17.2 10 | #port: 5432 11 | 12 | database: 13 | size: 200Mi 14 | #storageClassName: standard 15 | #volumeMount: /var/lib/postgres/data 16 | 17 | customConfig: mypostgres-conf 18 | 19 | #probe: 20 | # 21 | # livenessProbe: 22 | # exec: 23 | # command: 24 | # - sh 25 | # - -c 26 | # - exec pg_isready -U postgres -h $POD_IP 27 | # failureThreshold: 10 28 | # initialDelaySeconds: 60 29 | # periodSeconds: 20 30 | # successThreshold: 1 31 | # timeoutSeconds: 15 32 | 33 | # readinessProbe: 34 | # exec: 35 | # command: 36 | # - sh 37 | # - -c 38 | # - exec pg_isready -U postgres -h $POD_IP 39 | # failureThreshold: 3 40 | # initialDelaySeconds: 5 41 | # periodSeconds: 5 42 | # successThreshold: 1 43 | # timeoutSeconds: 3 44 | 45 | #securityContext: 46 | # runAsNonRoot: true 47 | # runAsUser: 999 48 | 49 | #failover: 50 | # isDisabled: true 51 | # promotePod: "mypostgres-3-0" 52 | 53 | #backup: 54 | # schedule: "*/5 * * * *" 55 | # pvcName: backup 56 | # volumeMount: /tmp/mypostgres 57 | 58 | #imagePullSecrets: 59 | # - name: my-private-docker-repo-secret 60 | # resources: 61 | # limits: 62 | # memory: "4Gi" 63 | # cpu: "1" 64 | # requests: 65 | # memory: "2Gi" 66 | # cpu: "1" 67 | 68 | #scheduler: 69 | # 70 | # affinity: 71 | # podAntiAffinity: 72 | # preferredDuringSchedulingIgnoredDuringExecution: 73 | # - weight: 90 74 | # podAffinityTerm: 75 | # labelSelector: 76 | # matchExpressions: 77 | # - key: app 78 | # operator: In 79 | # values: 80 | # - postgres-name 81 | # topologyKey: "kubernetes.io/hostname" 82 | # 83 | # tolerations: 84 | # - key: group 85 | # operator: Equal 86 | # value: critical 87 | 88 | # volume: 89 | # 90 | # volumeMounts: 91 | # - mountPath: /dev/shm 92 | # name: dshm 93 | # - mountPath: /dev/anything 94 | # name: anyMount 95 | # 96 | # volumes: 97 | # - name: dshm 98 | # emptyDir: 99 | # medium: Memory 100 | # sizeLimit: "600Mi" 101 | # 102 | # volumeClaimTemplates: 103 | # - name: anyMount 104 | # spec: 105 | # accessModes: [ "ReadWriteOnce" ] 106 | # storageClassName: standard 107 | # resources: 108 | # requests: 109 | # storage: 1Gi 110 | 111 | env: 112 | - name: PGPASSWORD 113 | valueFrom: 114 | secretKeyRef: 115 | name: mypostgres-secret 116 | key: superUserPassword 117 | 118 | - name: POSTGRES_PASSWORD 119 | valueFrom: 120 | secretKeyRef: 121 | name: mypostgres-secret 122 | key: superUserPassword 123 | 124 | - name: POSTGRES_REPLICATION_PASSWORD 125 | valueFrom: 126 | secretKeyRef: 127 | name: mypostgres-secret 128 | key: replicationUserPassword 129 | 130 | - name: POSTGRES_MYAPP_PASSWORD 131 | valueFrom: 132 | secretKeyRef: 133 | name: mypostgres-secret 134 | key: myAppUserPassword 135 | -------------------------------------------------------------------------------- /config/localresource/default-namespace/kustomization.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kustomize.config.k8s.io/v1beta1 2 | kind: Kustomization 3 | 4 | resources: 5 | - backup-pvc.yaml 6 | - secret.yaml 7 | - configMap.yaml 8 | - kubegres.yaml 9 | -------------------------------------------------------------------------------- /config/localresource/default-namespace/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: mypostgres-secret 5 | namespace: default 6 | type: Opaque 7 | stringData: 8 | superUserPassword: test 9 | replicationUserPassword: test 10 | myAppUserPassword: test 11 | -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | apiVersion: kustomize.config.k8s.io/v1beta1 4 | kind: Kustomization 5 | images: 6 | - name: controller 7 | newName: reactivetechio/kubegres 8 | newTag: "1.19" 9 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: kubegres 7 | app.kubernetes.io/managed-by: kustomize 8 | name: system 9 | --- 10 | apiVersion: apps/v1 11 | kind: Deployment 12 | metadata: 13 | name: controller-manager 14 | namespace: system 15 | labels: 16 | control-plane: controller-manager 17 | app.kubernetes.io/name: kubegres 18 | app.kubernetes.io/managed-by: kustomize 19 | spec: 20 | selector: 21 | matchLabels: 22 | control-plane: controller-manager 23 | replicas: 1 24 | template: 25 | metadata: 26 | annotations: 27 | kubectl.kubernetes.io/default-container: manager 28 | labels: 29 | control-plane: controller-manager 30 | spec: 31 | # TODO(user): Uncomment the following code to configure the nodeAffinity expression 32 | # according to the platforms which are supported by your solution. 33 | # It is considered best practice to support multiple architectures. You can 34 | # build your manager image using the makefile target docker-buildx. 35 | # affinity: 36 | # nodeAffinity: 37 | # requiredDuringSchedulingIgnoredDuringExecution: 38 | # nodeSelectorTerms: 39 | # - matchExpressions: 40 | # - key: kubernetes.io/arch 41 | # operator: In 42 | # values: 43 | # - amd64 44 | # - arm64 45 | # - ppc64le 46 | # - s390x 47 | # - key: kubernetes.io/os 48 | # operator: In 49 | # values: 50 | # - linux 51 | securityContext: 52 | runAsNonRoot: true 53 | # TODO(user): For common cases that do not require escalating privileges 54 | # it is recommended to ensure that all your Pods/Containers are restrictive. 55 | # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 56 | # Please uncomment the following code if your project does NOT have to work on old Kubernetes 57 | # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). 58 | # seccompProfile: 59 | # type: RuntimeDefault 60 | containers: 61 | - command: 62 | - /manager 63 | args: 64 | - --leader-elect 65 | - --health-probe-bind-address=:8081 66 | image: controller:latest 67 | name: manager 68 | securityContext: 69 | allowPrivilegeEscalation: false 70 | capabilities: 71 | drop: 72 | - "ALL" 73 | livenessProbe: 74 | httpGet: 75 | path: /healthz 76 | port: 8081 77 | initialDelaySeconds: 15 78 | periodSeconds: 20 79 | readinessProbe: 80 | httpGet: 81 | path: /readyz 82 | port: 8081 83 | initialDelaySeconds: 5 84 | periodSeconds: 10 85 | # TODO(user): Configure the resources accordingly based on the project requirements. 86 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 87 | resources: 88 | limits: 89 | cpu: 500m 90 | memory: 128Mi 91 | requests: 92 | cpu: 10m 93 | memory: 64Mi 94 | serviceAccountName: controller-manager 95 | terminationGracePeriodSeconds: 10 96 | -------------------------------------------------------------------------------- /config/network-policy/allow-metrics-traffic.yaml: -------------------------------------------------------------------------------- 1 | # This NetworkPolicy allows ingress traffic 2 | # with Pods running on namespaces labeled with 'metrics: enabled'. Only Pods on those 3 | # namespaces are able to gathering data from the metrics endpoint. 4 | apiVersion: networking.k8s.io/v1 5 | kind: NetworkPolicy 6 | metadata: 7 | labels: 8 | app.kubernetes.io/name: kubegres 9 | app.kubernetes.io/managed-by: kustomize 10 | name: allow-metrics-traffic 11 | namespace: system 12 | spec: 13 | podSelector: 14 | matchLabels: 15 | control-plane: controller-manager 16 | policyTypes: 17 | - Ingress 18 | ingress: 19 | # This allows ingress traffic from any namespace with the label metrics: enabled 20 | - from: 21 | - namespaceSelector: 22 | matchLabels: 23 | metrics: enabled # Only from namespaces with this label 24 | ports: 25 | - port: 8443 26 | protocol: TCP 27 | -------------------------------------------------------------------------------- /config/network-policy/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - allow-metrics-traffic.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | # Prometheus Monitor Service (Metrics) 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | labels: 6 | control-plane: controller-manager 7 | app.kubernetes.io/name: kubegres 8 | app.kubernetes.io/managed-by: kustomize 9 | name: controller-manager-metrics-monitor 10 | namespace: system 11 | spec: 12 | endpoints: 13 | - path: /metrics 14 | port: https # Ensure this is the name of the port that exposes HTTPS metrics 15 | scheme: https 16 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 17 | tlsConfig: 18 | # TODO(user): The option insecureSkipVerify: true is not recommended for production since it disables 19 | # certificate verification. This poses a significant security risk by making the system vulnerable to 20 | # man-in-the-middle attacks, where an attacker could intercept and manipulate the communication between 21 | # Prometheus and the monitored services. This could lead to unauthorized access to sensitive metrics data, 22 | # compromising the integrity and confidentiality of the information. 23 | # Please use the following options for secure configurations: 24 | # caFile: /etc/metrics-certs/ca.crt 25 | # certFile: /etc/metrics-certs/tls.crt 26 | # keyFile: /etc/metrics-certs/tls.key 27 | insecureSkipVerify: true 28 | selector: 29 | matchLabels: 30 | control-plane: controller-manager 31 | -------------------------------------------------------------------------------- /config/rbac/kubegres_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit kubegres. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: kubegres 7 | app.kubernetes.io/managed-by: kustomize 8 | name: kubegres-editor-role 9 | rules: 10 | - apiGroups: 11 | - kubegres.reactive-tech.io 12 | resources: 13 | - kubegres 14 | verbs: 15 | - create 16 | - delete 17 | - get 18 | - list 19 | - patch 20 | - update 21 | - watch 22 | - apiGroups: 23 | - kubegres.reactive-tech.io 24 | resources: 25 | - kubegres/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /config/rbac/kubegres_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view kubegres. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: kubegres 7 | app.kubernetes.io/managed-by: kustomize 8 | name: kubegres-viewer-role 9 | rules: 10 | - apiGroups: 11 | - kubegres.reactive-tech.io 12 | resources: 13 | - kubegres 14 | verbs: 15 | - get 16 | - list 17 | - watch 18 | - apiGroups: 19 | - kubegres.reactive-tech.io 20 | resources: 21 | - kubegres/status 22 | verbs: 23 | - get 24 | -------------------------------------------------------------------------------- /config/rbac/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | # All RBAC will be applied under this service account in 3 | # the deployment namespace. You may comment out this resource 4 | # if your manager will use a service account that exists at 5 | # runtime. Be sure to update RoleBinding and ClusterRoleBinding 6 | # subjects if changing service account names. 7 | - service_account.yaml 8 | - role.yaml 9 | - role_binding.yaml 10 | - leader_election_role.yaml 11 | - leader_election_role_binding.yaml 12 | # The following RBAC configurations are used to protect 13 | # the metrics endpoint with authn/authz. These configurations 14 | # ensure that only authorized users and service accounts 15 | # can access the metrics endpoint. Comment the following 16 | # permissions if you want to disable this protection. 17 | # More info: https://book.kubebuilder.io/reference/metrics.html 18 | - metrics_auth_role.yaml 19 | - metrics_auth_role_binding.yaml 20 | - metrics_reader_role.yaml 21 | # For each CRD, "Editor" and "Viewer" roles are scaffolded by 22 | # default, aiding admins in cluster management. Those roles are 23 | # not used by the Project itself. You can comment the following lines 24 | # if you do not want those helpers be installed with your Project. 25 | - kubegres_editor_role.yaml 26 | - kubegres_viewer_role.yaml 27 | 28 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions to do leader election. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: kubegres 7 | app.kubernetes.io/managed-by: kustomize 8 | name: leader-election-role 9 | rules: 10 | - apiGroups: 11 | - "" 12 | resources: 13 | - configmaps 14 | verbs: 15 | - get 16 | - list 17 | - watch 18 | - create 19 | - update 20 | - patch 21 | - delete 22 | - apiGroups: 23 | - coordination.k8s.io 24 | resources: 25 | - leases 26 | verbs: 27 | - get 28 | - list 29 | - watch 30 | - create 31 | - update 32 | - patch 33 | - delete 34 | - apiGroups: 35 | - "" 36 | resources: 37 | - events 38 | verbs: 39 | - create 40 | - patch 41 | -------------------------------------------------------------------------------- /config/rbac/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: RoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: kubegres 6 | app.kubernetes.io/managed-by: kustomize 7 | name: leader-election-rolebinding 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: Role 11 | name: leader-election-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: controller-manager 15 | namespace: system 16 | -------------------------------------------------------------------------------- /config/rbac/metrics_auth_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-auth-role 5 | rules: 6 | - apiGroups: 7 | - authentication.k8s.io 8 | resources: 9 | - tokenreviews 10 | verbs: 11 | - create 12 | - apiGroups: 13 | - authorization.k8s.io 14 | resources: 15 | - subjectaccessreviews 16 | verbs: 17 | - create 18 | -------------------------------------------------------------------------------- /config/rbac/metrics_auth_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | name: metrics-auth-rolebinding 5 | roleRef: 6 | apiGroup: rbac.authorization.k8s.io 7 | kind: ClusterRole 8 | name: metrics-auth-role 9 | subjects: 10 | - kind: ServiceAccount 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/rbac/metrics_reader_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | name: metrics-reader 5 | rules: 6 | - nonResourceURLs: 7 | - "/metrics" 8 | verbs: 9 | - get 10 | -------------------------------------------------------------------------------- /config/rbac/role.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: manager-role 6 | rules: 7 | - apiGroups: 8 | - "" 9 | resources: 10 | - configmaps 11 | - events 12 | - persistentvolumeclaims 13 | - pods 14 | - services 15 | verbs: 16 | - create 17 | - delete 18 | - get 19 | - list 20 | - patch 21 | - update 22 | - watch 23 | - apiGroups: 24 | - apps 25 | resources: 26 | - statefulsets 27 | verbs: 28 | - create 29 | - delete 30 | - get 31 | - list 32 | - patch 33 | - update 34 | - watch 35 | - apiGroups: 36 | - batch 37 | resources: 38 | - cronjobs 39 | verbs: 40 | - create 41 | - delete 42 | - get 43 | - list 44 | - patch 45 | - update 46 | - watch 47 | - apiGroups: 48 | - kubegres.reactive-tech.io 49 | resources: 50 | - kubegres 51 | verbs: 52 | - create 53 | - delete 54 | - get 55 | - list 56 | - patch 57 | - update 58 | - watch 59 | - apiGroups: 60 | - kubegres.reactive-tech.io 61 | resources: 62 | - kubegres/finalizers 63 | verbs: 64 | - update 65 | - apiGroups: 66 | - kubegres.reactive-tech.io 67 | resources: 68 | - kubegres/status 69 | verbs: 70 | - get 71 | - patch 72 | - update 73 | - apiGroups: 74 | - storage.k8s.io 75 | resources: 76 | - storageclasses 77 | verbs: 78 | - get 79 | - list 80 | - watch 81 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: kubegres 6 | app.kubernetes.io/managed-by: kustomize 7 | name: manager-rolebinding 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: ClusterRole 11 | name: manager-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: controller-manager 15 | namespace: system 16 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: kubegres 6 | app.kubernetes.io/managed-by: kustomize 7 | name: controller-manager 8 | namespace: system 9 | -------------------------------------------------------------------------------- /config/samples/kubegres_v1_kubegres.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kubegres.reactive-tech.io/v1 2 | kind: Kubegres 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: kubegres 6 | app.kubernetes.io/managed-by: kustomize 7 | name: kubegres-sample 8 | spec: 9 | # TODO(user): Add fields here 10 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples of your project ## 2 | resources: 3 | - kubegres_v1_kubegres.yaml 4 | # +kubebuilder:scaffold:manifestskustomizesamples 5 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module reactive-tech.io/kubegres 2 | 3 | go 1.23.4 4 | 5 | require ( 6 | github.com/go-logr/logr v1.4.2 7 | github.com/lib/pq v1.10.9 8 | github.com/onsi/ginkgo/v2 v2.22.2 9 | github.com/onsi/gomega v1.36.2 10 | k8s.io/api v0.31.0 11 | k8s.io/apimachinery v0.31.0 12 | k8s.io/client-go v0.31.0 13 | k8s.io/utils v0.0.0-20241210054802-24370beab758 14 | sigs.k8s.io/controller-runtime v0.19.3 15 | ) 16 | 17 | require ( 18 | github.com/antlr4-go/antlr/v4 v4.13.0 // indirect 19 | github.com/asaskevich/govalidator v0.0.0-20190424111038-f61b66f89f4a // indirect 20 | github.com/beorn7/perks v1.0.1 // indirect 21 | github.com/blang/semver/v4 v4.0.0 // indirect 22 | github.com/cenkalti/backoff/v4 v4.3.0 // indirect 23 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 24 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 25 | github.com/emicklei/go-restful/v3 v3.11.0 // indirect 26 | github.com/evanphx/json-patch/v5 v5.9.0 // indirect 27 | github.com/felixge/httpsnoop v1.0.4 // indirect 28 | github.com/fsnotify/fsnotify v1.7.0 // indirect 29 | github.com/fxamacker/cbor/v2 v2.7.0 // indirect 30 | github.com/go-logr/stdr v1.2.2 // indirect 31 | github.com/go-logr/zapr v1.3.0 // indirect 32 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 33 | github.com/go-openapi/jsonreference v0.20.2 // indirect 34 | github.com/go-openapi/swag v0.22.4 // indirect 35 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 36 | github.com/gogo/protobuf v1.3.2 // indirect 37 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 38 | github.com/golang/protobuf v1.5.4 // indirect 39 | github.com/google/cel-go v0.20.1 // indirect 40 | github.com/google/gnostic-models v0.6.8 // indirect 41 | github.com/google/go-cmp v0.6.0 // indirect 42 | github.com/google/gofuzz v1.2.0 // indirect 43 | github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect 44 | github.com/google/uuid v1.6.0 // indirect 45 | github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 // indirect 46 | github.com/imdario/mergo v0.3.6 // indirect 47 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 48 | github.com/josharian/intern v1.0.0 // indirect 49 | github.com/json-iterator/go v1.1.12 // indirect 50 | github.com/mailru/easyjson v0.7.7 // indirect 51 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 52 | github.com/modern-go/reflect2 v1.0.2 // indirect 53 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 54 | github.com/pkg/errors v0.9.1 // indirect 55 | github.com/prometheus/client_golang v1.19.1 // indirect 56 | github.com/prometheus/client_model v0.6.1 // indirect 57 | github.com/prometheus/common v0.55.0 // indirect 58 | github.com/prometheus/procfs v0.15.1 // indirect 59 | github.com/spf13/cobra v1.8.1 // indirect 60 | github.com/spf13/pflag v1.0.5 // indirect 61 | github.com/stoewer/go-strcase v1.2.0 // indirect 62 | github.com/x448/float16 v0.8.4 // indirect 63 | go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.53.0 // indirect 64 | go.opentelemetry.io/otel v1.28.0 // indirect 65 | go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.28.0 // indirect 66 | go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.27.0 // indirect 67 | go.opentelemetry.io/otel/metric v1.28.0 // indirect 68 | go.opentelemetry.io/otel/sdk v1.28.0 // indirect 69 | go.opentelemetry.io/otel/trace v1.28.0 // indirect 70 | go.opentelemetry.io/proto/otlp v1.3.1 // indirect 71 | go.uber.org/multierr v1.11.0 // indirect 72 | go.uber.org/zap v1.26.0 // indirect 73 | golang.org/x/exp v0.0.0-20230515195305-f3d0a9c9a5cc // indirect 74 | golang.org/x/net v0.33.0 // indirect 75 | golang.org/x/oauth2 v0.21.0 // indirect 76 | golang.org/x/sync v0.10.0 // indirect 77 | golang.org/x/sys v0.28.0 // indirect 78 | golang.org/x/term v0.27.0 // indirect 79 | golang.org/x/text v0.21.0 // indirect 80 | golang.org/x/time v0.3.0 // indirect 81 | golang.org/x/tools v0.28.0 // indirect 82 | gomodules.xyz/jsonpatch/v2 v2.4.0 // indirect 83 | google.golang.org/genproto/googleapis/api v0.0.0-20240528184218-531527333157 // indirect 84 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240701130421-f6361c86f094 // indirect 85 | google.golang.org/grpc v1.65.0 // indirect 86 | google.golang.org/protobuf v1.36.1 // indirect 87 | gopkg.in/inf.v0 v0.9.1 // indirect 88 | gopkg.in/yaml.v2 v2.4.0 // indirect 89 | gopkg.in/yaml.v3 v3.0.1 // indirect 90 | k8s.io/apiextensions-apiserver v0.31.0 // indirect 91 | k8s.io/apiserver v0.31.0 // indirect 92 | k8s.io/component-base v0.31.0 // indirect 93 | k8s.io/klog/v2 v2.130.1 // indirect 94 | k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 // indirect 95 | sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.30.3 // indirect 96 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 97 | sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect 98 | sigs.k8s.io/yaml v1.4.0 // indirect 99 | ) 100 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ -------------------------------------------------------------------------------- /internal/controller/ctx/CreateOwnerKeyIndexation.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package ctx 22 | 23 | import ( 24 | "context" 25 | apps "k8s.io/api/apps/v1" 26 | core "k8s.io/api/core/v1" 27 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 28 | postgresV1 "reactive-tech.io/kubegres/api/v1" 29 | ctrl "sigs.k8s.io/controller-runtime" 30 | "sigs.k8s.io/controller-runtime/pkg/client" 31 | ) 32 | 33 | func CreateOwnerKeyIndexation(mgr ctrl.Manager, 34 | ctx context.Context) error { 35 | 36 | if err := mgr.GetFieldIndexer().IndexField(ctx, &apps.StatefulSet{}, DeploymentOwnerKey, func(rawObj client.Object) []string { 37 | // grab the StatefultSet object, extract the owner... 38 | depl := rawObj.(*apps.StatefulSet) 39 | owner := metav1.GetControllerOf(depl) 40 | if owner == nil { 41 | return nil 42 | } 43 | // ...make sure it's a Kubegres... 44 | if owner.APIVersion != postgresV1.GroupVersion.String() || owner.Kind != KindKubegres { 45 | return nil 46 | } 47 | 48 | // ...and if so, return it 49 | return []string{owner.Name} 50 | }); err != nil { 51 | return err 52 | } 53 | 54 | if err := mgr.GetFieldIndexer().IndexField(ctx, &core.Service{}, DeploymentOwnerKey, func(rawObj client.Object) []string { 55 | // grab the Service object, extract the owner... 56 | depl := rawObj.(*core.Service) 57 | owner := metav1.GetControllerOf(depl) 58 | if owner == nil { 59 | return nil 60 | } 61 | // ...make sure it's a Kubegres... 62 | if owner.APIVersion != postgresV1.GroupVersion.String() || owner.Kind != KindKubegres { 63 | return nil 64 | } 65 | 66 | // ...and if so, return it 67 | return []string{owner.Name} 68 | }); err != nil { 69 | return err 70 | } 71 | 72 | if err := mgr.GetFieldIndexer().IndexField(ctx, &core.ConfigMap{}, DeploymentOwnerKey, func(rawObj client.Object) []string { 73 | // grab the CustomConfig object, extract the owner... 74 | depl := rawObj.(*core.ConfigMap) 75 | owner := metav1.GetControllerOf(depl) 76 | if owner == nil { 77 | return nil 78 | } 79 | // ...make sure it's a Kubegres... 80 | if owner.APIVersion != postgresV1.GroupVersion.String() || owner.Kind != KindKubegres { 81 | return nil 82 | } 83 | 84 | // ...and if so, return it 85 | return []string{owner.Name} 86 | }); err != nil { 87 | return err 88 | } 89 | 90 | if err := mgr.GetFieldIndexer().IndexField(ctx, &core.Pod{}, DeploymentOwnerKey, func(rawObj client.Object) []string { 91 | // grab the Pod object, extract the owner... 92 | depl := rawObj.(*core.Pod) 93 | owner := metav1.GetControllerOf(depl) 94 | if owner == nil { 95 | return nil 96 | } 97 | // ...make sure it's a Kubegres... 98 | if owner.APIVersion != postgresV1.GroupVersion.String() || owner.Kind != KindKubegres { 99 | return nil 100 | } 101 | 102 | // ...and if so, return it 103 | return []string{owner.Name} 104 | }); err != nil { 105 | return err 106 | } 107 | 108 | return nil 109 | } 110 | -------------------------------------------------------------------------------- /internal/controller/ctx/KubegresContext.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package ctx 22 | 23 | import ( 24 | "context" 25 | "strconv" 26 | "strings" 27 | 28 | "reactive-tech.io/kubegres/api/v1" 29 | "reactive-tech.io/kubegres/internal/controller/ctx/log" 30 | "reactive-tech.io/kubegres/internal/controller/ctx/status" 31 | "sigs.k8s.io/controller-runtime/pkg/client" 32 | ) 33 | 34 | type KubegresContext struct { 35 | Kubegres *v1.Kubegres 36 | Status *status.KubegresStatusWrapper 37 | Ctx context.Context 38 | Log log.LogWrapper 39 | Client client.Client 40 | } 41 | 42 | const ( 43 | PrimaryRoleName = "primary" 44 | KindKubegres = "Kubegres" 45 | DeploymentOwnerKey = ".metadata.controller" 46 | DatabaseVolumeName = "postgres-db" 47 | BaseConfigMapVolumeName = "base-config" 48 | CustomConfigMapVolumeName = "custom-config" 49 | BaseConfigMapName = "base-kubegres-config" 50 | CronJobNamePrefix = "backup-" 51 | DefaultContainerPortNumber = 5432 52 | DefaultPodServiceAccountName = "default" 53 | DefaultDatabaseVolumeMount = "/var/lib/postgresql/data" 54 | DefaultDatabaseFolder = "pgdata" 55 | EnvVarNamePgData = "PGDATA" 56 | EnvVarNameOfPostgresSuperUserPsw = "POSTGRES_PASSWORD" 57 | EnvVarNameOfPostgresReplicationUserPsw = "POSTGRES_REPLICATION_PASSWORD" 58 | ) 59 | 60 | func (r *KubegresContext) GetServiceResourceName(isPrimary bool) string { 61 | if isPrimary { 62 | return r.Kubegres.Name 63 | } 64 | return r.Kubegres.Name + "-replica" 65 | } 66 | 67 | func (r *KubegresContext) GetStatefulSetResourceName(instanceIndex int32) string { 68 | return r.Kubegres.Name + "-" + strconv.Itoa(int(instanceIndex)) 69 | } 70 | 71 | func (r *KubegresContext) IsReservedVolumeName(volumeName string) bool { 72 | return volumeName == DatabaseVolumeName || 73 | volumeName == BaseConfigMapVolumeName || 74 | volumeName == CustomConfigMapVolumeName || 75 | strings.Contains(volumeName, "kube-api") 76 | } 77 | -------------------------------------------------------------------------------- /internal/controller/ctx/log/InterfaceArrayUtil.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package log 22 | 23 | import ( 24 | "fmt" 25 | "reflect" 26 | ) 27 | 28 | func InterfacesToStr(keysAndValues ...interface{}) string { 29 | 30 | if len(keysAndValues) == 0 || (len(keysAndValues) == 1 && reflect.ValueOf(keysAndValues[0]).IsNil()) { 31 | return "" 32 | } 33 | return keyValuesPairsToStr(keysAndValues...) 34 | } 35 | 36 | func keyValuesPairsToStr(keysAndValues ...interface{}) string { 37 | keysAndValuesStr := "" 38 | comma := "" 39 | for _, keyAndValue := range keysAndValues { 40 | keysAndValuesStr += comma + interfaceToStr(keyAndValue) 41 | comma = ", " 42 | } 43 | return keysAndValuesStr 44 | } 45 | 46 | func interfaceToStr(keyAndValue interface{}) string { 47 | 48 | keysAndValuesSlice := reflect.ValueOf(keyAndValue) 49 | if keysAndValuesSlice.Kind() != reflect.Slice { 50 | return "" 51 | } 52 | 53 | keysAndValuesStr := "" 54 | comma := "" 55 | keysAndValuesLength := keysAndValuesSlice.Len() 56 | 57 | for i := 0; i < keysAndValuesLength; i += 2 { 58 | key := keysAndValuesSlice.Index(i).Interface() 59 | 60 | var value interface{} 61 | if (i + 1) < keysAndValuesLength { 62 | value = keysAndValuesSlice.Index(i + 1).Interface() 63 | } 64 | 65 | keysAndValuesStr += comma + fmt.Sprintf("'%s'", key) + ": " + fmt.Sprintf("%v", value) 66 | comma = ", " 67 | } 68 | 69 | return keysAndValuesStr 70 | } 71 | -------------------------------------------------------------------------------- /internal/controller/ctx/log/LogWrapper.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package log 22 | 23 | import ( 24 | "github.com/go-logr/logr" 25 | v1 "k8s.io/api/core/v1" 26 | "k8s.io/client-go/tools/record" 27 | postgresV1 "reactive-tech.io/kubegres/api/v1" 28 | ) 29 | 30 | type LogWrapper struct { 31 | Kubegres *postgresV1.Kubegres 32 | Logger logr.Logger 33 | Recorder record.EventRecorder 34 | } 35 | 36 | func (r *LogWrapper) WithValues(keysAndValues ...interface{}) { 37 | r.Logger = r.Logger.WithValues(keysAndValues...) 38 | } 39 | 40 | func (r *LogWrapper) WithName(name string) { 41 | r.Logger = r.Logger.WithName(name) 42 | } 43 | 44 | func (r *LogWrapper) Info(msg string, keysAndValues ...interface{}) { 45 | r.Logger.Info(msg, keysAndValues...) 46 | } 47 | 48 | func (r *LogWrapper) Error(err error, msg string, keysAndValues ...interface{}) { 49 | r.Logger.Error(err, msg, keysAndValues...) 50 | } 51 | 52 | func (r *LogWrapper) Warning(msg string, keysAndValues ...interface{}) { 53 | r.Logger.Info("Warning: "+msg, keysAndValues...) 54 | } 55 | 56 | func (r *LogWrapper) InfoEvent(eventReason string, msg string, keysAndValues ...interface{}) { 57 | r.Info(msg, keysAndValues...) 58 | r.Recorder.Eventf(r.Kubegres, v1.EventTypeNormal, eventReason, r.constructFullMsg(msg, keysAndValues)) 59 | } 60 | 61 | func (r *LogWrapper) ErrorEvent(eventReason string, err error, msg string, keysAndValues ...interface{}) { 62 | r.Error(err, msg, keysAndValues...) 63 | r.Recorder.Eventf(r.Kubegres, v1.EventTypeWarning, eventReason, r.constructFullErrMsg(err, msg, keysAndValues)) 64 | } 65 | 66 | func (r *LogWrapper) WarningEvent(eventReason string, msg string, keysAndValues ...interface{}) { 67 | r.Warning(msg, keysAndValues...) 68 | r.Recorder.Eventf(r.Kubegres, v1.EventTypeWarning, eventReason, r.constructFullMsg(msg, keysAndValues)) 69 | } 70 | 71 | func (r *LogWrapper) constructFullMsg(msg string, keysAndValues ...interface{}) string { 72 | if msg == "" { 73 | return "" 74 | } 75 | 76 | keysAndValuesStr := InterfacesToStr(keysAndValues...) 77 | if keysAndValuesStr != "" { 78 | return msg + " " + keysAndValuesStr 79 | } 80 | return msg 81 | } 82 | 83 | func (r *LogWrapper) constructFullErrMsg(err error, msg string, keysAndValues ...interface{}) string { 84 | msgToReturn := "" 85 | separator := "" 86 | 87 | customErrMsg := r.constructFullMsg(msg, keysAndValues...) 88 | if customErrMsg != "" { 89 | msgToReturn = customErrMsg 90 | separator = " - " 91 | } 92 | 93 | msgFromErr := err.Error() 94 | if msgFromErr != "" { 95 | msgToReturn += separator + msgFromErr 96 | } 97 | 98 | return msgToReturn 99 | } 100 | -------------------------------------------------------------------------------- /internal/controller/ctx/status/KubegresStatusWrapper.go: -------------------------------------------------------------------------------- 1 | package status 2 | 3 | import ( 4 | "context" 5 | v1 "reactive-tech.io/kubegres/api/v1" 6 | "reactive-tech.io/kubegres/internal/controller/ctx/log" 7 | "sigs.k8s.io/controller-runtime/pkg/client" 8 | ) 9 | 10 | type KubegresStatusWrapper struct { 11 | Kubegres *v1.Kubegres 12 | Ctx context.Context 13 | Log log.LogWrapper 14 | Client client.Client 15 | statusFieldsToUpdate map[string]interface{} 16 | } 17 | 18 | func (r *KubegresStatusWrapper) GetLastCreatedInstanceIndex() int32 { 19 | return r.Kubegres.Status.LastCreatedInstanceIndex 20 | } 21 | 22 | func (r *KubegresStatusWrapper) SetLastCreatedInstanceIndex(value int32) { 23 | r.addStatusFieldToUpdate("LastCreatedInstanceIndex", value) 24 | r.Kubegres.Status.LastCreatedInstanceIndex = value 25 | } 26 | 27 | func (r *KubegresStatusWrapper) GetBlockingOperation() v1.KubegresBlockingOperation { 28 | return r.Kubegres.Status.BlockingOperation 29 | } 30 | 31 | func (r *KubegresStatusWrapper) SetBlockingOperation(value v1.KubegresBlockingOperation) { 32 | r.addStatusFieldToUpdate("BlockingOperation", value) 33 | r.Kubegres.Status.BlockingOperation = value 34 | } 35 | 36 | func (r *KubegresStatusWrapper) GetEnforcedReplicas() int32 { 37 | return r.Kubegres.Status.EnforcedReplicas 38 | } 39 | 40 | func (r *KubegresStatusWrapper) SetEnforcedReplicas(value int32) { 41 | r.addStatusFieldToUpdate("EnforcedReplicas", value) 42 | r.Kubegres.Status.EnforcedReplicas = value 43 | } 44 | 45 | func (r *KubegresStatusWrapper) GetPreviousBlockingOperation() v1.KubegresBlockingOperation { 46 | return r.Kubegres.Status.PreviousBlockingOperation 47 | } 48 | 49 | func (r *KubegresStatusWrapper) SetPreviousBlockingOperation(value v1.KubegresBlockingOperation) { 50 | r.addStatusFieldToUpdate("PreviousBlockingOperation", value) 51 | r.Kubegres.Status.PreviousBlockingOperation = value 52 | } 53 | 54 | func (r *KubegresStatusWrapper) UpdateStatusIfChanged() error { 55 | if r.statusFieldsToUpdate == nil { 56 | return nil 57 | } 58 | 59 | for statusFieldName, statusFieldValue := range r.statusFieldsToUpdate { 60 | r.Log.Info("Updating Kubegres' status: ", 61 | "Field", statusFieldName, "New value", statusFieldValue) 62 | 63 | } 64 | 65 | err := r.Client.Status().Update(r.Ctx, r.Kubegres) 66 | 67 | if err != nil { 68 | r.Log.Error(err, "Failed to update Kubegres status") 69 | 70 | } else { 71 | r.Log.Info("Kubegres status updated.") 72 | } 73 | 74 | return err 75 | } 76 | 77 | func (r *KubegresStatusWrapper) addStatusFieldToUpdate(statusFieldName string, newValue interface{}) { 78 | 79 | if r.statusFieldsToUpdate == nil { 80 | r.statusFieldsToUpdate = make(map[string]interface{}) 81 | } 82 | 83 | r.statusFieldsToUpdate[statusFieldName] = newValue 84 | } 85 | -------------------------------------------------------------------------------- /internal/controller/operation/BlockingOperationConfig.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | import v1 "reactive-tech.io/kubegres/api/v1" 4 | 5 | type IsOperationCompleted func(operation v1.KubegresBlockingOperation) bool 6 | 7 | type BlockingOperationConfig struct { 8 | OperationId string 9 | StepId string 10 | TimeOutInSeconds int64 11 | CompletionChecker IsOperationCompleted 12 | 13 | // This flag is set to false by default, meaning once an operation is completed (operation = operationId + stepId), 14 | // it will be automatically removed as active operation and added as previous active operation. 15 | // 16 | // If this flag is set to false and the operation times-out, it is not removed and remain as an active operation 17 | // until it is manually removed. 18 | // 19 | // If this flag is set to true, after the completion of a stepId, we will add a new stepId and keep the same operationId. 20 | // The new stepId will be set to the value of the constant "BlockingOperation.transition_step_id". 21 | // This logic allows other types of operations (e.g. FailOver) to not start until an active operation (e.g. Spec update) 22 | // is either terminated manually or is waiting for the next step to start. 23 | AfterCompletionMoveToTransitionStep bool 24 | } 25 | 26 | const ( 27 | TransitionOperationStepId = "Transition step: waiting either for the next step to start or for the operation to be removed ..." 28 | 29 | OperationIdBaseConfigCountSpecEnforcement = "Base config count spec enforcement" 30 | OperationStepIdBaseConfigDeploying = "Base config is deploying" 31 | 32 | OperationIdPrimaryDbCountSpecEnforcement = "Primary DB count spec enforcement" 33 | OperationStepIdPrimaryDbDeploying = "Primary DB is deploying" 34 | OperationStepIdPrimaryDbWaitingBeforeFailingOver = "Waiting few seconds before failing over by promoting a Replica DB as a Primary DB" 35 | OperationStepIdPrimaryDbFailingOver = "Failing over by promoting a Replica DB as a Primary DB" 36 | 37 | OperationIdReplicaDbCountSpecEnforcement = "Replica DB count spec enforcement" 38 | OperationStepIdReplicaDbDeploying = "Replica DB is deploying" 39 | OperationStepIdReplicaDbUndeploying = "Replica DB is undeploying" 40 | 41 | OperationIdStatefulSetSpecEnforcing = "Enforcing StatefulSet's Spec" 42 | OperationStepIdStatefulSetSpecUpdating = "StatefulSet's spec is updating" 43 | OperationStepIdStatefulSetPodSpecUpdating = "StatefulSet Pod's spec is updating" 44 | OperationStepIdStatefulSetWaitingOnStuckPod = "Attempting to fix a stuck Pod by recreating it" 45 | ) 46 | -------------------------------------------------------------------------------- /internal/controller/operation/BlockingOperationError.go: -------------------------------------------------------------------------------- 1 | package operation 2 | 3 | const ( 4 | errorTypeThereIsAlreadyAnActiveOperation = "There is already an active operation which is running. We cannot have more than 1 active operation running." 5 | errorTypeOperationIdHasNoAssociatedConfig = "The given operationId has not an associated config. Please associate it by calling the method BlockingOperation.AddConfig()." 6 | ) 7 | 8 | type BlockingOperationError struct { 9 | ThereIsAlreadyAnActiveOperation bool 10 | OperationIdHasNoAssociatedConfig bool 11 | 12 | errorType string 13 | } 14 | 15 | func CreateBlockingOperationError(errorType, operationId string) *BlockingOperationError { 16 | return &BlockingOperationError{ 17 | errorType: "OperationId: '" + operationId + "' - " + errorType, 18 | ThereIsAlreadyAnActiveOperation: errorType == errorTypeThereIsAlreadyAnActiveOperation, 19 | OperationIdHasNoAssociatedConfig: errorType == errorTypeOperationIdHasNoAssociatedConfig, 20 | } 21 | } 22 | 23 | func (r *BlockingOperationError) Error() string { 24 | return "Cannot active a blocking operation. Reason: " + r.errorType 25 | } 26 | -------------------------------------------------------------------------------- /internal/controller/operation/log/BlockingOperationLogger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | v1 "reactive-tech.io/kubegres/api/v1" 5 | "reactive-tech.io/kubegres/internal/controller/ctx" 6 | "reactive-tech.io/kubegres/internal/controller/operation" 7 | ) 8 | 9 | type BlockingOperationLogger struct { 10 | kubegresContext ctx.KubegresContext 11 | blockingOperation *operation.BlockingOperation 12 | } 13 | 14 | func CreateBlockingOperationLogger(kubegresContext ctx.KubegresContext, blockingOperation *operation.BlockingOperation) BlockingOperationLogger { 15 | return BlockingOperationLogger{kubegresContext: kubegresContext, blockingOperation: blockingOperation} 16 | } 17 | 18 | func (r *BlockingOperationLogger) Log() { 19 | r.logActiveOperation() 20 | r.logPreviouslyActiveOperation() 21 | } 22 | 23 | func (r *BlockingOperationLogger) logActiveOperation() { 24 | 25 | activeOperation := r.blockingOperation.GetActiveOperation() 26 | activeOperationId := activeOperation.OperationId 27 | nbreSecondsLeftBeforeTimeOut := r.blockingOperation.GetNbreSecondsLeftBeforeTimeOut() 28 | 29 | keyAndValuesToLog := r.logOperation(activeOperation) 30 | keyAndValuesToLog = append(keyAndValuesToLog, "NbreSecondsLeftBeforeTimeOut", nbreSecondsLeftBeforeTimeOut) 31 | 32 | if activeOperationId == "" { 33 | r.kubegresContext.Log.Info("Active Blocking-Operation: None") 34 | } else { 35 | r.kubegresContext.Log.Info("Active Blocking-Operation ", keyAndValuesToLog...) 36 | } 37 | } 38 | 39 | func (r *BlockingOperationLogger) logPreviouslyActiveOperation() { 40 | 41 | previousActiveOperation := r.blockingOperation.GetPreviouslyActiveOperation() 42 | operationId := previousActiveOperation.OperationId 43 | 44 | keyAndValuesToLog := r.logOperation(previousActiveOperation) 45 | 46 | if operationId == "" { 47 | r.kubegresContext.Log.Info("Previous Blocking-Operation: None") 48 | } else { 49 | r.kubegresContext.Log.Info("Previous Blocking-Operation ", keyAndValuesToLog...) 50 | } 51 | } 52 | 53 | func (r *BlockingOperationLogger) logOperation(operation v1.KubegresBlockingOperation) []interface{} { 54 | 55 | operationId := operation.OperationId 56 | stepId := operation.StepId 57 | hasTimedOut := operation.HasTimedOut 58 | statefulSetInstanceIndex := operation.StatefulSetOperation.InstanceIndex 59 | statefulSetSpecDifferences := operation.StatefulSetSpecUpdateOperation.SpecDifferences 60 | 61 | var keysAndValues []interface{} 62 | keysAndValues = append(keysAndValues, "OperationId", operationId) 63 | keysAndValues = append(keysAndValues, "StepId", stepId) 64 | keysAndValues = append(keysAndValues, "HasTimedOut", hasTimedOut) 65 | 66 | if statefulSetInstanceIndex != 0 { 67 | keysAndValues = append(keysAndValues, "StatefulSetInstanceIndex", statefulSetInstanceIndex) 68 | } 69 | 70 | if statefulSetSpecDifferences != "" { 71 | keysAndValues = append(keysAndValues, "StatefulSetSpecDifferences", statefulSetSpecDifferences) 72 | } 73 | 74 | return keysAndValues 75 | } 76 | -------------------------------------------------------------------------------- /internal/controller/spec/defaultspec/DefaultStorageClass.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package defaultspec 22 | 23 | import ( 24 | "errors" 25 | storage "k8s.io/api/storage/v1" 26 | apierrors "k8s.io/apimachinery/pkg/api/errors" 27 | "reactive-tech.io/kubegres/internal/controller/ctx" 28 | ) 29 | 30 | type DefaultStorageClass struct { 31 | kubegresContext ctx.KubegresContext 32 | } 33 | 34 | func CreateDefaultStorageClass(kubegresContext ctx.KubegresContext) DefaultStorageClass { 35 | return DefaultStorageClass{kubegresContext} 36 | } 37 | 38 | func (r *DefaultStorageClass) GetDefaultStorageClassName() (string, error) { 39 | 40 | list := &storage.StorageClassList{} 41 | err := r.kubegresContext.Client.List(r.kubegresContext.Ctx, list) 42 | 43 | if err != nil { 44 | if apierrors.IsNotFound(err) { 45 | err = nil 46 | } else { 47 | r.kubegresContext.Log.ErrorEvent("DefaultStorageClassNameErr", err, "Unable to retrieve the name of the default storage class in the Kubernetes cluster.", "Namespace", r.kubegresContext.Kubegres.Namespace) 48 | } 49 | } 50 | 51 | for _, storageClass := range list.Items { 52 | if storageClass.Annotations["storageclass.kubernetes.io/is-default-class"] == "true" { 53 | return storageClass.Name, nil 54 | } 55 | } 56 | 57 | err = errors.New("Unable to retrieve the name of the default storage class in the Kubernetes cluster. Namespace: " + r.kubegresContext.Kubegres.Namespace) 58 | r.kubegresContext.Log.ErrorEvent("DefaultStorageClassNameErr", err, "Unable to retrieve the name of the default storage class in the Kubernetes cluster.", "Namespace", r.kubegresContext.Kubegres.Namespace) 59 | 60 | return "", err 61 | } 62 | -------------------------------------------------------------------------------- /internal/controller/spec/defaultspec/UndefinedSpecValuesChecker.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package defaultspec 22 | 23 | import ( 24 | core "k8s.io/api/core/v1" 25 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 26 | "reactive-tech.io/kubegres/internal/controller/ctx" 27 | "strconv" 28 | ) 29 | 30 | type UndefinedSpecValuesChecker struct { 31 | kubegresContext ctx.KubegresContext 32 | defaultStorageClass DefaultStorageClass 33 | } 34 | 35 | func SetDefaultForUndefinedSpecValues(kubegresContext ctx.KubegresContext, defaultStorageClass DefaultStorageClass) error { 36 | defaultSpec := UndefinedSpecValuesChecker{ 37 | kubegresContext: kubegresContext, 38 | defaultStorageClass: defaultStorageClass, 39 | } 40 | 41 | return defaultSpec.apply() 42 | } 43 | 44 | func (r *UndefinedSpecValuesChecker) apply() error { 45 | 46 | wasSpecChanged := false 47 | kubegresSpec := &r.kubegresContext.Kubegres.Spec 48 | const emptyStr = "" 49 | 50 | if kubegresSpec.Port <= 0 { 51 | wasSpecChanged = true 52 | kubegresSpec.Port = ctx.DefaultContainerPortNumber 53 | r.createLog("spec.port", strconv.Itoa(int(kubegresSpec.Port))) 54 | } 55 | 56 | if kubegresSpec.Database.VolumeMount == emptyStr { 57 | wasSpecChanged = true 58 | kubegresSpec.Database.VolumeMount = ctx.DefaultDatabaseVolumeMount 59 | r.createLog("spec.database.volumeMount", kubegresSpec.Database.VolumeMount) 60 | } 61 | 62 | if kubegresSpec.CustomConfig == emptyStr { 63 | wasSpecChanged = true 64 | kubegresSpec.CustomConfig = ctx.BaseConfigMapName 65 | r.createLog("spec.customConfig", kubegresSpec.CustomConfig) 66 | } 67 | 68 | if r.isStorageClassNameUndefinedInSpec() { 69 | wasSpecChanged = true 70 | defaultStorageClassName, err := r.defaultStorageClass.GetDefaultStorageClassName() 71 | if err != nil { 72 | return err 73 | } 74 | 75 | kubegresSpec.Database.StorageClassName = &defaultStorageClassName 76 | r.createLog("spec.Database.StorageClassName", defaultStorageClassName) 77 | } 78 | 79 | if kubegresSpec.Scheduler.Affinity == nil { 80 | kubegresSpec.Scheduler.Affinity = r.createDefaultAffinity() 81 | wasSpecChanged = true 82 | r.createLog("spec.Affinity", kubegresSpec.Scheduler.Affinity.String()) 83 | } 84 | 85 | if wasSpecChanged { 86 | return r.updateSpec() 87 | } 88 | 89 | return nil 90 | } 91 | 92 | func (r *UndefinedSpecValuesChecker) createLog(specName string, specValue string) { 93 | r.kubegresContext.Log.InfoEvent("DefaultSpecValue", "A default value was set for a field in Kubegres YAML spec.", specName, "New value: "+specValue+"") 94 | } 95 | 96 | func (r *UndefinedSpecValuesChecker) isStorageClassNameUndefinedInSpec() bool { 97 | storageClassName := r.kubegresContext.Kubegres.Spec.Database.StorageClassName 98 | return storageClassName == nil || *storageClassName == "" 99 | } 100 | 101 | func (r *UndefinedSpecValuesChecker) updateSpec() error { 102 | r.kubegresContext.Log.Info("Updating Kubegres Spec", "name", r.kubegresContext.Kubegres.Name) 103 | return r.kubegresContext.Client.Update(r.kubegresContext.Ctx, r.kubegresContext.Kubegres) 104 | } 105 | 106 | func (r *UndefinedSpecValuesChecker) createDefaultAffinity() *core.Affinity { 107 | 108 | resourceName := r.kubegresContext.Kubegres.Name 109 | 110 | weightedPodAffinityTerm := core.WeightedPodAffinityTerm{ 111 | Weight: 100, 112 | PodAffinityTerm: core.PodAffinityTerm{ 113 | LabelSelector: &metav1.LabelSelector{ 114 | MatchExpressions: []metav1.LabelSelectorRequirement{ 115 | { 116 | Key: "app", 117 | Operator: metav1.LabelSelectorOpIn, 118 | Values: []string{resourceName}, 119 | }, 120 | }, 121 | }, 122 | TopologyKey: "kubernetes.io/hostname", 123 | }, 124 | } 125 | 126 | return &core.Affinity{ 127 | PodAntiAffinity: &core.PodAntiAffinity{ 128 | PreferredDuringSchedulingIgnoredDuringExecution: []core.WeightedPodAffinityTerm{weightedPodAffinityTerm}, 129 | }, 130 | } 131 | } 132 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/comparator/PodSpecComparator.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package comparator 22 | 23 | import ( 24 | core "k8s.io/api/core/v1" 25 | postgresV1 "reactive-tech.io/kubegres/api/v1" 26 | ) 27 | 28 | type PodSpecComparator struct { 29 | Pod core.Pod 30 | PostgresSpec postgresV1.KubegresSpec 31 | } 32 | 33 | func (r *PodSpecComparator) IsSpecUpToDate() bool { 34 | return r.isImageUpToDate() && 35 | r.isPortUpToDate() 36 | } 37 | 38 | func (r *PodSpecComparator) isImageUpToDate() bool { 39 | 40 | if len(r.Pod.Spec.Containers) == 0 { 41 | return false 42 | } 43 | 44 | current := r.Pod.Spec.Containers[0].Image 45 | expected := r.PostgresSpec.Image 46 | return current == expected 47 | } 48 | 49 | func (r *PodSpecComparator) isPortUpToDate() bool { 50 | 51 | if len(r.Pod.Spec.Containers) == 0 { 52 | return false 53 | } 54 | 55 | current := r.Pod.Spec.Containers[0].Ports[0].ContainerPort 56 | expected := r.PostgresSpec.Port 57 | return current == expected 58 | } 59 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/resources_count_spec/BackUpCronJobCountSpecEnforcer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package resources_count_spec 22 | 23 | import ( 24 | batch "k8s.io/api/batch/v1" 25 | "reactive-tech.io/kubegres/internal/controller/ctx" 26 | "reactive-tech.io/kubegres/internal/controller/spec/template" 27 | states2 "reactive-tech.io/kubegres/internal/controller/states" 28 | ) 29 | 30 | type BackUpCronJobCountSpecEnforcer struct { 31 | kubegresContext ctx.KubegresContext 32 | resourcesStates states2.ResourcesStates 33 | resourcesCreator template.ResourcesCreatorFromTemplate 34 | } 35 | 36 | func CreateBackUpCronJobCountSpecEnforcer(kubegresContext ctx.KubegresContext, 37 | resourcesStates states2.ResourcesStates, 38 | resourcesCreator template.ResourcesCreatorFromTemplate) BackUpCronJobCountSpecEnforcer { 39 | 40 | return BackUpCronJobCountSpecEnforcer{ 41 | kubegresContext: kubegresContext, 42 | resourcesStates: resourcesStates, 43 | resourcesCreator: resourcesCreator, 44 | } 45 | } 46 | 47 | func (r *BackUpCronJobCountSpecEnforcer) EnforceSpec() error { 48 | 49 | if r.isCronJobDeployed() { 50 | 51 | if !r.hasSpecChanged() { 52 | return nil 53 | } 54 | 55 | err := r.deleteCronJob() 56 | if err != nil { 57 | return err 58 | } 59 | } 60 | 61 | if !r.isBackUpConfigured() { 62 | return nil 63 | } 64 | 65 | configMapNameForBackUp := r.getConfigMapNameForBackUp(r.resourcesStates.Config) 66 | cronJob, err := r.resourcesCreator.CreateBackUpCronJob(configMapNameForBackUp) 67 | if err != nil { 68 | r.kubegresContext.Log.ErrorEvent("BackUpCronJobTemplateErr", err, "Unable to create a BackUp CronJob object from template.") 69 | return err 70 | } 71 | 72 | return r.deployCronJob(cronJob) 73 | } 74 | 75 | func (r *BackUpCronJobCountSpecEnforcer) getConfigMapNameForBackUp(configStates states2.ConfigStates) string { 76 | if configStates.ConfigLocations.BackUpScript == ctx.BaseConfigMapVolumeName { 77 | return configStates.BaseConfigName 78 | } 79 | return configStates.CustomConfigName 80 | } 81 | 82 | func (r *BackUpCronJobCountSpecEnforcer) deployCronJob(cronJob batch.CronJob) error { 83 | 84 | r.kubegresContext.Log.Info("Deploying BackUp CronJob.", "CronJob name", cronJob.Name) 85 | 86 | if err := r.kubegresContext.Client.Create(r.kubegresContext.Ctx, &cronJob); err != nil { 87 | r.kubegresContext.Log.ErrorEvent("BackUpCronJobDeploymentErr", err, "Unable to deploy BackUp CronJob.", "CronJob name", cronJob.Name) 88 | return err 89 | } 90 | 91 | r.kubegresContext.Log.InfoEvent("BackUpCronJobDeployment", "Deployed BackUp CronJob.", "CronJob name", cronJob.Name) 92 | return nil 93 | } 94 | 95 | func (r *BackUpCronJobCountSpecEnforcer) isBackUpConfigured() bool { 96 | return r.kubegresContext.Kubegres.Spec.Backup.Schedule != "" 97 | } 98 | 99 | func (r *BackUpCronJobCountSpecEnforcer) isCronJobDeployed() bool { 100 | return r.resourcesStates.BackUp.IsCronJobDeployed 101 | } 102 | 103 | func (r *BackUpCronJobCountSpecEnforcer) hasSpecChanged() (hasSpecChanged bool) { 104 | 105 | cronJob := r.resourcesStates.BackUp.DeployedCronJob 106 | cronJobSpec := &cronJob.Spec 107 | cronJobTemplateSpec := cronJob.Spec.JobTemplate.Spec.Template.Spec 108 | kubegresBackUpSpec := r.kubegresContext.Kubegres.Spec.Backup 109 | 110 | currentSchedule := cronJobSpec.Schedule 111 | expectedSchedule := kubegresBackUpSpec.Schedule 112 | if currentSchedule != expectedSchedule { 113 | hasSpecChanged = true 114 | r.logSpecChange("spec.backup.schedule") 115 | } 116 | 117 | currentVolumeMount := cronJobTemplateSpec.Containers[0].VolumeMounts[0].MountPath 118 | expectedVolumeMount := kubegresBackUpSpec.VolumeMount 119 | if currentVolumeMount != expectedVolumeMount { 120 | hasSpecChanged = true 121 | r.logSpecChange("spec.backup.volumeMount") 122 | } 123 | 124 | currentPvcName := cronJobTemplateSpec.Volumes[0].PersistentVolumeClaim.ClaimName 125 | expectedPvcName := kubegresBackUpSpec.PvcName 126 | if currentPvcName != expectedPvcName { 127 | hasSpecChanged = true 128 | r.logSpecChange("spec.backup.pvcName") 129 | } 130 | 131 | currentCustomConfig := cronJobTemplateSpec.Volumes[1].ConfigMap.Name 132 | expectedCustomConfig := r.getConfigMapNameForBackUp(r.resourcesStates.Config) 133 | if currentCustomConfig != expectedCustomConfig { 134 | hasSpecChanged = true 135 | r.logSpecChange("spec.backup.customConfig") 136 | } 137 | 138 | return hasSpecChanged 139 | } 140 | 141 | func (r *BackUpCronJobCountSpecEnforcer) deleteCronJob() error { 142 | 143 | err := r.kubegresContext.Client.Delete(r.kubegresContext.Ctx, r.resourcesStates.BackUp.DeployedCronJob) 144 | if err != nil { 145 | r.kubegresContext.Log.ErrorEvent("PodSpecEnforcementErr", err, 146 | "Unable to delete a Backup CronJob which had its spec changed. "+ 147 | "The aim of the deletion is to trigger a re-creation of the CronJob.", 148 | "CronJob name:", r.resourcesStates.BackUp.DeployedCronJob.Name) 149 | return err 150 | } 151 | 152 | r.kubegresContext.Log.InfoEvent("BackUpCronJobDeletion", "Deleted BackUp CronJob.", "CronJob name", r.resourcesStates.BackUp.DeployedCronJob.Name) 153 | return nil 154 | } 155 | 156 | func (r *BackUpCronJobCountSpecEnforcer) logSpecChange(specName string) { 157 | r.kubegresContext.Log.Info("BackUp spec '"+specName+"' has changed. "+ 158 | "We will delete Backup CronJob resource so that it gets re-created by Kubegres "+ 159 | "and the spec change will be applied on creation.", 160 | "CronJob name:", r.resourcesStates.BackUp.DeployedCronJob.Name) 161 | } 162 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/resources_count_spec/BaseConfigMapCountSpecEnforcer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package resources_count_spec 22 | 23 | import ( 24 | "errors" 25 | core "k8s.io/api/core/v1" 26 | v1 "reactive-tech.io/kubegres/api/v1" 27 | "reactive-tech.io/kubegres/internal/controller/ctx" 28 | operation2 "reactive-tech.io/kubegres/internal/controller/operation" 29 | "reactive-tech.io/kubegres/internal/controller/spec/template" 30 | "reactive-tech.io/kubegres/internal/controller/states" 31 | "strconv" 32 | ) 33 | 34 | type BaseConfigMapCountSpecEnforcer struct { 35 | kubegresContext ctx.KubegresContext 36 | resourcesStates states.ResourcesStates 37 | resourcesCreator template.ResourcesCreatorFromTemplate 38 | blockingOperation *operation2.BlockingOperation 39 | } 40 | 41 | func CreateBaseConfigMapCountSpecEnforcer(kubegresContext ctx.KubegresContext, 42 | resourcesStates states.ResourcesStates, 43 | resourcesCreator template.ResourcesCreatorFromTemplate, 44 | blockingOperation *operation2.BlockingOperation) BaseConfigMapCountSpecEnforcer { 45 | 46 | return BaseConfigMapCountSpecEnforcer{ 47 | kubegresContext: kubegresContext, 48 | resourcesStates: resourcesStates, 49 | resourcesCreator: resourcesCreator, 50 | blockingOperation: blockingOperation, 51 | } 52 | } 53 | 54 | func (r *BaseConfigMapCountSpecEnforcer) CreateOperationConfig() operation2.BlockingOperationConfig { 55 | 56 | return operation2.BlockingOperationConfig{ 57 | OperationId: operation2.OperationIdBaseConfigCountSpecEnforcement, 58 | StepId: operation2.OperationStepIdBaseConfigDeploying, 59 | TimeOutInSeconds: 10, 60 | CompletionChecker: func(operation v1.KubegresBlockingOperation) bool { return r.isBaseConfigDeployed() }, 61 | } 62 | } 63 | 64 | func (r *BaseConfigMapCountSpecEnforcer) EnforceSpec() error { 65 | 66 | if r.blockingOperation.IsActiveOperationIdDifferentOf(operation2.OperationIdBaseConfigCountSpecEnforcement) { 67 | return nil 68 | } 69 | 70 | if r.hasLastAttemptTimedOut() { 71 | 72 | if r.isBaseConfigDeployed() { 73 | r.blockingOperation.RemoveActiveOperation() 74 | r.logKubegresFeaturesAreReEnabled() 75 | 76 | } else { 77 | r.logTimedOut() 78 | return nil 79 | } 80 | } 81 | 82 | if r.isBaseConfigDeployed() { 83 | return nil 84 | } 85 | 86 | baseConfigMap, err := r.resourcesCreator.CreateBaseConfigMap() 87 | if err != nil { 88 | r.kubegresContext.Log.ErrorEvent("BaseConfigMapTemplateErr", err, 89 | "Unable to create a Base ConfigMap object from template.", 90 | "Based ConfigMap name", ctx.BaseConfigMapName) 91 | return err 92 | } 93 | 94 | return r.deployBaseConfigMap(baseConfigMap) 95 | } 96 | 97 | func (r *BaseConfigMapCountSpecEnforcer) isBaseConfigDeployed() bool { 98 | return r.resourcesStates.Config.IsBaseConfigDeployed 99 | } 100 | 101 | func (r *BaseConfigMapCountSpecEnforcer) hasLastAttemptTimedOut() bool { 102 | return r.blockingOperation.HasActiveOperationIdTimedOut(operation2.OperationIdBaseConfigCountSpecEnforcement) 103 | } 104 | 105 | func (r *BaseConfigMapCountSpecEnforcer) logKubegresFeaturesAreReEnabled() { 106 | r.kubegresContext.Log.InfoEvent("KubegresReEnabled", "Base ConfigMap is available again. "+ 107 | "We can safely re-enable all features of Kubegres.") 108 | } 109 | 110 | func (r *BaseConfigMapCountSpecEnforcer) logTimedOut() { 111 | 112 | operationTimeOutStr := strconv.FormatInt(r.CreateOperationConfig().TimeOutInSeconds, 10) 113 | 114 | err := errors.New("Base ConfigMap deployment timed-out") 115 | r.kubegresContext.Log.ErrorEvent("BaseConfigMapDeploymentTimedOutErr", err, 116 | "Last Base ConfigMap deployment attempt has timed-out after "+operationTimeOutStr+" seconds. "+ 117 | "The new Base ConfigMap is still NOT ready. It must be fixed manually. "+ 118 | "Until Base ConfigMap is ready, most of the features of Kubegres are disabled for safety reason. ", 119 | "Based ConfigMap to fix", ctx.BaseConfigMapName) 120 | } 121 | 122 | func (r *BaseConfigMapCountSpecEnforcer) deployBaseConfigMap(configMap core.ConfigMap) error { 123 | 124 | r.kubegresContext.Log.Info("Deploying Base ConfigMap", "name", configMap.Name) 125 | 126 | if err := r.activateBlockingOperationForDeployment(); err != nil { 127 | r.kubegresContext.Log.ErrorEvent("BaseConfigMapDeploymentOperationActivationErr", err, 128 | "Error while activating blocking operation for the deployment of a Base ConfigMap.", 129 | "ConfigMap name", configMap.Name) 130 | return err 131 | } 132 | 133 | if err := r.kubegresContext.Client.Create(r.kubegresContext.Ctx, &configMap); err != nil { 134 | r.kubegresContext.Log.ErrorEvent("BaseConfigMapDeploymentErr", err, 135 | "Unable to deploy Base ConfigMap.", 136 | "ConfigMap name", configMap.Name) 137 | r.blockingOperation.RemoveActiveOperation() 138 | return err 139 | } 140 | 141 | r.kubegresContext.Log.InfoEvent("BaseConfigMapDeployment", "Deployed Base ConfigMap.", 142 | "ConfigMap name", configMap.Name) 143 | return nil 144 | } 145 | 146 | func (r *BaseConfigMapCountSpecEnforcer) activateBlockingOperationForDeployment() error { 147 | return r.blockingOperation.ActivateOperation(operation2.OperationIdBaseConfigCountSpecEnforcement, 148 | operation2.OperationStepIdBaseConfigDeploying) 149 | } 150 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/resources_count_spec/ResourcesCountSpecEnforcer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package resources_count_spec 22 | 23 | type ResourceCountSpecEnforcer interface { 24 | EnforceSpec() error 25 | } 26 | 27 | type ResourcesCountSpecEnforcer struct { 28 | registry []ResourceCountSpecEnforcer 29 | } 30 | 31 | func (r *ResourcesCountSpecEnforcer) AddSpecEnforcer(specEnforcer ResourceCountSpecEnforcer) { 32 | r.registry = append(r.registry, specEnforcer) 33 | } 34 | 35 | func (r *ResourcesCountSpecEnforcer) EnforceSpec() error { 36 | for _, specEnforcer := range r.registry { 37 | if err := specEnforcer.EnforceSpec(); err != nil { 38 | return err 39 | } 40 | } 41 | return nil 42 | } 43 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/resources_count_spec/ServicesCountSpecEnforcer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package resources_count_spec 22 | 23 | import ( 24 | core "k8s.io/api/core/v1" 25 | "reactive-tech.io/kubegres/internal/controller/ctx" 26 | "reactive-tech.io/kubegres/internal/controller/spec/template" 27 | "reactive-tech.io/kubegres/internal/controller/states" 28 | ) 29 | 30 | type ServicesCountSpecEnforcer struct { 31 | kubegresContext ctx.KubegresContext 32 | resourcesStates states.ResourcesStates 33 | resourcesCreator template.ResourcesCreatorFromTemplate 34 | } 35 | 36 | func CreateServicesCountSpecEnforcer(kubegresContext ctx.KubegresContext, 37 | resourcesStates states.ResourcesStates, 38 | resourcesCreator template.ResourcesCreatorFromTemplate) ServicesCountSpecEnforcer { 39 | 40 | return ServicesCountSpecEnforcer{ 41 | kubegresContext: kubegresContext, 42 | resourcesStates: resourcesStates, 43 | resourcesCreator: resourcesCreator, 44 | } 45 | } 46 | 47 | func (r *ServicesCountSpecEnforcer) EnforceSpec() error { 48 | 49 | if !r.isPrimaryServiceDeployed() && r.isPrimaryDbReady() { 50 | err := r.deployPrimaryService() 51 | if err != nil { 52 | return err 53 | } 54 | } 55 | 56 | if !r.isReplicaServiceDeployed() && r.isThereReadyReplica() { 57 | err := r.deployReplicaService() 58 | if err != nil { 59 | return err 60 | } 61 | } 62 | 63 | return nil 64 | } 65 | 66 | func (r *ServicesCountSpecEnforcer) isPrimaryServiceDeployed() bool { 67 | return r.resourcesStates.Services.Primary.IsDeployed 68 | } 69 | 70 | func (r *ServicesCountSpecEnforcer) isReplicaServiceDeployed() bool { 71 | return r.resourcesStates.Services.Replica.IsDeployed 72 | } 73 | 74 | func (r *ServicesCountSpecEnforcer) isPrimaryDbReady() bool { 75 | return r.resourcesStates.StatefulSets.Primary.IsReady 76 | } 77 | 78 | func (r *ServicesCountSpecEnforcer) isThereReadyReplica() bool { 79 | return r.resourcesStates.StatefulSets.Replicas.NbreReady > 0 80 | } 81 | 82 | func (r *ServicesCountSpecEnforcer) deployPrimaryService() error { 83 | return r.deployService(true) 84 | } 85 | 86 | func (r *ServicesCountSpecEnforcer) deployReplicaService() error { 87 | return r.deployService(false) 88 | } 89 | 90 | func (r *ServicesCountSpecEnforcer) deployService(isPrimary bool) error { 91 | 92 | primaryOrReplicaTxt := r.createLogLabel(isPrimary) 93 | 94 | service, err := r.createServiceResource(isPrimary) 95 | if err != nil { 96 | r.kubegresContext.Log.ErrorEvent("ServiceTemplateErr", err, "Unable to create "+primaryOrReplicaTxt+" Service object from template.") 97 | return err 98 | } 99 | 100 | if err := r.kubegresContext.Client.Create(r.kubegresContext.Ctx, &service); err != nil { 101 | r.kubegresContext.Log.ErrorEvent("ServiceDeploymentErr", err, "Unable to deploy "+primaryOrReplicaTxt+" Service.", "Service name", service.Name) 102 | return err 103 | } 104 | 105 | r.kubegresContext.Log.InfoEvent("ServiceDeployment", "Deployed "+primaryOrReplicaTxt+" Service.", "Service name", service.Name) 106 | return nil 107 | } 108 | 109 | func (r *ServicesCountSpecEnforcer) createLogLabel(isPrimary bool) string { 110 | if isPrimary { 111 | return "Primary" 112 | } else { 113 | return "Replica" 114 | } 115 | } 116 | 117 | func (r *ServicesCountSpecEnforcer) createServiceResource(isPrimary bool) (core.Service, error) { 118 | if isPrimary { 119 | return r.resourcesCreator.CreatePrimaryService() 120 | } else { 121 | return r.resourcesCreator.CreateReplicaService() 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/resources_count_spec/StatefulSetCountSpecEnforcer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package resources_count_spec 22 | 23 | import ( 24 | statefulset2 "reactive-tech.io/kubegres/internal/controller/spec/enforcer/resources_count_spec/statefulset" 25 | ) 26 | 27 | type StatefulSetCountSpecEnforcer struct { 28 | primaryDbCountSpecEnforcer statefulset2.PrimaryDbCountSpecEnforcer 29 | replicaDbCountSpecEnforcer statefulset2.ReplicaDbCountSpecEnforcer 30 | } 31 | 32 | func CreateStatefulSetCountSpecEnforcer(primaryDbCountSpecEnforcer statefulset2.PrimaryDbCountSpecEnforcer, 33 | replicaDbCountSpecEnforcer statefulset2.ReplicaDbCountSpecEnforcer) StatefulSetCountSpecEnforcer { 34 | 35 | return StatefulSetCountSpecEnforcer{ 36 | primaryDbCountSpecEnforcer: primaryDbCountSpecEnforcer, 37 | replicaDbCountSpecEnforcer: replicaDbCountSpecEnforcer, 38 | } 39 | } 40 | 41 | func (r *StatefulSetCountSpecEnforcer) EnforceSpec() error { 42 | 43 | if err := r.enforcePrimaryDbInstance(); err != nil { 44 | return err 45 | } 46 | return r.enforceReplicaDbInstances() 47 | } 48 | 49 | func (r *StatefulSetCountSpecEnforcer) enforcePrimaryDbInstance() error { 50 | return r.primaryDbCountSpecEnforcer.Enforce() 51 | } 52 | 53 | func (r *StatefulSetCountSpecEnforcer) enforceReplicaDbInstances() error { 54 | return r.replicaDbCountSpecEnforcer.Enforce() 55 | } 56 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/statefulset_spec/AffinitySpecEnforcer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package statefulset_spec 22 | 23 | import ( 24 | apps "k8s.io/api/apps/v1" 25 | "reactive-tech.io/kubegres/internal/controller/ctx" 26 | "reflect" 27 | ) 28 | 29 | type AffinitySpecEnforcer struct { 30 | kubegresContext ctx.KubegresContext 31 | } 32 | 33 | func CreateAffinitySpecEnforcer(kubegresContext ctx.KubegresContext) AffinitySpecEnforcer { 34 | return AffinitySpecEnforcer{kubegresContext: kubegresContext} 35 | } 36 | 37 | func (r *AffinitySpecEnforcer) GetSpecName() string { 38 | return "Affinity" 39 | } 40 | 41 | func (r *AffinitySpecEnforcer) CheckForSpecDifference(statefulSet *apps.StatefulSet) StatefulSetSpecDifference { 42 | 43 | current := statefulSet.Spec.Template.Spec.Affinity 44 | expected := r.kubegresContext.Kubegres.Spec.Scheduler.Affinity 45 | 46 | if !reflect.DeepEqual(current, expected) { 47 | return StatefulSetSpecDifference{ 48 | SpecName: r.GetSpecName(), 49 | Current: current.String(), 50 | Expected: expected.String(), 51 | } 52 | } 53 | 54 | return StatefulSetSpecDifference{} 55 | } 56 | 57 | func (r *AffinitySpecEnforcer) EnforceSpec(statefulSet *apps.StatefulSet) (wasSpecUpdated bool, err error) { 58 | statefulSet.Spec.Template.Spec.Affinity = r.kubegresContext.Kubegres.Spec.Scheduler.Affinity 59 | return true, nil 60 | } 61 | 62 | func (r *AffinitySpecEnforcer) OnSpecEnforcedSuccessfully(statefulSet *apps.StatefulSet) error { 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/statefulset_spec/ContainerSecurityContextSpecEnforcer.go: -------------------------------------------------------------------------------- 1 | package statefulset_spec 2 | 3 | import ( 4 | "reflect" 5 | 6 | apps "k8s.io/api/apps/v1" 7 | v1 "k8s.io/api/core/v1" 8 | "reactive-tech.io/kubegres/internal/controller/ctx" 9 | ) 10 | 11 | type ContainerSecurityContextSpecEnforcer struct { 12 | KubegresContext ctx.KubegresContext 13 | } 14 | 15 | func CreateContainerSecurityContextSpecEnforcer(kubegresContext ctx.KubegresContext) ContainerSecurityContextSpecEnforcer { 16 | return ContainerSecurityContextSpecEnforcer{KubegresContext: kubegresContext} 17 | } 18 | 19 | func (r *ContainerSecurityContextSpecEnforcer) GetSpecName() string { 20 | return "ContainerSecurityContext" 21 | } 22 | 23 | func (r *ContainerSecurityContextSpecEnforcer) CheckForSpecDifference(statefulSet *apps.StatefulSet) StatefulSetSpecDifference { 24 | current := statefulSet.Spec.Template.Spec.Containers[0].SecurityContext 25 | expected := r.KubegresContext.Kubegres.Spec.ContainerSecurityContext 26 | emptyContainerSecurityContext := &v1.SecurityContext{} 27 | 28 | if expected == nil && reflect.DeepEqual(current, emptyContainerSecurityContext) { 29 | return StatefulSetSpecDifference{} 30 | } 31 | 32 | if !reflect.DeepEqual(current, expected) { 33 | return StatefulSetSpecDifference{ 34 | SpecName: r.GetSpecName(), 35 | Current: current.String(), 36 | Expected: expected.String(), 37 | } 38 | } 39 | 40 | return StatefulSetSpecDifference{} 41 | } 42 | 43 | func (r *ContainerSecurityContextSpecEnforcer) EnforceSpec(statefulSet *apps.StatefulSet) (wasSpecUpdated bool, err error) { 44 | 45 | statefulSet.Spec.Template.Spec.Containers[0].SecurityContext = r.KubegresContext.Kubegres.Spec.ContainerSecurityContext 46 | 47 | if len(statefulSet.Spec.Template.Spec.InitContainers) > 0 { 48 | statefulSet.Spec.Template.Spec.InitContainers[0].SecurityContext = r.KubegresContext.Kubegres.Spec.ContainerSecurityContext 49 | } 50 | 51 | return true, nil 52 | } 53 | 54 | func (r *ContainerSecurityContextSpecEnforcer) OnSpecEnforcedSuccessfully(statefulSet *apps.StatefulSet) error { 55 | return nil 56 | } 57 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/statefulset_spec/CustomConfigSpecEnforcer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package statefulset_spec 22 | 23 | import ( 24 | apps "k8s.io/api/apps/v1" 25 | "reactive-tech.io/kubegres/internal/controller/spec/template" 26 | ) 27 | 28 | type CustomConfigSpecEnforcer struct { 29 | customConfigSpecHelper template.CustomConfigSpecHelper 30 | } 31 | 32 | func CreateCustomConfigSpecEnforcer(customConfigSpecHelper template.CustomConfigSpecHelper) CustomConfigSpecEnforcer { 33 | return CustomConfigSpecEnforcer{customConfigSpecHelper: customConfigSpecHelper} 34 | } 35 | 36 | func (r *CustomConfigSpecEnforcer) GetSpecName() string { 37 | return "CustomConfig" 38 | } 39 | 40 | func (r *CustomConfigSpecEnforcer) CheckForSpecDifference(statefulSet *apps.StatefulSet) StatefulSetSpecDifference { 41 | 42 | statefulSetCopy := *statefulSet 43 | hasStatefulSetChanged, changesDetails := r.customConfigSpecHelper.ConfigureStatefulSet(&statefulSetCopy) 44 | 45 | if hasStatefulSetChanged { 46 | return StatefulSetSpecDifference{ 47 | SpecName: r.GetSpecName(), 48 | Current: " ", 49 | Expected: changesDetails, 50 | } 51 | } 52 | 53 | return StatefulSetSpecDifference{} 54 | } 55 | 56 | func (r *CustomConfigSpecEnforcer) EnforceSpec(statefulSet *apps.StatefulSet) (wasSpecUpdated bool, err error) { 57 | wasSpecUpdated, _ = r.customConfigSpecHelper.ConfigureStatefulSet(statefulSet) 58 | return wasSpecUpdated, nil 59 | } 60 | 61 | func (r *CustomConfigSpecEnforcer) OnSpecEnforcedSuccessfully(statefulSet *apps.StatefulSet) error { 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/statefulset_spec/ImageSpecEnforcer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package statefulset_spec 22 | 23 | import ( 24 | apps "k8s.io/api/apps/v1" 25 | "reactive-tech.io/kubegres/internal/controller/ctx" 26 | ) 27 | 28 | type ImageSpecEnforcer struct { 29 | kubegresContext ctx.KubegresContext 30 | } 31 | 32 | func CreateImageSpecEnforcer(kubegresContext ctx.KubegresContext) ImageSpecEnforcer { 33 | return ImageSpecEnforcer{kubegresContext: kubegresContext} 34 | } 35 | 36 | func (r *ImageSpecEnforcer) GetSpecName() string { 37 | return "Image" 38 | } 39 | 40 | func (r *ImageSpecEnforcer) CheckForSpecDifference(statefulSet *apps.StatefulSet) StatefulSetSpecDifference { 41 | 42 | current := statefulSet.Spec.Template.Spec.Containers[0].Image 43 | expected := r.kubegresContext.Kubegres.Spec.Image 44 | 45 | if current != expected { 46 | return StatefulSetSpecDifference{ 47 | SpecName: r.GetSpecName(), 48 | Current: current, 49 | Expected: expected, 50 | } 51 | } 52 | 53 | return StatefulSetSpecDifference{} 54 | } 55 | 56 | func (r *ImageSpecEnforcer) EnforceSpec(statefulSet *apps.StatefulSet) (wasSpecUpdated bool, err error) { 57 | statefulSet.Spec.Template.Spec.Containers[0].Image = r.kubegresContext.Kubegres.Spec.Image 58 | 59 | if len(statefulSet.Spec.Template.Spec.InitContainers) > 0 { 60 | statefulSet.Spec.Template.Spec.InitContainers[0].Image = r.kubegresContext.Kubegres.Spec.Image 61 | } 62 | 63 | return true, nil 64 | } 65 | 66 | func (r *ImageSpecEnforcer) OnSpecEnforcedSuccessfully(statefulSet *apps.StatefulSet) error { 67 | return nil 68 | } 69 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/statefulset_spec/LivenessProbeSpecEnforcer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package statefulset_spec 22 | 23 | import ( 24 | apps "k8s.io/api/apps/v1" 25 | "reactive-tech.io/kubegres/internal/controller/ctx" 26 | "reactive-tech.io/kubegres/internal/controller/states" 27 | "reflect" 28 | ) 29 | 30 | type LivenessProbeSpecEnforcer struct { 31 | kubegresContext ctx.KubegresContext 32 | resourcesStates states.ResourcesStates 33 | } 34 | 35 | func CreateLivenessProbeSpecEnforcer(kubegresContext ctx.KubegresContext) LivenessProbeSpecEnforcer { 36 | return LivenessProbeSpecEnforcer{kubegresContext: kubegresContext} 37 | } 38 | 39 | func (r *LivenessProbeSpecEnforcer) GetSpecName() string { 40 | return "LivenessProbe" 41 | } 42 | 43 | func (r *LivenessProbeSpecEnforcer) CheckForSpecDifference(statefulSet *apps.StatefulSet) StatefulSetSpecDifference { 44 | current := statefulSet.Spec.Template.Spec.Containers[0].LivenessProbe 45 | expected := r.kubegresContext.Kubegres.Spec.Probe.LivenessProbe 46 | 47 | if expected == nil { 48 | return StatefulSetSpecDifference{} 49 | } 50 | 51 | if !reflect.DeepEqual(current, expected) { 52 | return StatefulSetSpecDifference{ 53 | SpecName: r.GetSpecName(), 54 | Current: current.String(), 55 | Expected: expected.String(), 56 | } 57 | } 58 | 59 | return StatefulSetSpecDifference{} 60 | } 61 | 62 | func (r *LivenessProbeSpecEnforcer) EnforceSpec(statefulSet *apps.StatefulSet) (wasSpecUpdated bool, err error) { 63 | statefulSet.Spec.Template.Spec.Containers[0].LivenessProbe = r.kubegresContext.Kubegres.Spec.Probe.LivenessProbe 64 | return true, nil 65 | } 66 | 67 | func (r *LivenessProbeSpecEnforcer) OnSpecEnforcedSuccessfully(statefulSet *apps.StatefulSet) error { 68 | return nil 69 | } 70 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/statefulset_spec/PortSpecEnforcer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package statefulset_spec 22 | 23 | import ( 24 | apps "k8s.io/api/apps/v1" 25 | core "k8s.io/api/core/v1" 26 | "reactive-tech.io/kubegres/internal/controller/ctx" 27 | "reactive-tech.io/kubegres/internal/controller/states" 28 | "strconv" 29 | ) 30 | 31 | type PortSpecEnforcer struct { 32 | kubegresContext ctx.KubegresContext 33 | resourcesStates states.ResourcesStates 34 | } 35 | 36 | func CreatePortSpecEnforcer(kubegresContext ctx.KubegresContext, resourcesStates states.ResourcesStates) PortSpecEnforcer { 37 | return PortSpecEnforcer{kubegresContext: kubegresContext, resourcesStates: resourcesStates} 38 | } 39 | 40 | func (r *PortSpecEnforcer) GetSpecName() string { 41 | return "Port" 42 | } 43 | 44 | func (r *PortSpecEnforcer) CheckForSpecDifference(statefulSet *apps.StatefulSet) StatefulSetSpecDifference { 45 | 46 | current := int(statefulSet.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort) 47 | expected := int(r.kubegresContext.Kubegres.Spec.Port) 48 | 49 | if current != expected { 50 | return StatefulSetSpecDifference{ 51 | SpecName: r.GetSpecName(), 52 | Current: strconv.Itoa(current), 53 | Expected: strconv.Itoa(expected), 54 | } 55 | } 56 | 57 | return StatefulSetSpecDifference{} 58 | } 59 | 60 | func (r *PortSpecEnforcer) EnforceSpec(statefulSet *apps.StatefulSet) (wasSpecUpdated bool, err error) { 61 | statefulSet.Spec.Template.Spec.Containers[0].Ports[0].ContainerPort = r.kubegresContext.Kubegres.Spec.Port 62 | return true, nil 63 | } 64 | 65 | func (r *PortSpecEnforcer) OnSpecEnforcedSuccessfully(statefulSet *apps.StatefulSet) error { 66 | 67 | if r.isPrimaryStatefulSet(statefulSet) && r.isPrimaryServicePortNotUpToDate() { 68 | return r.deletePrimaryService() 69 | 70 | } else if !r.isPrimaryStatefulSet(statefulSet) && r.isReplicaServicePortNotUpToDate() { 71 | return r.deleteReplicaService() 72 | } 73 | 74 | return nil 75 | } 76 | 77 | func (r *PortSpecEnforcer) isPrimaryStatefulSet(statefulSet *apps.StatefulSet) bool { 78 | return statefulSet.Name == r.resourcesStates.StatefulSets.Primary.StatefulSet.Name 79 | } 80 | 81 | func (r *PortSpecEnforcer) isPrimaryServicePortNotUpToDate() bool { 82 | if !r.resourcesStates.Services.Primary.IsDeployed { 83 | return false 84 | } 85 | 86 | servicePort := r.resourcesStates.Services.Primary.Service.Spec.Ports[0].Port 87 | postgresPort := r.kubegresContext.Kubegres.Spec.Port 88 | return postgresPort != servicePort 89 | } 90 | 91 | func (r *PortSpecEnforcer) isReplicaServicePortNotUpToDate() bool { 92 | if !r.resourcesStates.Services.Replica.IsDeployed { 93 | return false 94 | } 95 | 96 | servicePort := r.resourcesStates.Services.Replica.Service.Spec.Ports[0].Port 97 | postgresPort := r.kubegresContext.Kubegres.Spec.Port 98 | return postgresPort != servicePort 99 | } 100 | 101 | func (r *PortSpecEnforcer) deletePrimaryService() error { 102 | return r.deleteService(r.resourcesStates.Services.Primary.Service) 103 | } 104 | 105 | func (r *PortSpecEnforcer) deleteReplicaService() error { 106 | return r.deleteService(r.resourcesStates.Services.Replica.Service) 107 | } 108 | 109 | func (r *PortSpecEnforcer) deleteService(serviceToDelete core.Service) error { 110 | 111 | r.kubegresContext.Log.Info("Deleting Service because Spec port changed.", "Service name", serviceToDelete.Name) 112 | err := r.kubegresContext.Client.Delete(r.kubegresContext.Ctx, &serviceToDelete) 113 | 114 | if err != nil { 115 | r.kubegresContext.Log.ErrorEvent("ServiceDeletionErr", err, "Unable to delete a service to apply the new Spec port change.", "Service name", serviceToDelete.Name) 116 | r.kubegresContext.Log.Error(err, "Unable to delete Service", "name", serviceToDelete.Name) 117 | return err 118 | } 119 | 120 | r.kubegresContext.Log.InfoEvent("ServiceDeletion", "Deleted a service to apply the new Spec port change.", "Service name", serviceToDelete.Name) 121 | return nil 122 | } 123 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/statefulset_spec/ReadinessProbeSpecEnforcer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package statefulset_spec 22 | 23 | import ( 24 | apps "k8s.io/api/apps/v1" 25 | "reactive-tech.io/kubegres/internal/controller/ctx" 26 | "reflect" 27 | ) 28 | 29 | type ReadinessProbeSpecEnforcer struct { 30 | kubegresContext ctx.KubegresContext 31 | } 32 | 33 | func CreateReadinessProbeSpecEnforcer(kubegresContext ctx.KubegresContext) ReadinessProbeSpecEnforcer { 34 | return ReadinessProbeSpecEnforcer{kubegresContext: kubegresContext} 35 | } 36 | 37 | func (r *ReadinessProbeSpecEnforcer) GetSpecName() string { 38 | return "ReadinessProbe" 39 | } 40 | 41 | func (r *ReadinessProbeSpecEnforcer) CheckForSpecDifference(statefulSet *apps.StatefulSet) StatefulSetSpecDifference { 42 | current := statefulSet.Spec.Template.Spec.Containers[0].ReadinessProbe 43 | expected := r.kubegresContext.Kubegres.Spec.Probe.ReadinessProbe 44 | 45 | if expected == nil { 46 | return StatefulSetSpecDifference{} 47 | } 48 | 49 | if !reflect.DeepEqual(current, expected) { 50 | return StatefulSetSpecDifference{ 51 | SpecName: r.GetSpecName(), 52 | Current: current.String(), 53 | Expected: expected.String(), 54 | } 55 | } 56 | 57 | return StatefulSetSpecDifference{} 58 | } 59 | 60 | func (r *ReadinessProbeSpecEnforcer) EnforceSpec(statefulSet *apps.StatefulSet) (wasSpecUpdated bool, err error) { 61 | statefulSet.Spec.Template.Spec.Containers[0].ReadinessProbe = r.kubegresContext.Kubegres.Spec.Probe.ReadinessProbe 62 | return true, nil 63 | } 64 | 65 | func (r *ReadinessProbeSpecEnforcer) OnSpecEnforcedSuccessfully(statefulSet *apps.StatefulSet) error { 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/statefulset_spec/ResourcesSpecEnforcer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package statefulset_spec 22 | 23 | import ( 24 | apps "k8s.io/api/apps/v1" 25 | v1 "k8s.io/api/core/v1" 26 | "k8s.io/apimachinery/pkg/api/resource" 27 | "reactive-tech.io/kubegres/internal/controller/ctx" 28 | ) 29 | 30 | type ResourcesSpecEnforcer struct { 31 | kubegresContext ctx.KubegresContext 32 | } 33 | 34 | func CreateResourcesSpecEnforcer(kubegresContext ctx.KubegresContext) ResourcesSpecEnforcer { 35 | return ResourcesSpecEnforcer{kubegresContext: kubegresContext} 36 | } 37 | 38 | func (r *ResourcesSpecEnforcer) GetSpecName() string { 39 | return "Resources" 40 | } 41 | 42 | func (r *ResourcesSpecEnforcer) CheckForSpecDifference(statefulSet *apps.StatefulSet) StatefulSetSpecDifference { 43 | 44 | current := statefulSet.Spec.Template.Spec.Containers[0].Resources 45 | expected := r.kubegresContext.Kubegres.Spec.Resources 46 | 47 | if !r.compareResources(current, expected) { 48 | return StatefulSetSpecDifference{ 49 | SpecName: r.GetSpecName(), 50 | Current: current.String(), 51 | Expected: expected.String(), 52 | } 53 | } 54 | 55 | return StatefulSetSpecDifference{} 56 | } 57 | 58 | func (r *ResourcesSpecEnforcer) EnforceSpec(statefulSet *apps.StatefulSet) (wasSpecUpdated bool, err error) { 59 | statefulSet.Spec.Template.Spec.Containers[0].Resources = r.kubegresContext.Kubegres.Spec.Resources 60 | return true, nil 61 | } 62 | 63 | func (r *ResourcesSpecEnforcer) OnSpecEnforcedSuccessfully(statefulSet *apps.StatefulSet) error { 64 | return nil 65 | } 66 | 67 | func (r *ResourcesSpecEnforcer) compareResources(currentRequirement v1.ResourceRequirements, 68 | expectedRequirement v1.ResourceRequirements) bool { 69 | 70 | if !r.compareResourceLists(currentRequirement.Limits, expectedRequirement.Limits) { 71 | return false 72 | } 73 | 74 | if !r.compareResourceLists(currentRequirement.Requests, expectedRequirement.Requests) { 75 | return false 76 | } 77 | 78 | return true 79 | } 80 | 81 | func (r *ResourcesSpecEnforcer) compareResourceLists(current v1.ResourceList, expected v1.ResourceList) bool { 82 | if len(current) != len(expected) { 83 | return false 84 | } 85 | 86 | for key, expectedElement := range expected { 87 | expectedElementParsed := resource.MustParse(expectedElement.String()) 88 | currentElement := current[key] 89 | if expectedElementParsed != currentElement { 90 | return false 91 | } 92 | } 93 | return true 94 | } 95 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/statefulset_spec/SecurityContextSpecEnforcer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package statefulset_spec 22 | 23 | import ( 24 | v1 "k8s.io/api/core/v1" 25 | "reactive-tech.io/kubegres/internal/controller/ctx" 26 | "reflect" 27 | 28 | apps "k8s.io/api/apps/v1" 29 | ) 30 | 31 | type SecurityContextSpecEnforcer struct { 32 | kubegresContext ctx.KubegresContext 33 | } 34 | 35 | func CreateSecurityContextSpecEnforcer(kubegresContext ctx.KubegresContext) SecurityContextSpecEnforcer { 36 | return SecurityContextSpecEnforcer{kubegresContext: kubegresContext} 37 | } 38 | 39 | func (r *SecurityContextSpecEnforcer) GetSpecName() string { 40 | return "SecurityContext" 41 | } 42 | 43 | func (r *SecurityContextSpecEnforcer) CheckForSpecDifference(statefulSet *apps.StatefulSet) StatefulSetSpecDifference { 44 | 45 | current := statefulSet.Spec.Template.Spec.SecurityContext 46 | expected := r.kubegresContext.Kubegres.Spec.SecurityContext 47 | emptySecurityContext := &v1.PodSecurityContext{} 48 | 49 | if expected == nil && reflect.DeepEqual(current, emptySecurityContext) { 50 | return StatefulSetSpecDifference{} 51 | } 52 | 53 | if !reflect.DeepEqual(current, expected) { 54 | return StatefulSetSpecDifference{ 55 | SpecName: r.GetSpecName(), 56 | Current: current.String(), 57 | Expected: expected.String(), 58 | } 59 | } 60 | 61 | return StatefulSetSpecDifference{} 62 | } 63 | 64 | func (r *SecurityContextSpecEnforcer) EnforceSpec(statefulSet *apps.StatefulSet) (wasSpecUpdated bool, err error) { 65 | statefulSet.Spec.Template.Spec.SecurityContext = r.kubegresContext.Kubegres.Spec.SecurityContext 66 | return true, nil 67 | } 68 | 69 | func (r *SecurityContextSpecEnforcer) OnSpecEnforcedSuccessfully(statefulSet *apps.StatefulSet) error { 70 | return nil 71 | } 72 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/statefulset_spec/ServiceAccountNameSpecEnforcer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package statefulset_spec 22 | 23 | import ( 24 | apps "k8s.io/api/apps/v1" 25 | "reactive-tech.io/kubegres/internal/controller/ctx" 26 | ) 27 | 28 | type ServiceAccountNameSpecEnforcer struct { 29 | kubegresContext ctx.KubegresContext 30 | } 31 | 32 | func CreateServiceAccountNameSpecEnforcer(kubegresContext ctx.KubegresContext) ServiceAccountNameSpecEnforcer { 33 | return ServiceAccountNameSpecEnforcer{kubegresContext: kubegresContext} 34 | } 35 | 36 | func (r *ServiceAccountNameSpecEnforcer) GetSpecName() string { 37 | return "ServiceAccountName" 38 | } 39 | 40 | func (r *ServiceAccountNameSpecEnforcer) CheckForSpecDifference(statefulSet *apps.StatefulSet) StatefulSetSpecDifference { 41 | 42 | current := statefulSet.Spec.Template.Spec.ServiceAccountName 43 | expected := r.kubegresContext.Kubegres.Spec.ServiceAccountName 44 | 45 | if current != expected { 46 | return StatefulSetSpecDifference{ 47 | SpecName: r.GetSpecName(), 48 | Current: current, 49 | Expected: expected, 50 | } 51 | } 52 | 53 | return StatefulSetSpecDifference{} 54 | } 55 | 56 | func (r *ServiceAccountNameSpecEnforcer) EnforceSpec(statefulSet *apps.StatefulSet) (wasSpecUpdated bool, err error) { 57 | statefulSet.Spec.Template.Spec.ServiceAccountName = r.kubegresContext.Kubegres.Spec.ServiceAccountName 58 | return true, nil 59 | } 60 | 61 | func (r *ServiceAccountNameSpecEnforcer) OnSpecEnforcedSuccessfully(_ *apps.StatefulSet) error { 62 | return nil 63 | } 64 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/statefulset_spec/StatefulSetSpecDifferences.go: -------------------------------------------------------------------------------- 1 | package statefulset_spec 2 | 3 | type StatefulSetSpecDifference struct { 4 | SpecName string 5 | Current string 6 | Expected string 7 | } 8 | 9 | func (r *StatefulSetSpecDifference) IsThereDifference() bool { 10 | return r.SpecName != "" 11 | } 12 | 13 | type StatefulSetSpecDifferences struct { 14 | Differences []StatefulSetSpecDifference 15 | } 16 | 17 | func (r *StatefulSetSpecDifferences) IsThereDifference() bool { 18 | return len(r.Differences) > 0 19 | } 20 | 21 | func (r *StatefulSetSpecDifferences) GetSpecDifferencesAsString() string { 22 | if !r.IsThereDifference() { 23 | return "" 24 | } 25 | 26 | specDifferencesStr := "" 27 | separator := "" 28 | for _, specDiff := range r.Differences { 29 | specDifferencesStr += separator + specDiff.SpecName + ": " + specDiff.Expected 30 | separator = ", " 31 | } 32 | 33 | return specDifferencesStr 34 | } 35 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/statefulset_spec/StatefulSetsSpecEnforcer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package statefulset_spec 22 | 23 | import ( 24 | apps "k8s.io/api/apps/v1" 25 | "reactive-tech.io/kubegres/internal/controller/ctx" 26 | ) 27 | 28 | type StatefulSetSpecEnforcer interface { 29 | GetSpecName() string 30 | CheckForSpecDifference(statefulSet *apps.StatefulSet) StatefulSetSpecDifference 31 | EnforceSpec(statefulSet *apps.StatefulSet) (wasSpecUpdated bool, err error) 32 | OnSpecEnforcedSuccessfully(statefulSet *apps.StatefulSet) error 33 | } 34 | 35 | type StatefulSetsSpecsEnforcer struct { 36 | kubegresContext ctx.KubegresContext 37 | registry []StatefulSetSpecEnforcer 38 | } 39 | 40 | func CreateStatefulSetsSpecsEnforcer(kubegresContext ctx.KubegresContext) StatefulSetsSpecsEnforcer { 41 | return StatefulSetsSpecsEnforcer{kubegresContext: kubegresContext} 42 | } 43 | 44 | func (r *StatefulSetsSpecsEnforcer) AddSpecEnforcer(specEnforcer StatefulSetSpecEnforcer) { 45 | r.registry = append(r.registry, specEnforcer) 46 | } 47 | 48 | func (r *StatefulSetsSpecsEnforcer) CheckForSpecDifferences(statefulSet *apps.StatefulSet) StatefulSetSpecDifferences { 49 | 50 | var specDifferences []StatefulSetSpecDifference 51 | 52 | for _, specEnforcer := range r.registry { 53 | specDifference := specEnforcer.CheckForSpecDifference(statefulSet) 54 | if specDifference.IsThereDifference() { 55 | specDifferences = append(specDifferences, specDifference) 56 | } 57 | } 58 | 59 | r.logSpecDifferences(specDifferences, statefulSet) 60 | 61 | return StatefulSetSpecDifferences{ 62 | Differences: specDifferences, 63 | } 64 | } 65 | 66 | func (r *StatefulSetsSpecsEnforcer) EnforceSpec(statefulSet *apps.StatefulSet) error { 67 | 68 | var updatedSpecDifferences []StatefulSetSpecDifference 69 | 70 | for _, specEnforcer := range r.registry { 71 | 72 | specDifference := specEnforcer.CheckForSpecDifference(statefulSet) 73 | if !specDifference.IsThereDifference() { 74 | continue 75 | } 76 | 77 | wasSpecUpdated, err := specEnforcer.EnforceSpec(statefulSet) 78 | if err != nil { 79 | return err 80 | 81 | } else if wasSpecUpdated { 82 | updatedSpecDifferences = append(updatedSpecDifferences, specDifference) 83 | } 84 | } 85 | 86 | if len(updatedSpecDifferences) > 0 { 87 | r.kubegresContext.Log.Info("Updating Spec of a StatefulSet", "StatefulSet name", statefulSet.Name) 88 | err := r.kubegresContext.Client.Update(r.kubegresContext.Ctx, statefulSet) 89 | if err != nil { 90 | return err 91 | } 92 | 93 | r.logUpdatedSpecDifferences(updatedSpecDifferences, statefulSet) 94 | } 95 | 96 | return nil 97 | } 98 | 99 | func (r *StatefulSetsSpecsEnforcer) OnSpecUpdatedSuccessfully(statefulSet *apps.StatefulSet) error { 100 | for _, specEnforcer := range r.registry { 101 | if err := specEnforcer.OnSpecEnforcedSuccessfully(statefulSet); err != nil { 102 | return err 103 | } 104 | } 105 | return nil 106 | } 107 | 108 | func (r *StatefulSetsSpecsEnforcer) logSpecDifferences(specDifferences []StatefulSetSpecDifference, statefulSet *apps.StatefulSet) { 109 | for _, specDifference := range specDifferences { 110 | r.kubegresContext.Log.InfoEvent("StatefulSetOperation", "The Spec is NOT up-to-date for a StatefulSet.", "StatefulSet name", statefulSet.Name, "SpecName", specDifference.SpecName, "Expected", specDifference.Expected, "Current", specDifference.Current) 111 | } 112 | } 113 | 114 | func (r *StatefulSetsSpecsEnforcer) logUpdatedSpecDifferences(specDiffBeforeUpdate []StatefulSetSpecDifference, statefulSet *apps.StatefulSet) { 115 | for _, specDifference := range specDiffBeforeUpdate { 116 | r.kubegresContext.Log.InfoEvent("StatefulSetOperation", "Updated a StatefulSet with up-to-date Spec.", "StatefulSet name", statefulSet.Name, "SpecName", specDifference.SpecName, "New", specDifference.Expected) 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /internal/controller/spec/enforcer/statefulset_spec/TolerationsSpecEnforcer.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package statefulset_spec 22 | 23 | import ( 24 | apps "k8s.io/api/apps/v1" 25 | v1 "k8s.io/api/core/v1" 26 | "reactive-tech.io/kubegres/internal/controller/ctx" 27 | "reflect" 28 | ) 29 | 30 | type TolerationsSpecEnforcer struct { 31 | kubegresContext ctx.KubegresContext 32 | } 33 | 34 | func CreateTolerationsSpecEnforcer(kubegresContext ctx.KubegresContext) TolerationsSpecEnforcer { 35 | return TolerationsSpecEnforcer{kubegresContext: kubegresContext} 36 | } 37 | 38 | func (r *TolerationsSpecEnforcer) GetSpecName() string { 39 | return "Tolerations" 40 | } 41 | 42 | func (r *TolerationsSpecEnforcer) CheckForSpecDifference(statefulSet *apps.StatefulSet) StatefulSetSpecDifference { 43 | 44 | current := statefulSet.Spec.Template.Spec.Tolerations 45 | expected := r.kubegresContext.Kubegres.Spec.Scheduler.Tolerations 46 | 47 | if !r.compare(current, expected) { 48 | return StatefulSetSpecDifference{ 49 | SpecName: r.GetSpecName(), 50 | Current: r.toString(current), 51 | Expected: r.toString(expected), 52 | } 53 | } 54 | 55 | return StatefulSetSpecDifference{} 56 | } 57 | 58 | func (r *TolerationsSpecEnforcer) EnforceSpec(statefulSet *apps.StatefulSet) (wasSpecUpdated bool, err error) { 59 | statefulSet.Spec.Template.Spec.Tolerations = r.kubegresContext.Kubegres.Spec.Scheduler.Tolerations 60 | return true, nil 61 | } 62 | 63 | func (r *TolerationsSpecEnforcer) OnSpecEnforcedSuccessfully(statefulSet *apps.StatefulSet) error { 64 | return nil 65 | } 66 | 67 | func (r *TolerationsSpecEnforcer) compare(current []v1.Toleration, expected []v1.Toleration) bool { 68 | 69 | if len(current) != len(expected) { 70 | return false 71 | } 72 | 73 | index := 0 74 | for _, expectedItem := range expected { 75 | if !reflect.DeepEqual(expectedItem, current[index]) { 76 | return false 77 | } 78 | index++ 79 | } 80 | 81 | return true 82 | } 83 | 84 | func (r *TolerationsSpecEnforcer) toString(tolerations []v1.Toleration) string { 85 | 86 | toString := "" 87 | for _, toleration := range tolerations { 88 | toString += toleration.String() + " - " 89 | } 90 | return toString 91 | } 92 | -------------------------------------------------------------------------------- /internal/controller/spec/template/ResourceTemplateLoader.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package template 22 | 23 | import ( 24 | apps "k8s.io/api/apps/v1" 25 | batch "k8s.io/api/batch/v1" 26 | core "k8s.io/api/core/v1" 27 | "k8s.io/apimachinery/pkg/runtime" 28 | "k8s.io/client-go/kubernetes/scheme" 29 | log2 "reactive-tech.io/kubegres/internal/controller/ctx/log" 30 | "reactive-tech.io/kubegres/internal/controller/spec/template/yaml" 31 | ) 32 | 33 | type ResourceTemplateLoader struct { 34 | log log2.LogWrapper 35 | } 36 | 37 | func (r *ResourceTemplateLoader) LoadBaseConfigMap() (configMap core.ConfigMap, err error) { 38 | 39 | obj, err := r.decodeYaml(yaml.BaseConfigMapTemplate) 40 | 41 | if err != nil { 42 | r.log.Error(err, "Unable to load Kubegres CustomConfig. Given error:") 43 | return core.ConfigMap{}, err 44 | } 45 | 46 | return *obj.(*core.ConfigMap), nil 47 | } 48 | 49 | func (r *ResourceTemplateLoader) LoadPrimaryService() (serviceTemplate core.Service, err error) { 50 | serviceTemplate, err = r.loadService(yaml.PrimaryServiceTemplate) 51 | return serviceTemplate, err 52 | } 53 | 54 | func (r *ResourceTemplateLoader) LoadReplicaService() (serviceTemplate core.Service, err error) { 55 | return r.loadService(yaml.ReplicaServiceTemplate) 56 | } 57 | 58 | func (r *ResourceTemplateLoader) LoadPrimaryStatefulSet() (statefulSetTemplate apps.StatefulSet, err error) { 59 | return r.loadStatefulSet(yaml.PrimaryStatefulSetTemplate) 60 | } 61 | 62 | func (r *ResourceTemplateLoader) LoadReplicaStatefulSet() (statefulSetTemplate apps.StatefulSet, err error) { 63 | return r.loadStatefulSet(yaml.ReplicaStatefulSetTemplate) 64 | } 65 | 66 | func (r *ResourceTemplateLoader) LoadBackUpCronJob() (cronJob batch.CronJob, err error) { 67 | obj, err := r.decodeYaml(yaml.BackUpCronJobTemplate) 68 | 69 | if err != nil { 70 | r.log.Error(err, "Unable to load Kubegres BackUp CronJob. Given error:") 71 | return batch.CronJob{}, err 72 | } 73 | 74 | return *obj.(*batch.CronJob), nil 75 | } 76 | 77 | func (r *ResourceTemplateLoader) loadService(yamlContents string) (serviceTemplate core.Service, err error) { 78 | 79 | obj, err := r.decodeYaml(yamlContents) 80 | 81 | if err != nil { 82 | r.log.Error(err, "Unable to decode Kubegres Service. Given error: ") 83 | return core.Service{}, err 84 | } 85 | 86 | return *obj.(*core.Service), nil 87 | } 88 | 89 | func (r *ResourceTemplateLoader) loadStatefulSet(yamlContents string) (statefulSetTemplate apps.StatefulSet, err error) { 90 | 91 | obj, err := r.decodeYaml(yamlContents) 92 | 93 | if err != nil { 94 | r.log.Error(err, "Unable to decode Kubegres StatefulSet. Given error: ") 95 | return apps.StatefulSet{}, err 96 | } 97 | 98 | return *obj.(*apps.StatefulSet), nil 99 | } 100 | 101 | func (r *ResourceTemplateLoader) decodeYaml(yamlContents string) (runtime.Object, error) { 102 | 103 | decode := scheme.Codecs.UniversalDeserializer().Decode 104 | 105 | obj, _, err := decode([]byte(yamlContents), nil, nil) 106 | 107 | if err != nil { 108 | r.log.Error(err, "Error in decode: ", "obj", obj) 109 | } 110 | 111 | return obj, err 112 | } 113 | -------------------------------------------------------------------------------- /internal/controller/spec/template/yaml/BackUpCronJobTemplate.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: batch/v1 2 | kind: CronJob 3 | metadata: 4 | name: backup-job-postgres-name 5 | spec: 6 | 7 | # Corn format: https://en.wikipedia.org/wiki/Cron 8 | schedule: "* */1 * * *" 9 | 10 | concurrencyPolicy: Forbid 11 | 12 | jobTemplate: 13 | spec: 14 | template: 15 | spec: 16 | 17 | restartPolicy: OnFailure 18 | 19 | volumes: 20 | - name: backup-volume 21 | persistentVolumeClaim: 22 | claimName: backup-pvc-postgres-name 23 | 24 | - name: postgres-config 25 | configMap: 26 | name: base-kubegres-config 27 | defaultMode: 0777 28 | 29 | containers: 30 | - name: backup-postgres 31 | image: postgres:latest 32 | imagePullPolicy: IfNotPresent 33 | args: 34 | - sh 35 | - -c 36 | - /tmp/backup_database.sh 37 | 38 | volumeMounts: 39 | - name: backup-volume 40 | mountPath: toBeReplaced 41 | 42 | - name: postgres-config 43 | mountPath: /tmp/backup_database.sh 44 | subPath: backup_database.sh 45 | 46 | env: 47 | - name: PGPASSWORD 48 | valueFrom: 49 | secretKeyRef: 50 | name: toBeReplaced 51 | key: superUserPassword 52 | 53 | - name: KUBEGRES_RESOURCE_NAME 54 | value: toBeReplaced 55 | 56 | - name: BACKUP_DESTINATION_FOLDER 57 | value: toBeReplaced 58 | 59 | - name: BACKUP_SOURCE_DB_HOST_NAME 60 | value: toBeReplaced 61 | -------------------------------------------------------------------------------- /internal/controller/spec/template/yaml/PrimaryServiceTemplate.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: postgres-name 5 | namespace: default 6 | labels: 7 | app: postgres-name 8 | replicationRole: primary 9 | spec: 10 | clusterIP: None 11 | ports: 12 | - protocol: TCP 13 | port: 5432 14 | selector: 15 | app: postgres-name 16 | replicationRole: primary 17 | -------------------------------------------------------------------------------- /internal/controller/spec/template/yaml/PrimaryStatefulSetTemplate.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: postgres-name-0 5 | namespace: default 6 | labels: 7 | app: postgres-name 8 | index: '1' 9 | replicationRole: primary 10 | 11 | spec: 12 | serviceName: "postgres-name" 13 | replicas: 1 14 | selector: 15 | matchLabels: 16 | app: postgres-name 17 | index: '1' 18 | 19 | volumeClaimTemplates: 20 | - metadata: 21 | name: postgres-db 22 | spec: 23 | accessModes: [ "ReadWriteOnce" ] 24 | storageClassName: standard 25 | resources: 26 | requests: 27 | storage: 1Gi 28 | 29 | template: 30 | metadata: 31 | labels: 32 | app: postgres-name 33 | index: '1' 34 | replicationRole: primary 35 | 36 | spec: 37 | terminationGracePeriodSeconds: 10 38 | volumes: 39 | - name: base-config 40 | configMap: 41 | name: base-kubegres-config 42 | defaultMode: 0777 43 | 44 | - name: custom-config 45 | configMap: 46 | name: toBeReplaced 47 | defaultMode: 0777 48 | 49 | containers: 50 | - name: postgres-name-0 51 | image: postgres:latest 52 | imagePullPolicy: IfNotPresent 53 | args: ["-c", "config_file=/etc/postgres.conf", "-c", "hba_file=/etc/pg_hba.conf"] 54 | 55 | ports: 56 | - containerPort: 5432 57 | protocol: TCP 58 | 59 | env: 60 | - name: POD_IP 61 | valueFrom: 62 | fieldRef: 63 | apiVersion: v1 64 | fieldPath: status.podIP 65 | 66 | livenessProbe: 67 | exec: 68 | command: 69 | - sh 70 | - -c 71 | - exec pg_isready -U postgres -h $POD_IP 72 | failureThreshold: 10 73 | initialDelaySeconds: 60 74 | periodSeconds: 20 75 | successThreshold: 1 76 | timeoutSeconds: 15 77 | 78 | readinessProbe: 79 | exec: 80 | command: 81 | - sh 82 | - -c 83 | - exec pg_isready -U postgres -h $POD_IP 84 | failureThreshold: 3 85 | initialDelaySeconds: 5 86 | periodSeconds: 10 87 | successThreshold: 1 88 | timeoutSeconds: 3 89 | 90 | volumeMounts: 91 | - name: postgres-db 92 | mountPath: toBeReplaced 93 | 94 | - name: base-config 95 | mountPath: /docker-entrypoint-initdb.d/primary_create_replication_role.sh 96 | subPath: primary_create_replication_role.sh 97 | 98 | - name: base-config 99 | mountPath: /etc/postgres.conf 100 | subPath: postgres.conf 101 | 102 | - name: base-config 103 | mountPath: /docker-entrypoint-initdb.d/primary_init_script.sh 104 | subPath: primary_init_script.sh 105 | 106 | - name: base-config 107 | mountPath: /etc/pg_hba.conf 108 | subPath: pg_hba.conf 109 | -------------------------------------------------------------------------------- /internal/controller/spec/template/yaml/ReplicaServiceTemplate.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: postgres-name-replica 5 | namespace: default 6 | labels: 7 | app: postgres-name 8 | replicationRole: replica 9 | spec: 10 | clusterIP: None 11 | ports: 12 | - protocol: TCP 13 | port: 5432 14 | selector: 15 | app: postgres-name 16 | replicationRole: replica 17 | -------------------------------------------------------------------------------- /internal/controller/spec/template/yaml/ReplicaStatefulSetTemplate.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: StatefulSet 3 | metadata: 4 | name: postgres-name-1 5 | namespace: default 6 | labels: 7 | app: postgres-name 8 | index: '2' 9 | replicationRole: replica 10 | 11 | spec: 12 | serviceName: "postgres-replica-name" 13 | replicas: 1 14 | selector: 15 | matchLabels: 16 | app: postgres-name 17 | index: '2' 18 | 19 | volumeClaimTemplates: 20 | - metadata: 21 | name: postgres-db 22 | spec: 23 | accessModes: [ "ReadWriteOnce" ] 24 | storageClassName: standard 25 | resources: 26 | requests: 27 | storage: 1Gi 28 | 29 | template: 30 | metadata: 31 | labels: 32 | app: postgres-name 33 | index: '2' 34 | replicationRole: replica 35 | 36 | spec: 37 | terminationGracePeriodSeconds: 10 38 | volumes: 39 | - name: base-config 40 | configMap: 41 | name: base-kubegres-config 42 | defaultMode: 0777 43 | 44 | - name: custom-config 45 | configMap: 46 | name: toBeReplaced 47 | defaultMode: 0777 48 | 49 | initContainers: 50 | - name: setup-replica-data-directory 51 | image: postgres:latest 52 | imagePullPolicy: IfNotPresent 53 | env: 54 | - name: PRIMARY_HOST_NAME 55 | - name: PGPASSWORD 56 | - name: PGDATA 57 | 58 | command: 59 | - sh 60 | - -c 61 | - /tmp/copy_primary_data_to_replica.sh 62 | 63 | volumeMounts: 64 | - name: postgres-db 65 | mountPath: toBeReplaced 66 | 67 | - name: base-config 68 | mountPath: /tmp/copy_primary_data_to_replica.sh 69 | subPath: copy_primary_data_to_replica.sh 70 | 71 | containers: 72 | - name: postgres-name-1 73 | image: postgres:latest 74 | imagePullPolicy: IfNotPresent 75 | args: ["-c", "config_file=/etc/postgres.conf", "-c", "hba_file=/etc/pg_hba.conf"] 76 | 77 | ports: 78 | - containerPort: 5432 79 | protocol: TCP 80 | 81 | env: 82 | - name: POD_IP 83 | valueFrom: 84 | fieldRef: 85 | apiVersion: v1 86 | fieldPath: status.podIP 87 | 88 | livenessProbe: 89 | exec: 90 | command: 91 | - sh 92 | - -c 93 | - exec pg_isready -U postgres -h $POD_IP 94 | failureThreshold: 10 95 | initialDelaySeconds: 60 96 | periodSeconds: 20 97 | successThreshold: 1 98 | timeoutSeconds: 15 99 | 100 | readinessProbe: 101 | exec: 102 | command: 103 | - sh 104 | - -c 105 | - exec pg_isready -U postgres -h $POD_IP 106 | failureThreshold: 3 107 | initialDelaySeconds: 5 108 | periodSeconds: 10 109 | successThreshold: 1 110 | timeoutSeconds: 3 111 | 112 | volumeMounts: 113 | - name: postgres-db 114 | mountPath: toBeReplaced 115 | 116 | - name: base-config 117 | mountPath: /etc/postgres.conf 118 | subPath: postgres.conf 119 | 120 | - name: base-config 121 | mountPath: /etc/pg_hba.conf 122 | subPath: pg_hba.conf 123 | -------------------------------------------------------------------------------- /internal/controller/states/BackUpStates.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package states 22 | 23 | import ( 24 | batch "k8s.io/api/batch/v1" 25 | "k8s.io/api/core/v1" 26 | apierrors "k8s.io/apimachinery/pkg/api/errors" 27 | "reactive-tech.io/kubegres/internal/controller/ctx" 28 | "sigs.k8s.io/controller-runtime/pkg/client" 29 | ) 30 | 31 | type BackUpStates struct { 32 | IsCronJobDeployed bool 33 | IsPvcDeployed bool 34 | ConfigMap string 35 | CronJobLastScheduleTime string 36 | DeployedCronJob *batch.CronJob 37 | 38 | kubegresContext ctx.KubegresContext 39 | } 40 | 41 | func loadBackUpStates(kubegresContext ctx.KubegresContext) (BackUpStates, error) { 42 | backUpStates := BackUpStates{kubegresContext: kubegresContext} 43 | err := backUpStates.loadStates() 44 | return backUpStates, err 45 | } 46 | 47 | func (r *BackUpStates) loadStates() (err error) { 48 | 49 | r.DeployedCronJob, err = r.getDeployedCronJob() 50 | if err != nil { 51 | return err 52 | } 53 | 54 | if r.DeployedCronJob.Name != "" { 55 | r.IsCronJobDeployed = true 56 | 57 | if len(r.DeployedCronJob.Spec.JobTemplate.Spec.Template.Spec.Volumes) >= 2 { 58 | r.ConfigMap = r.DeployedCronJob.Spec.JobTemplate.Spec.Template.Spec.Volumes[1].ConfigMap.Name 59 | } 60 | 61 | if r.DeployedCronJob.Status.LastScheduleTime != nil { 62 | r.CronJobLastScheduleTime = r.DeployedCronJob.Status.LastScheduleTime.String() 63 | } 64 | } 65 | 66 | backUpPvc, err := r.getDeployedPvc() 67 | if err != nil { 68 | return err 69 | } 70 | 71 | if backUpPvc.Name != "" { 72 | r.IsPvcDeployed = true 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func (r *BackUpStates) getDeployedCronJob() (*batch.CronJob, error) { 79 | 80 | namespace := r.kubegresContext.Kubegres.Namespace 81 | resourceName := ctx.CronJobNamePrefix + r.kubegresContext.Kubegres.Name 82 | resourceKey := client.ObjectKey{Namespace: namespace, Name: resourceName} 83 | cronJob := &batch.CronJob{} 84 | 85 | err := r.kubegresContext.Client.Get(r.kubegresContext.Ctx, resourceKey, cronJob) 86 | 87 | if err != nil { 88 | if apierrors.IsNotFound(err) { 89 | err = nil 90 | } else { 91 | r.kubegresContext.Log.ErrorEvent("BackUpCronJobLoadingErr", err, "Unable to load any deployed BackUp CronJob.", "CronJob name", resourceName) 92 | } 93 | } 94 | 95 | return cronJob, err 96 | } 97 | 98 | func (r *BackUpStates) getDeployedPvc() (*v1.PersistentVolumeClaim, error) { 99 | 100 | namespace := r.kubegresContext.Kubegres.Namespace 101 | resourceName := r.kubegresContext.Kubegres.Spec.Backup.PvcName 102 | resourceKey := client.ObjectKey{Namespace: namespace, Name: resourceName} 103 | pvc := &v1.PersistentVolumeClaim{} 104 | 105 | err := r.kubegresContext.Client.Get(r.kubegresContext.Ctx, resourceKey, pvc) 106 | 107 | if err != nil { 108 | if apierrors.IsNotFound(err) { 109 | err = nil 110 | } else { 111 | r.kubegresContext.Log.ErrorEvent("BackUpPersistentVolumeClaimLoadingErr", err, "Unable to load any deployed BackUp PersistentVolumeClaim.", "PersistentVolumeClaim name", resourceName) 112 | } 113 | } 114 | 115 | return pvc, err 116 | } 117 | -------------------------------------------------------------------------------- /internal/controller/states/ConfigStates.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package states 22 | 23 | import ( 24 | core "k8s.io/api/core/v1" 25 | apierrors "k8s.io/apimachinery/pkg/api/errors" 26 | "reactive-tech.io/kubegres/internal/controller/ctx" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | ) 29 | 30 | const ( 31 | ConfigMapDataKeyPostgresConf = "postgres.conf" 32 | ConfigMapDataKeyPrimaryInitScript = "primary_init_script.sh" 33 | ConfigMapDataKeyPgHbaConf = "pg_hba.conf" 34 | ConfigMapDataKeyBackUpScript = "backup_database.sh" 35 | ) 36 | 37 | type ConfigStates struct { 38 | IsBaseConfigDeployed bool 39 | BaseConfigName string 40 | IsCustomConfigDeployed bool 41 | CustomConfigName string 42 | ConfigLocations ConfigLocations 43 | 44 | kubegresContext ctx.KubegresContext 45 | } 46 | 47 | // Stores as string the volume-name for each config-type which can be either 'base-config' or 'custom-config' 48 | type ConfigLocations struct { 49 | PostgreConf string 50 | PrimaryInitScript string 51 | BackUpScript string 52 | PgHbaConf string 53 | } 54 | 55 | func loadConfigStates(kubegresContext ctx.KubegresContext) (ConfigStates, error) { 56 | 57 | configMapStates := ConfigStates{kubegresContext: kubegresContext} 58 | configMapStates.BaseConfigName = ctx.BaseConfigMapName 59 | configMapStates.CustomConfigName = kubegresContext.Kubegres.Spec.CustomConfig 60 | 61 | err := configMapStates.loadStates() 62 | 63 | return configMapStates, err 64 | } 65 | 66 | func (r *ConfigStates) loadStates() (err error) { 67 | 68 | r.ConfigLocations.PostgreConf = ctx.BaseConfigMapVolumeName 69 | r.ConfigLocations.PrimaryInitScript = ctx.BaseConfigMapVolumeName 70 | r.ConfigLocations.BackUpScript = ctx.BaseConfigMapVolumeName 71 | r.ConfigLocations.PgHbaConf = ctx.BaseConfigMapVolumeName 72 | 73 | baseConfigMap, err := r.getBaseDeployedConfigMap() 74 | if err != nil { 75 | return err 76 | } 77 | 78 | if r.isBaseConfigMap(baseConfigMap) { 79 | r.IsBaseConfigDeployed = true 80 | } 81 | 82 | if r.isBaseConfigAlsoCustomConfig() { 83 | return nil 84 | } 85 | 86 | customConfigMap, err := r.getDeployedCustomCustomConfigMap() 87 | if err != nil { 88 | return err 89 | } 90 | 91 | if r.isCustomConfigDeployed(customConfigMap) { 92 | 93 | r.IsCustomConfigDeployed = true 94 | 95 | if customConfigMap.Data[ConfigMapDataKeyPostgresConf] != "" { 96 | r.ConfigLocations.PostgreConf = ctx.CustomConfigMapVolumeName 97 | } 98 | 99 | if customConfigMap.Data[ConfigMapDataKeyPrimaryInitScript] != "" { 100 | r.ConfigLocations.PrimaryInitScript = ctx.CustomConfigMapVolumeName 101 | } 102 | 103 | if customConfigMap.Data[ConfigMapDataKeyBackUpScript] != "" { 104 | r.ConfigLocations.BackUpScript = ctx.CustomConfigMapVolumeName 105 | } 106 | 107 | if customConfigMap.Data[ConfigMapDataKeyPgHbaConf] != "" { 108 | r.ConfigLocations.PgHbaConf = ctx.CustomConfigMapVolumeName 109 | } 110 | } 111 | 112 | return nil 113 | } 114 | 115 | func (r *ConfigStates) isBaseConfigAlsoCustomConfig() bool { 116 | return r.CustomConfigName == r.BaseConfigName 117 | } 118 | 119 | func (r *ConfigStates) isBaseConfigMap(configMap *core.ConfigMap) bool { 120 | return configMap.Name == r.BaseConfigName 121 | } 122 | 123 | func (r *ConfigStates) isCustomConfigDeployed(configMap *core.ConfigMap) bool { 124 | return configMap.Name != "" && configMap.Name != r.BaseConfigName 125 | } 126 | 127 | func (r *ConfigStates) getBaseDeployedConfigMap() (*core.ConfigMap, error) { 128 | 129 | namespace := r.kubegresContext.Kubegres.Namespace 130 | resourceName := r.BaseConfigName 131 | configMapKey := client.ObjectKey{Namespace: namespace, Name: resourceName} 132 | 133 | return r.getDeployedConfigMap(configMapKey, resourceName, "Base") 134 | } 135 | 136 | func (r *ConfigStates) getDeployedCustomCustomConfigMap() (*core.ConfigMap, error) { 137 | 138 | namespace := r.kubegresContext.Kubegres.Namespace 139 | resourceName := r.CustomConfigName 140 | configMapKey := client.ObjectKey{Namespace: namespace, Name: resourceName} 141 | 142 | return r.getDeployedConfigMap(configMapKey, resourceName, "Init") 143 | } 144 | 145 | func (r *ConfigStates) getDeployedConfigMap(configMapKey client.ObjectKey, resourceName string, logLabel string) (*core.ConfigMap, error) { 146 | 147 | configMap := &core.ConfigMap{} 148 | err := r.kubegresContext.Client.Get(r.kubegresContext.Ctx, configMapKey, configMap) 149 | 150 | if err != nil { 151 | if apierrors.IsNotFound(err) { 152 | err = nil 153 | } else { 154 | r.kubegresContext.Log.ErrorEvent("ConfigMapLoadingErr", err, "Unable to load any deployed "+logLabel+" Config.", "Config name", resourceName) 155 | } 156 | } 157 | 158 | return configMap, err 159 | } 160 | -------------------------------------------------------------------------------- /internal/controller/states/DbStorageClassStates.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package states 22 | 23 | import ( 24 | storage "k8s.io/api/storage/v1" 25 | apierrors "k8s.io/apimachinery/pkg/api/errors" 26 | "reactive-tech.io/kubegres/internal/controller/ctx" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | ) 29 | 30 | type DbStorageClassStates struct { 31 | IsDeployed bool 32 | StorageClassName string 33 | 34 | kubegresContext ctx.KubegresContext 35 | } 36 | 37 | func loadDbStorageClass(kubegresContext ctx.KubegresContext) (DbStorageClassStates, error) { 38 | dbStorageClassStates := DbStorageClassStates{kubegresContext: kubegresContext} 39 | err := dbStorageClassStates.loadStates() 40 | 41 | return dbStorageClassStates, err 42 | } 43 | 44 | func (r *DbStorageClassStates) loadStates() error { 45 | 46 | dbStorageClass, err := r.GetStorageClass() 47 | if err != nil { 48 | return err 49 | } 50 | 51 | if dbStorageClass.Name != "" { 52 | r.IsDeployed = true 53 | r.StorageClassName = dbStorageClass.Name 54 | } 55 | 56 | return nil 57 | } 58 | 59 | func (r *DbStorageClassStates) GetStorageClass() (*storage.StorageClass, error) { 60 | 61 | namespace := "" 62 | resourceName := r.getSpecStorageClassName() 63 | resourceKey := client.ObjectKey{Namespace: namespace, Name: resourceName} 64 | storageClass := &storage.StorageClass{} 65 | 66 | err := r.kubegresContext.Client.Get(r.kubegresContext.Ctx, resourceKey, storageClass) 67 | 68 | if err != nil { 69 | if apierrors.IsNotFound(err) { 70 | err = nil 71 | } else { 72 | r.kubegresContext.Log.ErrorEvent("DatabaseStorageClassLoadingErr", err, "Unable to load any deployed Database StorageClass.", "StorageClass name", resourceName) 73 | } 74 | } 75 | 76 | return storageClass, err 77 | } 78 | 79 | func (r *DbStorageClassStates) getSpecStorageClassName() string { 80 | return *r.kubegresContext.Kubegres.Spec.Database.StorageClassName 81 | } 82 | -------------------------------------------------------------------------------- /internal/controller/states/ResourcesStates.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package states 22 | 23 | import ( 24 | "reactive-tech.io/kubegres/internal/controller/ctx" 25 | "reactive-tech.io/kubegres/internal/controller/states/statefulset" 26 | ) 27 | 28 | type ResourcesStates struct { 29 | DbStorageClass DbStorageClassStates 30 | StatefulSets statefulset.StatefulSetsStates 31 | Services ServicesStates 32 | Config ConfigStates 33 | BackUp BackUpStates 34 | 35 | kubegresContext ctx.KubegresContext 36 | } 37 | 38 | func LoadResourcesStates(kubegresContext ctx.KubegresContext) (ResourcesStates, error) { 39 | resourcesStates := ResourcesStates{kubegresContext: kubegresContext} 40 | err := resourcesStates.loadStates() 41 | return resourcesStates, err 42 | } 43 | 44 | func (r *ResourcesStates) loadStates() (err error) { 45 | 46 | err = r.loadDbStorageClassStates() 47 | if err != nil { 48 | return err 49 | } 50 | 51 | err = r.loadConfigStates() 52 | if err != nil { 53 | return err 54 | } 55 | 56 | err = r.loadStatefulSetsStates() 57 | if err != nil { 58 | return err 59 | } 60 | 61 | err = r.loadServicesStates() 62 | if err != nil { 63 | return err 64 | } 65 | 66 | err = r.loadBackUpStates() 67 | if err != nil { 68 | return err 69 | } 70 | 71 | return nil 72 | } 73 | 74 | func (r *ResourcesStates) loadDbStorageClassStates() (err error) { 75 | r.DbStorageClass, err = loadDbStorageClass(r.kubegresContext) 76 | return err 77 | } 78 | 79 | func (r *ResourcesStates) loadConfigStates() (err error) { 80 | r.Config, err = loadConfigStates(r.kubegresContext) 81 | return err 82 | } 83 | 84 | func (r *ResourcesStates) loadStatefulSetsStates() (err error) { 85 | r.StatefulSets, err = statefulset.LoadStatefulSetsStates(r.kubegresContext) 86 | return err 87 | } 88 | 89 | func (r *ResourcesStates) loadServicesStates() (err error) { 90 | r.Services, err = loadServicesStates(r.kubegresContext) 91 | return err 92 | } 93 | 94 | func (r *ResourcesStates) loadBackUpStates() (err error) { 95 | r.BackUp, err = loadBackUpStates(r.kubegresContext) 96 | return err 97 | } 98 | -------------------------------------------------------------------------------- /internal/controller/states/ServicesStates.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package states 22 | 23 | import ( 24 | core "k8s.io/api/core/v1" 25 | apierrors "k8s.io/apimachinery/pkg/api/errors" 26 | "reactive-tech.io/kubegres/internal/controller/ctx" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | ) 29 | 30 | type ServicesStates struct { 31 | Primary ServiceWrapper 32 | Replica ServiceWrapper 33 | 34 | kubegresContext ctx.KubegresContext 35 | } 36 | 37 | type ServiceWrapper struct { 38 | Name string 39 | IsDeployed bool 40 | Service core.Service 41 | } 42 | 43 | func loadServicesStates(kubegresContext ctx.KubegresContext) (ServicesStates, error) { 44 | servicesStates := ServicesStates{kubegresContext: kubegresContext} 45 | err := servicesStates.loadStates() 46 | return servicesStates, err 47 | } 48 | 49 | func (r *ServicesStates) loadStates() (err error) { 50 | 51 | deployedServices, err := r.getDeployedServices() 52 | if err != nil { 53 | return err 54 | } 55 | 56 | for _, service := range deployedServices.Items { 57 | 58 | serviceWrapper := ServiceWrapper{IsDeployed: true, Service: service} 59 | 60 | if r.isPrimary(service) { 61 | serviceWrapper.Name = r.kubegresContext.GetServiceResourceName(true) 62 | r.Primary = serviceWrapper 63 | 64 | } else { 65 | serviceWrapper.Name = r.kubegresContext.GetServiceResourceName(false) 66 | r.Replica = serviceWrapper 67 | } 68 | } 69 | 70 | return nil 71 | } 72 | 73 | func (r *ServicesStates) getDeployedServices() (*core.ServiceList, error) { 74 | 75 | list := &core.ServiceList{} 76 | opts := []client.ListOption{ 77 | client.InNamespace(r.kubegresContext.Kubegres.Namespace), 78 | client.MatchingFields{ctx.DeploymentOwnerKey: r.kubegresContext.Kubegres.Name}, 79 | } 80 | err := r.kubegresContext.Client.List(r.kubegresContext.Ctx, list, opts...) 81 | 82 | if err != nil { 83 | if apierrors.IsNotFound(err) { 84 | err = nil 85 | } else { 86 | r.kubegresContext.Log.ErrorEvent("ServiceLoadingErr", err, "Unable to load any deployed Services.", "Kubegres name", r.kubegresContext.Kubegres.Name) 87 | } 88 | } 89 | 90 | return list, err 91 | } 92 | 93 | func (r *ServicesStates) isPrimary(service core.Service) bool { 94 | return service.Labels["replicationRole"] == ctx.PrimaryRoleName 95 | } 96 | -------------------------------------------------------------------------------- /internal/controller/states/log/ResourcesStatesLogger.go: -------------------------------------------------------------------------------- 1 | package log 2 | 3 | import ( 4 | "reactive-tech.io/kubegres/internal/controller/ctx" 5 | states2 "reactive-tech.io/kubegres/internal/controller/states" 6 | "reactive-tech.io/kubegres/internal/controller/states/statefulset" 7 | ) 8 | 9 | type ResourcesStatesLogger struct { 10 | kubegresContext ctx.KubegresContext 11 | resourcesStates states2.ResourcesStates 12 | } 13 | 14 | func CreateResourcesStatesLogger(kubegresContext ctx.KubegresContext, resourcesStates states2.ResourcesStates) ResourcesStatesLogger { 15 | return ResourcesStatesLogger{kubegresContext: kubegresContext, resourcesStates: resourcesStates} 16 | } 17 | 18 | func (r *ResourcesStatesLogger) Log() { 19 | r.logDbStorageClassStates() 20 | r.logConfigStates() 21 | r.logStatefulSetsStates() 22 | r.logServicesStates() 23 | r.logBackUpStates() 24 | } 25 | 26 | func (r *ResourcesStatesLogger) logDbStorageClassStates() { 27 | r.kubegresContext.Log.Info("Database StorageClass states.", 28 | "IsDeployed", r.resourcesStates.DbStorageClass.IsDeployed, 29 | "name", r.resourcesStates.DbStorageClass.StorageClassName) 30 | } 31 | 32 | func (r *ResourcesStatesLogger) logConfigStates() { 33 | r.kubegresContext.Log.Info("Base Config states", 34 | "IsDeployed", r.resourcesStates.Config.IsBaseConfigDeployed, 35 | "name", r.resourcesStates.Config.BaseConfigName) 36 | 37 | if r.resourcesStates.Config.BaseConfigName != r.resourcesStates.Config.CustomConfigName { 38 | r.kubegresContext.Log.Info("Custom Config states", 39 | "IsDeployed", r.resourcesStates.Config.IsCustomConfigDeployed, 40 | "name", r.resourcesStates.Config.CustomConfigName) 41 | } 42 | } 43 | 44 | func (r *ResourcesStatesLogger) logStatefulSetsStates() { 45 | statefulSets := r.resourcesStates.StatefulSets 46 | r.kubegresContext.Log.Info("All StatefulSets deployment states: ", 47 | "Spec expected to deploy", statefulSets.SpecExpectedNbreToDeploy, 48 | "Nbre Deployed", statefulSets.NbreDeployed) 49 | 50 | r.logStatefulSetWrapper("Primary states", statefulSets.Primary) 51 | 52 | for _, replicaStatefulSetWrapper := range statefulSets.Replicas.All.GetAllSortedByInstanceIndex() { 53 | r.logStatefulSetWrapper("Replica states", replicaStatefulSetWrapper) 54 | } 55 | } 56 | 57 | func (r *ResourcesStatesLogger) logStatefulSetWrapper(logLabel string, statefulSetWrapper statefulset.StatefulSetWrapper) { 58 | 59 | if !statefulSetWrapper.IsDeployed { 60 | r.kubegresContext.Log.Info(logLabel+": ", 61 | "IsDeployed", statefulSetWrapper.IsDeployed) 62 | 63 | } else { 64 | r.kubegresContext.Log.Info(logLabel+": ", 65 | "IsDeployed", statefulSetWrapper.IsDeployed, 66 | "Name", statefulSetWrapper.StatefulSet.Name, 67 | "IsReady", statefulSetWrapper.IsReady, 68 | "Pod Name", statefulSetWrapper.Pod.Pod.Name, 69 | "Pod IsDeployed", statefulSetWrapper.Pod.IsDeployed, 70 | "Pod IsReady", statefulSetWrapper.Pod.IsReady, 71 | "Pod IsStuck", statefulSetWrapper.Pod.IsStuck) 72 | } 73 | } 74 | 75 | func (r *ResourcesStatesLogger) logServicesStates() { 76 | r.logServiceWrapper("Primary Service states", r.resourcesStates.Services.Primary) 77 | r.logServiceWrapper("Replica Service states", r.resourcesStates.Services.Replica) 78 | } 79 | 80 | func (r *ResourcesStatesLogger) logServiceWrapper(logLabel string, serviceWrapper states2.ServiceWrapper) { 81 | 82 | if !serviceWrapper.IsDeployed { 83 | r.kubegresContext.Log.Info(logLabel+": ", 84 | "IsDeployed", serviceWrapper.IsDeployed) 85 | 86 | } else { 87 | r.kubegresContext.Log.Info(logLabel+": ", 88 | "IsDeployed", serviceWrapper.IsDeployed, 89 | "name", serviceWrapper.Name) 90 | } 91 | } 92 | 93 | func (r *ResourcesStatesLogger) logBackUpStates() { 94 | r.kubegresContext.Log.Info("BackUp states.", 95 | "IsCronJobDeployed", r.resourcesStates.BackUp.IsCronJobDeployed, 96 | "IsPvcDeployed", r.resourcesStates.BackUp.IsPvcDeployed, 97 | "ConfigMap", r.resourcesStates.BackUp.ConfigMap, 98 | "CronJobLastScheduleTime", r.resourcesStates.BackUp.CronJobLastScheduleTime) 99 | } 100 | -------------------------------------------------------------------------------- /internal/controller/states/statefulset/PodsStates.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package statefulset 22 | 23 | import ( 24 | core "k8s.io/api/core/v1" 25 | apierrors "k8s.io/apimachinery/pkg/api/errors" 26 | "reactive-tech.io/kubegres/internal/controller/ctx" 27 | "sigs.k8s.io/controller-runtime/pkg/client" 28 | "strconv" 29 | ) 30 | 31 | type PodStates struct { 32 | pods []PodWrapper 33 | kubegresContext ctx.KubegresContext 34 | } 35 | 36 | type PodWrapper struct { 37 | IsDeployed bool 38 | IsReady bool 39 | IsStuck bool 40 | InstanceIndex int32 41 | Pod core.Pod 42 | } 43 | 44 | func loadPodsStates(kubegresContext ctx.KubegresContext) (PodStates, error) { 45 | podStates := PodStates{kubegresContext: kubegresContext} 46 | err := podStates.loadStates() 47 | return podStates, err 48 | } 49 | 50 | func (r *PodStates) loadStates() (err error) { 51 | 52 | deployedPods, err := r.getDeployedPods() 53 | if err != nil { 54 | return err 55 | } 56 | 57 | for _, pod := range deployedPods.Items { 58 | 59 | isPodReady := r.isPodReady(pod) 60 | isPodStuck := r.isPodStuck(pod) 61 | 62 | podWrapper := PodWrapper{ 63 | IsDeployed: true, 64 | IsReady: isPodReady && !isPodStuck, 65 | IsStuck: isPodStuck, 66 | InstanceIndex: r.getInstanceIndex(pod), 67 | Pod: pod, 68 | } 69 | 70 | r.pods = append(r.pods, podWrapper) 71 | } 72 | 73 | return nil 74 | } 75 | 76 | func (r *PodStates) getDeployedPods() (*core.PodList, error) { 77 | 78 | list := &core.PodList{} 79 | opts := []client.ListOption{ 80 | client.InNamespace(r.kubegresContext.Kubegres.Namespace), 81 | client.MatchingLabels{"app": r.kubegresContext.Kubegres.Name}, 82 | } 83 | err := r.kubegresContext.Client.List(r.kubegresContext.Ctx, list, opts...) 84 | 85 | if err != nil { 86 | if apierrors.IsNotFound(err) { 87 | r.kubegresContext.Log.Info("There is not any deployed Pods yet", "Kubegres name", r.kubegresContext.Kubegres.Name) 88 | err = nil 89 | } else { 90 | r.kubegresContext.Log.ErrorEvent("PodLoadingErr", err, "Unable to load any deployed Pods.", "Kubegres name", r.kubegresContext.Kubegres.Name) 91 | } 92 | } 93 | 94 | return list, err 95 | } 96 | 97 | func (r *PodStates) isPodReady(pod core.Pod) bool { 98 | 99 | if len(pod.Status.ContainerStatuses) == 0 { 100 | return false 101 | } 102 | 103 | return pod.Status.ContainerStatuses[0].Ready 104 | } 105 | 106 | func (r *PodStates) isPodStuck(pod core.Pod) bool { 107 | 108 | if len(pod.Status.ContainerStatuses) == 0 || 109 | pod.Status.ContainerStatuses[0].State.Waiting == nil { 110 | return false 111 | } 112 | 113 | waitingReason := pod.Status.ContainerStatuses[0].State.Waiting.Reason 114 | if waitingReason == "CrashLoopBackOff" || waitingReason == "Error" { 115 | r.kubegresContext.Log.Info("POD is waiting", "Reason", waitingReason) 116 | return true 117 | } 118 | 119 | return false 120 | } 121 | 122 | func (r *PodStates) getInstanceIndex(pod core.Pod) int32 { 123 | instanceIndex, _ := strconv.ParseInt(pod.Labels["index"], 10, 32) 124 | return int32(instanceIndex) 125 | } 126 | -------------------------------------------------------------------------------- /internal/controller/states/statefulset/StatefulSetWrapper.go: -------------------------------------------------------------------------------- 1 | package statefulset 2 | 3 | import ( 4 | "errors" 5 | "k8s.io/api/apps/v1" 6 | "sort" 7 | "strconv" 8 | ) 9 | 10 | type StatefulSetWrapper struct { 11 | IsDeployed bool 12 | IsReady bool 13 | InstanceIndex int32 14 | StatefulSet v1.StatefulSet 15 | Pod PodWrapper 16 | } 17 | 18 | type StatefulSetWrappers struct { 19 | statefulSetsSortedByInstanceIndex []StatefulSetWrapper 20 | statefulSetsReverseSortedByInstanceIndex []StatefulSetWrapper 21 | } 22 | 23 | func (r *StatefulSetWrappers) Add(statefulSetWrapper StatefulSetWrapper) { 24 | 25 | r.statefulSetsSortedByInstanceIndex = append(r.statefulSetsSortedByInstanceIndex, statefulSetWrapper) 26 | sort.Sort(SortByInstanceIndex(r.statefulSetsSortedByInstanceIndex)) 27 | 28 | r.statefulSetsReverseSortedByInstanceIndex = append(r.statefulSetsReverseSortedByInstanceIndex, statefulSetWrapper) 29 | sort.Sort(ReverseSortByInstanceIndex(r.statefulSetsReverseSortedByInstanceIndex)) 30 | } 31 | 32 | func (r *StatefulSetWrappers) GetAllSortedByInstanceIndex() []StatefulSetWrapper { 33 | return r.copy(r.statefulSetsSortedByInstanceIndex) 34 | } 35 | 36 | func (r *StatefulSetWrappers) GetAllReverseSortedByInstanceIndex() []StatefulSetWrapper { 37 | return r.copy(r.statefulSetsReverseSortedByInstanceIndex) 38 | } 39 | 40 | func (r *StatefulSetWrappers) GetByInstanceIndex(instanceIndex int32) (StatefulSetWrapper, error) { 41 | 42 | for _, statefulSet := range r.statefulSetsSortedByInstanceIndex { 43 | if instanceIndex == statefulSet.InstanceIndex { 44 | return statefulSet, nil 45 | } 46 | } 47 | 48 | return StatefulSetWrapper{}, errors.New("Given StatefulSet's instanceIndex '" + strconv.Itoa(int(instanceIndex)) + "' does not exist.") 49 | } 50 | 51 | func (r *StatefulSetWrappers) GetByName(statefulSetName string) (StatefulSetWrapper, error) { 52 | 53 | for _, statefulSet := range r.statefulSetsSortedByInstanceIndex { 54 | if statefulSetName == statefulSet.StatefulSet.Name { 55 | return statefulSet, nil 56 | } 57 | } 58 | 59 | return StatefulSetWrapper{}, errors.New("Given StatefulSet's name '" + statefulSetName + "' does not exist.") 60 | } 61 | 62 | func (r *StatefulSetWrappers) copy(toCopy []StatefulSetWrapper) []StatefulSetWrapper { 63 | var copyAll []StatefulSetWrapper 64 | copy(toCopy, copyAll) 65 | return toCopy 66 | } 67 | 68 | /* 69 | func (r *StatefulSetWrappers) GetByArrayIndex(arrayIndex int32) (StatefulSetWrapper, error) { 70 | 71 | arrayLength := len(r.statefulSets) 72 | if arrayIndex < 0 || int(arrayIndex) >= arrayLength { 73 | return StatefulSetWrapper{}, errors.New("Given StatefulSet's arrayIndex '" + strconv.Itoa(int(arrayIndex)) + "' does not exist.") 74 | } 75 | 76 | return r.statefulSets[arrayIndex], nil 77 | } 78 | */ 79 | -------------------------------------------------------------------------------- /internal/controller/states/statefulset/StatefulSetWrappersSorting.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package statefulset 22 | 23 | import ( 24 | "strconv" 25 | ) 26 | 27 | type SortByInstanceIndex []StatefulSetWrapper 28 | type ReverseSortByInstanceIndex []StatefulSetWrapper 29 | 30 | func (f SortByInstanceIndex) Len() int { 31 | return len(f) 32 | } 33 | 34 | func (f SortByInstanceIndex) Less(i, j int) bool { 35 | return getInstanceIndex(f[i]) < getInstanceIndex(f[j]) 36 | } 37 | 38 | func (f SortByInstanceIndex) Swap(i, j int) { 39 | f[i], f[j] = f[j], f[i] 40 | } 41 | 42 | func (f ReverseSortByInstanceIndex) Len() int { 43 | return len(f) 44 | } 45 | 46 | func (f ReverseSortByInstanceIndex) Less(i, j int) bool { 47 | return getInstanceIndex(f[i]) > getInstanceIndex(f[j]) 48 | } 49 | 50 | func (f ReverseSortByInstanceIndex) Swap(i, j int) { 51 | f[i], f[j] = f[j], f[i] 52 | } 53 | 54 | func getInstanceIndex(statefulSet StatefulSetWrapper) int32 { 55 | instanceIndexStr := statefulSet.StatefulSet.Spec.Template.Labels["index"] 56 | instanceIndex, _ := strconv.ParseInt(instanceIndexStr, 10, 32) 57 | return int32(instanceIndex) 58 | } 59 | -------------------------------------------------------------------------------- /internal/test/resourceConfigs/ConfigForTest.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package resourceConfigs 22 | 23 | import "time" 24 | 25 | const ( 26 | TestTimeout = time.Second * 240 27 | TestRetryInterval = time.Second * 5 28 | DefaultNamespace = "default" 29 | PrimaryReplicationRole = "primary" 30 | DbPort = 5432 31 | DbUser = "postgres" 32 | DbPassword = "postgresSuperUserPsw" 33 | DbName = "postgres" 34 | TableName = "account" 35 | DbHost = "172.18.0.3" 36 | 37 | KubegresYamlFile = "resourceConfigs/kubegres.yaml" 38 | KubegresResourceName = "my-kubegres" 39 | 40 | SecretYamlFile = "resourceConfigs/secret.yaml" 41 | SecretResourceName = "my-kubegres-secret" 42 | 43 | ServiceAccountYamlFile = "resourceConfigs/serviceAccount.yaml" 44 | ServiceAccountResourceName = "my-kubegres" 45 | 46 | ServiceToSqlQueryPrimaryDbYamlFile = "resourceConfigs/primaryService.yaml" 47 | ServiceToSqlQueryPrimaryDbResourceName = "test-kubegres-primary" 48 | ServiceToSqlQueryPrimaryDbNodePort = 30007 49 | 50 | ServiceToSqlQueryReplicaDbServiceYamlFile = "resourceConfigs/replicaService.yaml" 51 | ServiceToSqlQueryReplicaDbServiceResourceName = "test-kubegres-replica" 52 | ServiceToSqlQueryReplicaDbNodePort = 30008 53 | 54 | BackUpPvcResourceName = "test-pvc-for-backup" 55 | BackUpPvcResourceName2 = "test-pvc-for-backup-2" 56 | BackUpPvcYamlFile = "resourceConfigs/backupPvc.yaml" 57 | 58 | CustomConfigMapEmptyResourceName = "config-empty" 59 | CustomConfigMapEmptyYamlFile = "resourceConfigs/customConfig/configMap_empty.yaml" 60 | 61 | CustomConfigMapWithAllConfigsResourceName = "config-with-all-configs" 62 | CustomConfigMapWithAllConfigsYamlFile = "resourceConfigs/customConfig/configMap_with_all_configs.yaml" 63 | 64 | CustomConfigMapWithBackupDatabaseScriptResourceName = "config-with-backup-database-script" 65 | CustomConfigMapWithBackupDatabaseScriptYamlFile = "resourceConfigs/customConfig/configMap_with_backup_database_script.yaml" 66 | 67 | CustomConfigMapWithPgHbaConfResourceName = "config-with-pg-hba-conf" 68 | CustomConfigMapWithPgHbaConfYamlFile = "resourceConfigs/customConfig/configMap_with_pg_hba_conf.yaml" 69 | 70 | CustomConfigMapWithPostgresConfResourceName = "config-with-postgres-conf" 71 | CustomConfigMapWithPostgresConfYamlFile = "resourceConfigs/customConfig/configMap_with_postgres_conf.yaml" 72 | 73 | CustomConfigMapWithPrimaryInitScriptResourceName = "config-with-primary-init-script" 74 | CustomConfigMapWithPrimaryInitScriptYamlFile = "resourceConfigs/customConfig/configMap_with_primary_init_script.yaml" 75 | 76 | CustomConfigMapWithPostgresConfAndWalLevelSetToLogicalResourceName = "config-with-postgres-conf-wal-level-to-logical" 77 | CustomConfigMapWithPostgresConfAndWalLevelSetToLogicalYamlFile = "resourceConfigs/customConfig/configMap_with_postgres_conf_and_wal_level_to_logical.yaml" 78 | ) 79 | -------------------------------------------------------------------------------- /internal/test/resourceConfigs/LoadTestYaml.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package resourceConfigs 22 | 23 | import ( 24 | "io/ioutil" 25 | "log" 26 | 27 | v1 "k8s.io/api/core/v1" 28 | "k8s.io/apimachinery/pkg/runtime" 29 | "k8s.io/client-go/kubernetes/scheme" 30 | kubegresv1 "reactive-tech.io/kubegres/api/v1" 31 | ) 32 | 33 | func LoadCustomConfigMapYaml(yamlFileName string) v1.ConfigMap { 34 | fileContents := getFileContents(yamlFileName) 35 | obj := decodeYaml(fileContents) 36 | return *obj.(*v1.ConfigMap) 37 | } 38 | 39 | func LoadBackUpPvcYaml() *v1.PersistentVolumeClaim { 40 | fileContents := getFileContents(BackUpPvcYamlFile) 41 | obj := decodeYaml(fileContents) 42 | return obj.(*v1.PersistentVolumeClaim) 43 | } 44 | 45 | func LoadKubegresYaml() *kubegresv1.Kubegres { 46 | fileContents := getFileContents(KubegresYamlFile) 47 | obj := decodeYaml(fileContents) 48 | return obj.(*kubegresv1.Kubegres) 49 | } 50 | 51 | func LoadSecretYaml() v1.Secret { 52 | fileContents := getFileContents(SecretYamlFile) 53 | obj := decodeYaml(fileContents) 54 | return *obj.(*v1.Secret) 55 | } 56 | 57 | func LoadServiceAccountYaml() v1.ServiceAccount { 58 | fileContents := getFileContents(ServiceAccountYamlFile) 59 | obj := decodeYaml(fileContents) 60 | return *obj.(*v1.ServiceAccount) 61 | } 62 | 63 | func LoadYamlServiceToSqlQueryPrimaryDb() v1.Service { 64 | fileContents := getFileContents(ServiceToSqlQueryPrimaryDbYamlFile) 65 | obj := decodeYaml(fileContents) 66 | return *obj.(*v1.Service) 67 | } 68 | 69 | func LoadYamlServiceToSqlQueryReplicaDb() v1.Service { 70 | fileContents := getFileContents(ServiceToSqlQueryReplicaDbServiceYamlFile) 71 | obj := decodeYaml(fileContents) 72 | return *obj.(*v1.Service) 73 | } 74 | 75 | func getFileContents(filePath string) string { 76 | contents, err := ioutil.ReadFile(filePath) 77 | if err != nil { 78 | log.Fatal("Unable to find file '"+filePath+"'. Given error: ", err) 79 | } 80 | return string(contents) 81 | } 82 | 83 | func decodeYaml(yamlContents string) runtime.Object { 84 | 85 | decode := scheme.Codecs.UniversalDeserializer().Decode 86 | 87 | obj, _, err := decode([]byte(yamlContents), nil, nil) 88 | 89 | if err != nil { 90 | log.Fatal("Error in decode:", obj, err) 91 | } 92 | 93 | return obj 94 | } 95 | -------------------------------------------------------------------------------- /internal/test/resourceConfigs/backupPvc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: PersistentVolumeClaim 3 | metadata: 4 | name: test-pvc-for-backup 5 | namespace: default 6 | labels: 7 | environment: acceptancetesting 8 | spec: 9 | storageClassName: standard 10 | accessModes: 11 | - ReadWriteOnce 12 | resources: 13 | requests: 14 | storage: 100Mi 15 | -------------------------------------------------------------------------------- /internal/test/resourceConfigs/customConfig/configMap_empty.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: config-empty 5 | namespace: default 6 | labels: 7 | environment: acceptancetesting 8 | -------------------------------------------------------------------------------- /internal/test/resourceConfigs/customConfig/configMap_with_all_configs.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: config-with-all-configs 5 | namespace: default 6 | labels: 7 | environment: acceptancetesting 8 | 9 | data: 10 | 11 | postgres.conf: | 12 | # Replication configs 13 | listen_addresses = '*' 14 | max_wal_senders = 10 15 | max_connections = 100 16 | shared_buffers = 128MB 17 | 18 | # Logging 19 | #log_destination = 'stderr,csvlog' 20 | #logging_collector = on 21 | #log_directory = 'pg_log' 22 | #log_filename= 'postgresql-%Y-%m-%d_%H%M%S.log' 23 | 24 | 25 | primary_init_script.sh: | 26 | #!/bin/bash 27 | set -e 28 | 29 | dt=$(date '+%d/%m/%Y %H:%M:%S'); 30 | echo "$dt - Running init script the 1st time Primary Kubegres container is created..."; 31 | 32 | customDatabaseName="mydb" 33 | customUserName="mydbuser" 34 | 35 | echo "$dt - Running: psql -v ON_ERROR_STOP=1 --username $POSTGRES_USER --dbname $POSTGRES_DB ..."; 36 | 37 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL 38 | CREATE DATABASE $customDatabaseName; 39 | CREATE USER $customUserName WITH PASSWORD '$POSTGRES_MYAPP_PASSWORD'; 40 | \connect $customDatabaseName; 41 | CREATE TABLE account(user_id serial PRIMARY KEY, username VARCHAR (50) NOT NULL); 42 | INSERT INTO account VALUES (1, 'username1'); 43 | INSERT INTO account VALUES (2, 'username2'); 44 | GRANT ALL PRIVILEGES ON DATABASE "$customDatabaseName" to $customUserName; 45 | GRANT ALL ON "account" to $customUserName; 46 | EOSQL 47 | 48 | echo "$dt - Init script is completed"; 49 | 50 | 51 | pg_hba.conf: | 52 | # TYPE DATABASE USER ADDRESS METHOD 53 | # Replication connections by a user with the replication privilege 54 | host replication replication all md5 55 | # As long as it is authenticated, all connections allowed except from "0.0.0.0/0" 56 | local all all md5 57 | host all all all md5 58 | host all all 0.0.0.0/0 reject 59 | 60 | 61 | backup_database.sh: | 62 | #!/bin/bash 63 | set -e 64 | 65 | dt=$(date '+%d/%m/%Y %H:%M:%S'); 66 | fileDt=$(date '+%d_%m_%Y_%H_%M_%S'); 67 | backUpFileName="$KUBEGRES_RESOURCE_NAME-backup-$fileDt.gz" 68 | backUpFilePath="$BACKUP_DESTINATION_FOLDER/$backUpFileName" 69 | 70 | echo "$dt - Starting DB backup of Kubegres resource $KUBEGRES_RESOURCE_NAME into file: $backUpFilePath"; 71 | echo "$dt - Running: pg_dumpall -h $BACKUP_SOURCE_DB_HOST_NAME -U postgres -c | gzip > $backUpFilePath" 72 | 73 | pg_dumpall -h $BACKUP_SOURCE_DB_HOST_NAME -U postgres -c | gzip > $backUpFilePath 74 | 75 | if [ $? -ne 0 ]; then 76 | rm $backUpFilePath 77 | echo "Unable to execute a BackUp. Please check DB connection settings" 78 | exit 1 79 | fi 80 | 81 | echo "$dt - DB backup completed for Kubegres resource $KUBEGRES_RESOURCE_NAME into file: $backUpFilePath"; 82 | -------------------------------------------------------------------------------- /internal/test/resourceConfigs/customConfig/configMap_with_backup_database_script.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: config-with-backup-database-script 5 | namespace: default 6 | labels: 7 | environment: acceptancetesting 8 | 9 | data: 10 | 11 | backup_database.sh: | 12 | #!/bin/bash 13 | set -e 14 | 15 | dt=$(date '+%d/%m/%Y %H:%M:%S'); 16 | fileDt=$(date '+%d_%m_%Y_%H_%M_%S'); 17 | backUpFileName="$KUBEGRES_RESOURCE_NAME-backup-$fileDt.gz" 18 | backUpFilePath="$BACKUP_DESTINATION_FOLDER/$backUpFileName" 19 | 20 | echo "$dt - Starting DB backup of Kubegres resource $KUBEGRES_RESOURCE_NAME into file: $backUpFilePath"; 21 | echo "$dt - Running: pg_dumpall -h $BACKUP_SOURCE_DB_HOST_NAME -U postgres -c | gzip > $backUpFilePath" 22 | 23 | pg_dumpall -h $BACKUP_SOURCE_DB_HOST_NAME -U postgres -c | gzip > $backUpFilePath 24 | 25 | if [ $? -ne 0 ]; then 26 | rm $backUpFilePath 27 | echo "Unable to execute a BackUp. Please check DB connection settings" 28 | exit 1 29 | fi 30 | 31 | echo "$dt - DB backup completed for Kubegres resource $KUBEGRES_RESOURCE_NAME into file: $backUpFilePath"; 32 | -------------------------------------------------------------------------------- /internal/test/resourceConfigs/customConfig/configMap_with_pg_hba_conf.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: config-with-pg-hba-conf 5 | namespace: default 6 | labels: 7 | environment: acceptancetesting 8 | 9 | data: 10 | 11 | pg_hba.conf: | 12 | # TYPE DATABASE USER ADDRESS METHOD 13 | # Replication connections by a user with the replication privilege 14 | host replication replication all md5 15 | # As long as it is authenticated, all connections allowed except from "0.0.0.0/0" 16 | local all all md5 17 | host all all all md5 18 | host all all 0.0.0.0/0 reject 19 | 20 | -------------------------------------------------------------------------------- /internal/test/resourceConfigs/customConfig/configMap_with_postgres_conf.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: config-with-postgres-conf 5 | namespace: default 6 | labels: 7 | environment: acceptancetesting 8 | 9 | data: 10 | 11 | postgres.conf: | 12 | # Replication configs 13 | listen_addresses = '*' 14 | max_wal_senders = 10 15 | max_connections = 100 16 | shared_buffers = 128MB 17 | 18 | # Logging 19 | #log_destination = 'stderr,csvlog' 20 | #logging_collector = on 21 | #log_directory = 'pg_log' 22 | #log_filename= 'postgresql-%Y-%m-%d_%H%M%S.log' 23 | -------------------------------------------------------------------------------- /internal/test/resourceConfigs/customConfig/configMap_with_postgres_conf_and_wal_level_to_logical.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: config-with-postgres-conf-wal-level-to-logical 5 | namespace: default 6 | labels: 7 | environment: acceptancetesting 8 | 9 | data: 10 | 11 | postgres.conf: | 12 | # We set the property 'wal_level' to 'logical' which overrides its default value which was 'replica' 13 | wal_level = logical 14 | listen_addresses = '*' 15 | max_wal_senders = 10 16 | max_connections = 100 17 | shared_buffers = 128MB 18 | 19 | # Logging 20 | #log_destination = 'stderr,csvlog' 21 | #logging_collector = on 22 | #log_directory = 'pg_log' 23 | #log_filename= 'postgresql-%Y-%m-%d_%H%M%S.log' 24 | -------------------------------------------------------------------------------- /internal/test/resourceConfigs/customConfig/configMap_with_primary_init_script.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ConfigMap 3 | metadata: 4 | name: config-with-primary-init-script 5 | namespace: default 6 | labels: 7 | environment: acceptancetesting 8 | 9 | data: 10 | 11 | primary_init_script.sh: | 12 | #!/bin/bash 13 | set -e 14 | 15 | dt=$(date '+%d/%m/%Y %H:%M:%S'); 16 | echo "$dt - Running init script the 1st time Primary Kubegres container is created..."; 17 | 18 | echo "$dt - Running: psql -v ON_ERROR_STOP=1 --username $POSTGRES_USER --dbname $POSTGRES_DB ..."; 19 | 20 | psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL 21 | CREATE TABLE account(user_id serial PRIMARY KEY, username VARCHAR (50) NOT NULL); 22 | INSERT INTO account VALUES (1, 'username1'); 23 | INSERT INTO account VALUES (2, 'username2'); 24 | EOSQL 25 | 26 | echo "$dt - Init script is completed"; 27 | -------------------------------------------------------------------------------- /internal/test/resourceConfigs/kubegres.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: kubegres.reactive-tech.io/v1 2 | kind: Kubegres 3 | metadata: 4 | name: my-kubegres 5 | namespace: default 6 | labels: 7 | environment: acceptancetesting 8 | spec: 9 | 10 | replicas: 3 11 | image: postgres:17.2 12 | port: 5432 13 | 14 | database: 15 | size: 200Mi 16 | storageClassName: standard 17 | volumeMount: /var/lib/postgres/data 18 | 19 | customConfig: base-kubegres-config 20 | 21 | #backup: 22 | # schedule: "*/1 * * * *" 23 | # pvcName: postgres-db-my-kubegres-2-0 24 | # volumeMount: /tmp/my-kubegres 25 | 26 | env: 27 | - name: POSTGRES_PASSWORD 28 | valueFrom: 29 | secretKeyRef: 30 | name: my-kubegres-secret 31 | key: superUserPassword 32 | 33 | - name: POSTGRES_REPLICATION_PASSWORD 34 | valueFrom: 35 | secretKeyRef: 36 | name: my-kubegres-secret 37 | key: replicationUserPassword 38 | 39 | - name: POSTGRES_MYAPP_PASSWORD 40 | valueFrom: 41 | secretKeyRef: 42 | name: my-kubegres-secret 43 | key: myAppUserPassword 44 | -------------------------------------------------------------------------------- /internal/test/resourceConfigs/primaryService.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: test-kubegres-primary 5 | namespace: default 6 | labels: 7 | environment: acceptancetesting 8 | spec: 9 | type: NodePort 10 | ports: 11 | - protocol: TCP 12 | port: 5432 13 | nodePort: 30007 14 | selector: 15 | app: my-kubegres 16 | replicationRole: primary 17 | 18 | -------------------------------------------------------------------------------- /internal/test/resourceConfigs/replicaService.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: test-kubegres-replica 5 | namespace: default 6 | labels: 7 | environment: acceptancetesting 8 | spec: 9 | type: NodePort 10 | ports: 11 | - protocol: TCP 12 | port: 5432 13 | nodePort: 30008 14 | selector: 15 | app: my-kubegres 16 | replicationRole: replica 17 | -------------------------------------------------------------------------------- /internal/test/resourceConfigs/secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | metadata: 4 | name: my-kubegres-secret 5 | namespace: default 6 | type: Opaque 7 | stringData: 8 | superUserPassword: postgresSuperUserPsw 9 | replicationUserPassword: postgresReplicaPsw 10 | myAppUserPassword: myAppUserPsw 11 | -------------------------------------------------------------------------------- /internal/test/resourceConfigs/serviceAccount.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | name: my-kubegres 5 | namespace: default -------------------------------------------------------------------------------- /internal/test/suite_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package test 22 | 23 | import ( 24 | "fmt" 25 | "k8s.io/client-go/tools/record" 26 | "log" 27 | "path/filepath" 28 | "reactive-tech.io/kubegres/internal/controller" 29 | util2 "reactive-tech.io/kubegres/internal/test/util" 30 | "reactive-tech.io/kubegres/internal/test/util/kindcluster" 31 | "runtime" 32 | ctrl "sigs.k8s.io/controller-runtime" 33 | "testing" 34 | "time" 35 | 36 | . "github.com/onsi/ginkgo/v2" 37 | . "github.com/onsi/gomega" 38 | "k8s.io/client-go/kubernetes/scheme" 39 | "sigs.k8s.io/controller-runtime/pkg/client" 40 | "sigs.k8s.io/controller-runtime/pkg/envtest" 41 | logf "sigs.k8s.io/controller-runtime/pkg/log" 42 | "sigs.k8s.io/controller-runtime/pkg/log/zap" 43 | 44 | postgresv1 "reactive-tech.io/kubegres/api/v1" 45 | // +kubebuilder:scaffold:imports 46 | ) 47 | 48 | // These tests use Ginkgo (BDD-style Go testing framework). Refer to 49 | // http://onsi.github.io/ginkgo/ to learn more about Ginkgo. 50 | 51 | var kindCluster kindcluster.KindTestClusterUtil 52 | 53 | // var cfgTest *rest.Config 54 | var k8sClientTest client.Client 55 | var testEnv *envtest.Environment 56 | var eventRecorderTest util2.MockEventRecorderTestUtil 57 | 58 | func TestAPIs(t *testing.T) { 59 | RegisterFailHandler(Fail) 60 | 61 | RunSpecs(t, "Controller Suite") 62 | } 63 | 64 | var _ = BeforeSuite(func() { 65 | 66 | kindCluster.StartCluster() 67 | 68 | logf.SetLogger(zap.New(zap.WriteTo(GinkgoWriter), zap.UseDevMode(true))) 69 | 70 | log.Print("START OF: BeforeSuite") 71 | 72 | By("bootstrapping test environment") 73 | useExistingCluster := true 74 | testEnv = &envtest.Environment{ 75 | CRDDirectoryPaths: []string{filepath.Join("..", "..", "config", "crd", "bases")}, 76 | ErrorIfCRDPathMissing: true, 77 | UseExistingCluster: &useExistingCluster, 78 | 79 | // The BinaryAssetsDirectory is only required if you want to run the tests directly 80 | // without call the makefile target test. If not informed it will look for the 81 | // default path defined in controller-runtime which is /usr/local/kubebuilder/. 82 | // Note that you must have the required binaries setup under the bin directory to perform 83 | // the tests directly. When we run make test it will be setup and used automatically. 84 | BinaryAssetsDirectory: filepath.Join("..", "..", "bin", "k8s", 85 | fmt.Sprintf("1.31.0-%s-%s", runtime.GOOS, runtime.GOARCH)), 86 | } 87 | 88 | cfg, err := testEnv.Start() 89 | Expect(err).ToNot(HaveOccurred()) 90 | Expect(cfg).ToNot(BeNil()) 91 | cfg.Timeout = 2 * time.Hour 92 | 93 | err = postgresv1.AddToScheme(scheme.Scheme) 94 | Expect(err).NotTo(HaveOccurred()) 95 | 96 | // +kubebuilder:scaffold:scheme 97 | 98 | k8sClientTest, err = client.New(cfg, client.Options{Scheme: scheme.Scheme}) 99 | Expect(err).ToNot(HaveOccurred()) 100 | Expect(k8sClientTest).ToNot(BeNil()) 101 | 102 | k8sManager, err := ctrl.NewManager(cfg, ctrl.Options{ 103 | Scheme: scheme.Scheme, 104 | }) 105 | Expect(err).ToNot(HaveOccurred()) 106 | 107 | mockLogger := util2.CreateMockLogger() 108 | eventRecorderTest = util2.MockEventRecorderTestUtil{} 109 | 110 | err = (&controller.KubegresReconciler{ 111 | Client: k8sManager.GetClient(), 112 | Logger: mockLogger, 113 | Scheme: k8sManager.GetScheme(), 114 | Recorder: record.EventRecorder(&eventRecorderTest), 115 | }).SetupWithManager(k8sManager) 116 | Expect(err).ToNot(HaveOccurred()) 117 | 118 | go func() { 119 | err = k8sManager.Start(ctrl.SetupSignalHandler()) 120 | if err != nil { 121 | log.Fatal("ERROR while starting Kubernetes: ", err) 122 | } 123 | Expect(err).ToNot(HaveOccurred()) 124 | }() 125 | 126 | log.Println("Waiting for Kubernetes to start") 127 | log.Println("Kubernetes has started") 128 | 129 | k8sClientTest = k8sManager.GetClient() 130 | Expect(k8sClientTest).ToNot(BeNil()) 131 | 132 | log.Print("END OF: BeforeSuite") 133 | 134 | }) 135 | 136 | var _ = AfterSuite(func() { 137 | 138 | log.Print("START OF: Suite AfterSuite") 139 | 140 | By("tearing down the test environment") 141 | 142 | err := testEnv.Stop() 143 | Expect(err).ToNot(HaveOccurred()) 144 | 145 | time.Sleep(5 * time.Second) 146 | 147 | kindCluster.DeleteCluster() 148 | 149 | log.Print("END OF: Suite AfterSuite") 150 | }) 151 | -------------------------------------------------------------------------------- /internal/test/util/MockEventRecorderTestUtil.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package util 22 | 23 | import ( 24 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 25 | "k8s.io/apimachinery/pkg/runtime" 26 | "log" 27 | log2 "reactive-tech.io/kubegres/internal/controller/ctx/log" 28 | ) 29 | 30 | type MockEventRecorderTestUtil struct { 31 | eventRecords []EventRecord 32 | } 33 | 34 | type EventRecord struct { 35 | Eventtype string 36 | Reason string 37 | Message string 38 | } 39 | 40 | func (r *MockEventRecorderTestUtil) CheckEventExist(eventRecordToSearch EventRecord) bool { 41 | 42 | for _, eventRecord := range r.eventRecords { 43 | if eventRecord == eventRecordToSearch { 44 | return true 45 | } 46 | } 47 | return false 48 | } 49 | 50 | func (r *MockEventRecorderTestUtil) RemoveAllEvents() { 51 | r.eventRecords = []EventRecord{} 52 | } 53 | 54 | func (r *MockEventRecorderTestUtil) Event(object runtime.Object, eventtype, reason, message string) { 55 | r.Eventf(object, eventtype, reason, message) 56 | } 57 | 58 | func (r *MockEventRecorderTestUtil) Eventf(object runtime.Object, eventtype, reason, messageFmt string, args ...interface{}) { 59 | messageFmt = r.constructFullMsg(messageFmt, args) 60 | log.Println("Event - eventtype: '" + eventtype + "', reason: '" + reason + "', message: '" + messageFmt + "'") 61 | r.eventRecords = append(r.eventRecords, EventRecord{Eventtype: eventtype, Reason: reason, Message: messageFmt}) 62 | } 63 | 64 | func (r *MockEventRecorderTestUtil) PastEventf(object runtime.Object, timestamp metav1.Time, eventtype, reason, messageFmt string, args ...interface{}) { 65 | r.Eventf(object, eventtype, reason, messageFmt, args) 66 | } 67 | 68 | func (r *MockEventRecorderTestUtil) AnnotatedEventf(object runtime.Object, annotations map[string]string, eventtype, reason, messageFmt string, args ...interface{}) { 69 | r.Eventf(object, eventtype, reason, messageFmt, args) 70 | } 71 | 72 | func (r *MockEventRecorderTestUtil) constructFullMsg(msg string, keysAndValues ...interface{}) string { 73 | keysAndValuesStr := log2.InterfacesToStr(keysAndValues...) 74 | if keysAndValuesStr != "" { 75 | return msg + " " + keysAndValuesStr 76 | } 77 | return msg 78 | } 79 | -------------------------------------------------------------------------------- /internal/test/util/MockLogger.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package util 22 | 23 | import ( 24 | "github.com/go-logr/logr" 25 | "log" 26 | log2 "reactive-tech.io/kubegres/internal/controller/ctx/log" 27 | "strings" 28 | ) 29 | 30 | type MockLogSink struct { 31 | name string 32 | } 33 | 34 | func CreateMockLogger() logr.Logger { 35 | logSink := &MockLogSink{} 36 | logger := logr.New(logSink) 37 | return logger 38 | } 39 | 40 | func (r *MockLogSink) Init(info logr.RuntimeInfo) { 41 | } 42 | 43 | func (r *MockLogSink) Enabled(level int) bool { 44 | return true 45 | } 46 | 47 | func (r *MockLogSink) Info(level int, msg string, keysAndValues ...interface{}) { 48 | log.Println(r.constructFullMsg(msg, keysAndValues)) 49 | } 50 | 51 | func (r *MockLogSink) Error(err error, msg string, keysAndValues ...interface{}) { 52 | log.Println(r.constructFullErrMsg(err, msg, keysAndValues)) 53 | } 54 | 55 | func (r *MockLogSink) WithValues(keysAndValues ...interface{}) logr.LogSink { 56 | return r 57 | } 58 | 59 | func (r *MockLogSink) WithName(name string) logr.LogSink { 60 | if !strings.Contains(r.name, name) { 61 | r.name += name + " - " 62 | } 63 | return r 64 | } 65 | 66 | func (r *MockLogSink) constructFullErrMsg(err error, msg string, keysAndValues ...interface{}) string { 67 | msgToReturn := "" 68 | separator := "" 69 | 70 | customErrMsg := r.constructFullMsg(msg, keysAndValues...) 71 | if customErrMsg != "" { 72 | msgToReturn = customErrMsg 73 | separator = " - " 74 | } 75 | 76 | msgFromErr := err.Error() 77 | if msgFromErr != "" { 78 | msgToReturn += separator + msgFromErr 79 | } 80 | 81 | return msgToReturn 82 | } 83 | 84 | func (r *MockLogSink) constructFullMsg(msg string, keysAndValues ...interface{}) string { 85 | if msg == "" { 86 | return r.name + "" 87 | } 88 | 89 | keysAndValuesStr := log2.InterfacesToStr(keysAndValues...) 90 | if keysAndValuesStr != "" { 91 | return r.name + msg + " " + keysAndValuesStr 92 | } 93 | return r.name + msg 94 | } 95 | -------------------------------------------------------------------------------- /internal/test/util/TestResourceModifier.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | v12 "k8s.io/api/core/v1" 5 | postgresv1 "reactive-tech.io/kubegres/api/v1" 6 | ) 7 | 8 | type TestResourceModifier struct { 9 | } 10 | 11 | func (r *TestResourceModifier) AppendAnnotation(annotationKey, annotationValue string, kubegresResource *postgresv1.Kubegres) { 12 | 13 | if kubegresResource.Annotations == nil { 14 | kubegresResource.Annotations = make(map[string]string) 15 | } 16 | 17 | kubegresResource.Annotations[annotationKey] = annotationValue 18 | } 19 | 20 | func (r *TestResourceModifier) AppendEnvVar(envVarName, envVarVal string, kubegresResource *postgresv1.Kubegres) { 21 | 22 | envVar := v12.EnvVar{ 23 | Name: envVarName, 24 | Value: envVarVal, 25 | } 26 | 27 | kubegresResource.Spec.Env = append(kubegresResource.Spec.Env, envVar) 28 | } 29 | 30 | func (r *TestResourceModifier) AppendEnvVarFromSecretKey(envVarName, envVarSecretKeyValueName string, kubegresResource *postgresv1.Kubegres) { 31 | 32 | envVar := v12.EnvVar{ 33 | Name: envVarName, 34 | ValueFrom: &v12.EnvVarSource{ 35 | SecretKeyRef: &v12.SecretKeySelector{ 36 | Key: envVarSecretKeyValueName, 37 | LocalObjectReference: v12.LocalObjectReference{ 38 | Name: "my-kubegres-secret", 39 | }, 40 | }, 41 | }, 42 | } 43 | 44 | kubegresResource.Spec.Env = append(kubegresResource.Spec.Env, envVar) 45 | } 46 | -------------------------------------------------------------------------------- /internal/test/util/kindcluster/KindTestClusterUtil.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package kindcluster 22 | 23 | import ( 24 | "bytes" 25 | "log" 26 | "os" 27 | "os/exec" 28 | "path/filepath" 29 | "strings" 30 | ) 31 | 32 | const clusterName = "kubegres" 33 | 34 | type KindTestClusterUtil struct { 35 | kindExecPath string 36 | } 37 | 38 | func (r *KindTestClusterUtil) StartCluster() { 39 | 40 | log.Println("STARTING KIND CLUSTER '" + clusterName + "'") 41 | 42 | // start kind cluster 43 | var err error 44 | r.kindExecPath, err = exec.LookPath("kind") 45 | if err != nil { 46 | log.Fatal("We cannot find the executable 'kind'. " + 47 | "Make sure 'kind' is installed and the executable 'kind' " + 48 | "is in the classpath before running the tests.") 49 | } 50 | 51 | if r.isClusterRunning() { 52 | log.Println("Cluster is already running. No need to restart it.") 53 | r.installOperator() 54 | return 55 | } 56 | 57 | clusterConfigFilePath := r.getClusterConfigFilePath() 58 | 59 | log.Println("Running 'kind create cluster --name " + clusterName + " --config " + clusterConfigFilePath + "'") 60 | 61 | var out bytes.Buffer 62 | cmdStartCluster := &exec.Cmd{ 63 | Path: r.kindExecPath, 64 | Args: []string{r.kindExecPath, "create", "cluster", "--name", clusterName, "--config", clusterConfigFilePath}, 65 | Stdout: &out, 66 | Stderr: os.Stdout, 67 | } 68 | 69 | err = cmdStartCluster.Run() 70 | if err != nil { 71 | log.Fatal("Unable to execute the command 'kind create cluster --name "+clusterName+" --config "+clusterConfigFilePath+"'", err) 72 | } else { 73 | log.Println("CLUSTER STARTED") 74 | r.installOperator() 75 | } 76 | } 77 | 78 | func (r *KindTestClusterUtil) DeleteCluster() bool { 79 | 80 | log.Println("DELETING KIND CLUSTER '" + clusterName + "'") 81 | 82 | log.Println("Running 'kind delete cluster --name " + clusterName + "'") 83 | 84 | var out bytes.Buffer 85 | cmdDeleteCluster := &exec.Cmd{ 86 | Path: r.kindExecPath, 87 | Args: []string{r.kindExecPath, "delete", "cluster", "--name", clusterName}, 88 | Stdout: &out, 89 | Stderr: os.Stdout, 90 | } 91 | 92 | err := cmdDeleteCluster.Run() 93 | if err != nil { 94 | log.Fatal("Unable to execute the command 'kind delete cluster --name "+clusterName+"'", err) 95 | return false 96 | } 97 | 98 | return true 99 | } 100 | 101 | func (r *KindTestClusterUtil) isClusterRunning() bool { 102 | 103 | var out bytes.Buffer 104 | cmdGetClusters := &exec.Cmd{ 105 | Path: r.kindExecPath, 106 | Args: []string{r.kindExecPath, "get", "clusters"}, 107 | Stdout: &out, 108 | Stderr: os.Stdout, 109 | } 110 | 111 | err := cmdGetClusters.Run() 112 | if err != nil { 113 | log.Fatal("Unable to execute the command 'kind get clusters'", err) 114 | } 115 | 116 | outputLines := strings.Split(out.String(), "\n") 117 | for _, cluster := range outputLines { 118 | if cluster == clusterName { 119 | return true 120 | } 121 | log.Println("cluster: '" + cluster + "'") 122 | } 123 | return false 124 | } 125 | 126 | func (r *KindTestClusterUtil) installOperator() bool { 127 | 128 | log.Println("Installing Kubegres operator in Cluster") 129 | 130 | makeFilePath := r.getMakeFilePath() 131 | makeFileFolder := r.getMakeFileFolder(makeFilePath) 132 | log.Println("Running 'make install -f " + makeFilePath + " -C " + makeFileFolder + "'") 133 | 134 | // -C /home/alex/source/kubegres 135 | 136 | makeExecPath, err := exec.LookPath("make") 137 | if err != nil { 138 | log.Fatal("We cannot find the executable 'make'. " + 139 | "Make sure 'make' is installed and the executable 'make' " + 140 | "is in the classpath before running the tests.") 141 | } 142 | 143 | var out bytes.Buffer 144 | cmdMakeInstallClusters := &exec.Cmd{ 145 | Path: makeExecPath, 146 | Args: []string{makeExecPath, "install", "-f", makeFilePath, "-C", makeFileFolder}, 147 | Stdout: &out, 148 | Stderr: os.Stdout, 149 | } 150 | 151 | err = cmdMakeInstallClusters.Run() 152 | if err != nil { 153 | log.Fatal("Unable to execute the command 'make install -f "+makeFilePath+" -C "+makeFileFolder+"'", err) 154 | return false 155 | } 156 | 157 | return true 158 | } 159 | 160 | func (r *KindTestClusterUtil) getClusterConfigFilePath() string { 161 | fileAbsolutePath, err := filepath.Abs("util/kindcluster/kind-cluster-config.yaml") 162 | if err != nil { 163 | log.Fatal("Error while trying to get the absolute-path of 'kind-cluster-config.yaml': ", err) 164 | } 165 | return fileAbsolutePath 166 | } 167 | 168 | func (r *KindTestClusterUtil) getMakeFilePath() string { 169 | fileAbsolutePath, err := filepath.Abs("../../Makefile") 170 | if err != nil { 171 | log.Fatal("Error while trying to get the absolute-path of 'MakeFile': ", err) 172 | } 173 | return fileAbsolutePath 174 | } 175 | 176 | func (r *KindTestClusterUtil) getMakeFileFolder(makeFilePath string) string { 177 | return filepath.Dir(makeFilePath) 178 | } 179 | -------------------------------------------------------------------------------- /internal/test/util/kindcluster/createCluster.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | clusterName="kubegres" 4 | 5 | kind get clusters | grep clusterName && doesClusterExist=1 || doesClusterExist=0 6 | 7 | if [ doesClusterExist == 0 ]; then 8 | 9 | kind create cluster --name clusterName --config "+clusterConfigFilePath+"' 10 | 11 | fi 12 | 13 | 14 | -------------------------------------------------------------------------------- /internal/test/util/kindcluster/kind-cluster-config.yaml: -------------------------------------------------------------------------------- 1 | # 5 nodes (4 workers) cluster config 2 | kind: Cluster 3 | apiVersion: kind.x-k8s.io/v1alpha4 4 | nodes: 5 | - role: control-plane 6 | extraPortMappings: 7 | - containerPort: 30007 8 | hostPort: 30007 9 | protocol: TCP 10 | - containerPort: 30008 11 | hostPort: 30008 12 | protocol: TCP 13 | - role: worker 14 | - role: worker 15 | - role: worker 16 | - role: worker 17 | -------------------------------------------------------------------------------- /internal/test/util/testcases/DbQueryTestCases.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2023 Reactive Tech Limited. 3 | "Reactive Tech Limited" is a company located in England, United Kingdom. 4 | https://www.reactive-tech.io 5 | 6 | Lead Developer: Alex Arica 7 | 8 | Licensed under the Apache License, Version 2.0 (the "License"); 9 | you may not use this file except in compliance with the License. 10 | You may obtain a copy of the License at 11 | 12 | http://www.apache.org/licenses/LICENSE-2.0 13 | 14 | Unless required by applicable law or agreed to in writing, software 15 | distributed under the License is distributed on an "AS IS" BASIS, 16 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 17 | See the License for the specific language governing permissions and 18 | limitations under the License. 19 | */ 20 | 21 | package testcases 22 | 23 | import ( 24 | . "github.com/onsi/gomega" 25 | "log" 26 | "reactive-tech.io/kubegres/internal/test/resourceConfigs" 27 | util2 "reactive-tech.io/kubegres/internal/test/util" 28 | ) 29 | 30 | type DbQueryTestCases struct { 31 | connectionPrimaryDb util2.DbConnectionDbUtil 32 | connectionReplicaDb util2.DbConnectionDbUtil 33 | } 34 | 35 | func InitDbQueryTestCases(resourceCreator util2.TestResourceCreator, kubegresName string) DbQueryTestCases { 36 | return InitDbQueryTestCasesWithNodePorts(resourceCreator, kubegresName, resourceConfigs.ServiceToSqlQueryPrimaryDbNodePort, resourceConfigs.ServiceToSqlQueryReplicaDbNodePort) 37 | } 38 | 39 | func InitDbQueryTestCasesWithNodePorts(resourceCreator util2.TestResourceCreator, kubegresName string, primaryServiceNodePort, replicaServiceNodePort int) DbQueryTestCases { 40 | return DbQueryTestCases{ 41 | connectionPrimaryDb: util2.InitDbConnectionDbUtil(resourceCreator, kubegresName, primaryServiceNodePort, true), 42 | connectionReplicaDb: util2.InitDbConnectionDbUtil(resourceCreator, kubegresName, replicaServiceNodePort, false), 43 | } 44 | } 45 | 46 | func (r *DbQueryTestCases) ThenWeCanSqlQueryPrimaryDb() { 47 | 48 | Eventually(func() bool { 49 | 50 | isInserted := r.connectionPrimaryDb.InsertUser() 51 | if !isInserted { 52 | return false 53 | } 54 | 55 | users := r.connectionPrimaryDb.GetUsers() 56 | r.connectionPrimaryDb.Close() 57 | 58 | if r.connectionPrimaryDb.LastInsertedUserId != "" { 59 | if !r.isLastInsertedUserInDb(users) { 60 | log.Println("Does not contain the last inserted userId: '" + r.connectionPrimaryDb.LastInsertedUserId + "'") 61 | return false 62 | } 63 | } 64 | 65 | return len(users) == r.connectionPrimaryDb.NbreInsertedUsers 66 | 67 | }, resourceConfigs.TestTimeout, resourceConfigs.TestRetryInterval).Should(BeTrue()) 68 | } 69 | 70 | func (r *DbQueryTestCases) ThenWeCanSqlQueryReplicaDb() { 71 | Eventually(func() bool { 72 | users := r.connectionReplicaDb.GetUsers() 73 | r.connectionReplicaDb.Close() 74 | return len(users) == r.connectionPrimaryDb.NbreInsertedUsers 75 | 76 | }, resourceConfigs.TestTimeout, resourceConfigs.TestRetryInterval).Should(BeTrue()) 77 | } 78 | 79 | func (r *DbQueryTestCases) isLastInsertedUserInDb(users []util2.AccountUser) bool { 80 | for _, user := range users { 81 | if user.UserId == r.connectionPrimaryDb.LastInsertedUserId { 82 | return true 83 | } 84 | } 85 | return false 86 | } 87 | --------------------------------------------------------------------------------