├── _config.yml ├── config ├── manager │ ├── kustomization.yaml │ └── manager.yaml ├── prometheus │ ├── kustomization.yaml │ └── monitor.yaml ├── scorecard │ ├── bases │ │ └── config.yaml │ ├── patches │ │ ├── basic.config.yaml │ │ └── olm.config.yaml │ └── kustomization.yaml ├── default │ ├── manager_config_patch.yaml │ ├── manager_auth_proxy_patch.yaml │ └── kustomization.yaml ├── samples │ ├── engineconfiguration │ │ ├── simple.yaml │ │ ├── uriargs.yaml │ │ ├── aws-rds.yaml │ │ ├── engineconfigurationsecret.yaml │ │ └── full-example.yaml │ ├── database │ │ ├── simple.yaml │ │ ├── drop-on-delete.yaml │ │ ├── schemas.yaml │ │ ├── extensions.yaml │ │ └── full.yaml │ ├── userrole │ │ ├── provided-simple-secret.yaml │ │ ├── managed-simple-rotation.yaml │ │ ├── managed-simple.yaml │ │ └── provided-simple.yaml │ ├── engineconfigurationsecret.yaml │ ├── kustomization.yaml │ ├── postgresql_v1alpha1_postgresqlengineconfiguration.yaml │ ├── postgresql_v1alpha1_postgresqluserrole.yaml │ ├── postgresql_v1alpha1_postgresqlpublication.yaml │ └── postgresql_v1alpha1_postgresqldatabase.yaml ├── crd │ ├── patches │ │ ├── cainjection_in_postgresql_postgresqldatabases.yaml │ │ ├── cainjection_in_postgresql_postgresqlpublications.yaml │ │ ├── cainjection_in_postgresql_postgresqluserroles.yaml │ │ ├── cainjection_in_postgresql_postgresqlengineconfigurations.yaml │ │ ├── webhook_in_postgresql_postgresqldatabases.yaml │ │ ├── webhook_in_postgresql_postgresqluserroles.yaml │ │ ├── webhook_in_postgresql_postgresqlpublications.yaml │ │ └── webhook_in_postgresql_postgresqlengineconfigurations.yaml │ ├── kustomizeconfig.yaml │ ├── kustomization.yaml │ └── bases │ │ ├── postgresql.easymile.com_postgresqldatabases.yaml │ │ ├── postgresql.easymile.com_postgresqlpublications.yaml │ │ └── postgresql.easymile.com_postgresqluserroles.yaml ├── rbac │ ├── service_account.yaml │ ├── postgresql_postgresqluser_viewer_role.yaml │ ├── postgresql_postgresqldatabase_viewer_role.yaml │ ├── postgresql_postgresqluserrole_viewer_role.yaml │ ├── auth_proxy_client_clusterrole.yaml │ ├── postgresql_postgresqluser_editor_role.yaml │ ├── postgresql_postgresqlengineconfiguration_viewer_role.yaml │ ├── postgresql_postgresqldatabase_editor_role.yaml │ ├── postgresql_postgresqluserrole_editor_role.yaml │ ├── postgresql_postgresqlengineconfiguration_editor_role.yaml │ ├── role_binding.yaml │ ├── auth_proxy_role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── auth_proxy_role.yaml │ ├── auth_proxy_service.yaml │ ├── kustomization.yaml │ ├── postgresql_postgresqlpublication_viewer_role.yaml │ ├── postgresql_postgresqlpublication_editor_role.yaml │ ├── leader_election_role.yaml │ └── role.yaml └── manifests │ └── kustomization.yaml ├── docs ├── README.md └── how-to │ ├── setup-local.md │ └── make-release.md ├── internal └── controller │ ├── config │ └── config.go │ ├── utils │ ├── random.go │ └── utils.go │ └── postgresql │ └── postgres │ ├── azure.go │ ├── create-publication-builder.go │ ├── update-publication-builder.go │ ├── aws.go │ ├── pool_manager.go │ └── postgres.go ├── .dockerignore ├── helm └── postgresql-operator │ ├── Chart.yaml │ ├── templates │ ├── service_account.yaml │ ├── prometheus_rules.yaml │ ├── service.yaml │ ├── role_binding.yaml │ ├── leader_election_role_binding.yaml │ ├── leader_election_role.yaml │ ├── grafana_configmap.yaml │ ├── service_monitor.yaml │ ├── _helpers.tpl │ ├── role.yaml │ └── deployment.yaml │ ├── .helmignore │ ├── values.yaml │ ├── files │ └── dashboards │ │ └── custom-metrics-dashboard.json │ └── crds │ ├── postgresql.easymile.com_postgresqldatabases.yaml │ └── postgresql.easymile.com_postgresqlpublications.yaml ├── .github ├── workflows │ ├── labeler.yml │ ├── size.yml │ ├── stale.yml │ └── ci.yml ├── labeler.yml ├── semantic.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md └── CODE_OF_CONDUCT.md ├── api └── postgresql │ ├── common │ └── common.go │ └── v1alpha1 │ ├── groupversion_info.go │ ├── postgresqldatabase_types.go │ ├── postgresqlpublication_types.go │ ├── postgresqlengineconfiguration_types.go │ └── postgresqluserrole_types.go ├── .gitignore ├── hack └── boilerplate.go.txt ├── .vscode └── launch.json ├── grafana └── custom-metrics │ ├── config.yaml │ └── custom-metrics-dashboard.json ├── LICENSE ├── Dockerfile ├── PROJECT ├── CONTRIBUTING.md ├── go.mod ├── CHANGELOG.md └── README.md /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-cayman -------------------------------------------------------------------------------- /config/manager/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - manager.yaml 3 | -------------------------------------------------------------------------------- /config/prometheus/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - monitor.yaml 3 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Summary 2 | 3 | - [CRDs documentation](crds/) 4 | - [How to ?](how-to/) 5 | -------------------------------------------------------------------------------- /internal/controller/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | const Finalizer = "finalizer.postgresql.easymile.com" 4 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | # More info: https://docs.docker.com/engine/reference/builder/#dockerignore-file 2 | # Ignore build and test binaries. 3 | bin/ 4 | testbin/ 5 | -------------------------------------------------------------------------------- /helm/postgresql-operator/Chart.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | appVersion: "3.4.0" 3 | description: A Helm chart for Kubernetes 4 | name: postgresql-operator 5 | version: 0.3.0 6 | -------------------------------------------------------------------------------- /config/scorecard/bases/config.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: scorecard.operatorframework.io/v1alpha3 2 | kind: Configuration 3 | metadata: 4 | name: config 5 | stages: 6 | - parallel: true 7 | tests: [] 8 | -------------------------------------------------------------------------------- /config/default/manager_config_patch.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: controller-manager 5 | namespace: system 6 | spec: 7 | template: 8 | spec: 9 | containers: 10 | - name: manager 11 | -------------------------------------------------------------------------------- /.github/workflows/labeler.yml: -------------------------------------------------------------------------------- 1 | name: "Pull Request Labeler" 2 | on: 3 | - pull_request 4 | 5 | jobs: 6 | triage: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/labeler@v4 10 | with: 11 | repo-token: "${{ secrets.GITHUB_TOKEN }}" 12 | -------------------------------------------------------------------------------- /.github/workflows/size.yml: -------------------------------------------------------------------------------- 1 | name: "Add size label" 2 | on: pull_request 3 | jobs: 4 | size-label: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - name: size-label 8 | uses: pascalgn/size-label-action@v0.4.3 9 | env: 10 | GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}" 11 | -------------------------------------------------------------------------------- /api/postgresql/common/common.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | type CRLink struct { 4 | // Custom resource name 5 | // +required 6 | // +kubebuilder:validation:Required 7 | Name string `json:"name"` 8 | // Custom resource namespace 9 | // +optional 10 | Namespace string `json:"namespace,omitempty"` 11 | } 12 | -------------------------------------------------------------------------------- /config/scorecard/patches/basic.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - basic-check-spec 7 | image: quay.io/operator-framework/scorecard-test:v1.33.0 8 | labels: 9 | suite: basic 10 | test: basic-check-spec-test 11 | -------------------------------------------------------------------------------- /.github/labeler.yml: -------------------------------------------------------------------------------- 1 | # Add dockerfile label 2 | dockerfile: 3 | - build/Dockerfile 4 | 5 | # Add test label 6 | tests: 7 | - pkg/**/*_test.go 8 | 9 | actions: 10 | - .github/workflows/** 11 | - .github/labeler.yml 12 | 13 | ci: 14 | - .travis.yml 15 | 16 | documentation: 17 | - "**/*.md" 18 | - "*.md" 19 | -------------------------------------------------------------------------------- /config/samples/engineconfiguration/simple.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: postgresql.easymile.com/v1alpha1 2 | kind: PostgresqlEngineConfiguration 3 | metadata: 4 | name: simple 5 | spec: 6 | # PostgreSQL Hostname 7 | host: postgres 8 | # Secret name in the current namespace to find "user" and "password" 9 | secretName: pgenginesecrets 10 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_postgresql_postgresqldatabases.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: postgresqldatabases.postgresql.easymile.com 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_postgresql_postgresqlpublications.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: CERTIFICATE_NAMESPACE/CERTIFICATE_NAME 7 | name: postgresqlpublications.postgresql.easymile.com 8 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_postgresql_postgresqluserroles.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: postgresqluserroles.postgresql.easymile.com 8 | -------------------------------------------------------------------------------- /helm/postgresql-operator/templates/service_account.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.serviceAccount.create }} 2 | apiVersion: v1 3 | kind: ServiceAccount 4 | metadata: 5 | name: {{ template "postgresql-operator.serviceAccountName" . }} 6 | labels: 7 | {{ include "postgresql-operator.labels" . | indent 4 }} 8 | annotations: 9 | {{- toYaml .Values.serviceAccount.annotations | nindent 4 }} 10 | {{- end }} 11 | -------------------------------------------------------------------------------- /config/crd/patches/cainjection_in_postgresql_postgresqlengineconfigurations.yaml: -------------------------------------------------------------------------------- 1 | # The following patch adds a directive for certmanager to inject CA into the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME) 7 | name: postgresqlengineconfigurations.postgresql.easymile.com 8 | -------------------------------------------------------------------------------- /config/samples/database/simple.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: postgresql.easymile.com/v1alpha1 2 | kind: PostgresqlDatabase 3 | metadata: 4 | name: simple 5 | spec: 6 | # Engine configuration link 7 | engineConfiguration: 8 | # Resource name 9 | name: simple 10 | # Resource namespace 11 | # Will use resource namespace if not set 12 | # namespace: 13 | # Database name 14 | database: databasename 15 | -------------------------------------------------------------------------------- /config/samples/engineconfiguration/uriargs.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: postgresql.easymile.com/v1alpha1 2 | kind: PostgresqlEngineConfiguration 3 | metadata: 4 | name: uriargs 5 | spec: 6 | # PostgreSQL Hostname 7 | host: postgres 8 | # Secret name in the current namespace to find "user" and "password" 9 | secretName: pgenginesecrets 10 | # URI args to add for PostgreSQL URL 11 | uriArgs: sslmode=disabled 12 | -------------------------------------------------------------------------------- /config/samples/engineconfiguration/aws-rds.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: postgresql.easymile.com/v1alpha1 2 | kind: PostgresqlEngineConfiguration 3 | metadata: 4 | name: aws-rds 5 | spec: 6 | # Provider type 7 | provider: AWS 8 | # PostgreSQL Hostname 9 | host: super-pg.cpenn4cuat8g.us-east-1.rds.amazonaws.com 10 | # Secret name in the current namespace to find "user" and "password" 11 | secretName: pgenginesecrets 12 | -------------------------------------------------------------------------------- /config/samples/userrole/provided-simple-secret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | type: Opaque 4 | metadata: 5 | name: provided-simple 6 | data: 7 | # User name for connection to PostgreSQL engine 8 | # Here it is "fake" according to the dev examples 9 | USERNAME: ZmFrZQ== 10 | # Password for connection to PostgreSQL engine 11 | # Here it is "fake" according to the dev examples 12 | PASSWORD: ZmFrZQ== 13 | -------------------------------------------------------------------------------- /config/samples/engineconfigurationsecret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | type: Opaque 4 | metadata: 5 | name: pgenginesecrets 6 | data: 7 | # User name for connection to PostgreSQL engine 8 | # Here it is "postgres" according to the dev examples 9 | user: cG9zdGdyZXM= 10 | # Password for connection to PostgreSQL engine 11 | # Here it is "postgres" according to the dev examples 12 | password: cG9zdGdyZXM= 13 | -------------------------------------------------------------------------------- /config/samples/engineconfiguration/engineconfigurationsecret.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Secret 3 | type: Opaque 4 | metadata: 5 | name: pgenginesecrets 6 | data: 7 | # User name for connection to PostgreSQL engine 8 | # Here it is "postgres" according to the dev examples 9 | user: cG9zdGdyZXM= 10 | # Password for connection to PostgreSQL engine 11 | # Here it is "postgres" according to the dev examples 12 | password: cG9zdGdyZXM= 13 | -------------------------------------------------------------------------------- /config/rbac/service_account.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ServiceAccount 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: serviceaccount 6 | app.kubernetes.io/instance: controller-manager-sa 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: postgresql-operator 9 | app.kubernetes.io/part-of: postgresql-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: controller-manager 12 | namespace: system 13 | -------------------------------------------------------------------------------- /config/samples/kustomization.yaml: -------------------------------------------------------------------------------- 1 | ## Append samples you want in your CSV to this file as resources ## 2 | resources: 3 | - postgresql_v1alpha1_postgresqlengineconfiguration.yaml 4 | - postgresql_v1alpha1_postgresqldatabase.yaml 5 | - postgresql_v1alpha1_postgresqluser.yaml 6 | - postgresql_v1alpha2_postgresqluser.yaml 7 | - postgresql_v1alpha1_postgresqluserrole.yaml 8 | - postgresql_v1alpha1_postgresqlpublication.yaml 9 | #+kubebuilder:scaffold:manifestskustomizesamples 10 | -------------------------------------------------------------------------------- /helm/postgresql-operator/.helmignore: -------------------------------------------------------------------------------- 1 | # Patterns to ignore when building packages. 2 | # This supports shell glob matching, relative path matching, and 3 | # negation (prefixed with !). Only one pattern per line. 4 | .DS_Store 5 | # Common VCS dirs 6 | .git/ 7 | .gitignore 8 | .bzr/ 9 | .bzrignore 10 | .hg/ 11 | .hgignore 12 | .svn/ 13 | # Common backup files 14 | *.swp 15 | *.bak 16 | *.tmp 17 | *~ 18 | # Various IDEs 19 | .project 20 | .idea/ 21 | *.tmproj 22 | .vscode/ 23 | -------------------------------------------------------------------------------- /config/scorecard/kustomization.yaml: -------------------------------------------------------------------------------- 1 | resources: 2 | - bases/config.yaml 3 | patchesJson6902: 4 | - path: patches/basic.config.yaml 5 | target: 6 | group: scorecard.operatorframework.io 7 | version: v1alpha3 8 | kind: Configuration 9 | name: config 10 | - path: patches/olm.config.yaml 11 | target: 12 | group: scorecard.operatorframework.io 13 | version: v1alpha3 14 | kind: Configuration 15 | name: config 16 | #+kubebuilder:scaffold:patchesJson6902 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | bin 8 | testbin/* 9 | 10 | # Test binary, build with `go test -c` 11 | *.test 12 | 13 | # Output of the go coverage tool, specifically when used with LiteIDE 14 | *.out 15 | 16 | # Kubernetes Generated files - skip generated files, except for vendored files 17 | 18 | !vendor/**/zz_generated.* 19 | 20 | # editor and IDE paraphernalia 21 | .idea 22 | *.swp 23 | *.swo 24 | *~ 25 | 26 | coverage.html 27 | -------------------------------------------------------------------------------- /config/rbac/postgresql_postgresqluser_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view postgresqlusers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: postgresqluser-viewer-role 6 | rules: 7 | - apiGroups: 8 | - postgresql.easymile.com 9 | resources: 10 | - postgresqlusers 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - postgresql.easymile.com 17 | resources: 18 | - postgresqlusers/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/samples/database/drop-on-delete.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: postgresql.easymile.com/v1alpha1 2 | kind: PostgresqlDatabase 3 | metadata: 4 | name: drop-on-delete 5 | spec: 6 | # Engine configuration link 7 | engineConfiguration: 8 | # Resource name 9 | name: simple 10 | # Resource namespace 11 | # Will use resource namespace if not set 12 | # namespace: 13 | # Database name 14 | database: databasename 15 | # Should drop on delete ? 16 | # Default set to false 17 | dropOnDelete: true 18 | -------------------------------------------------------------------------------- /helm/postgresql-operator/templates/prometheus_rules.yaml: -------------------------------------------------------------------------------- 1 | {{ if .Values.prometheus.rules.enabled }} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: PrometheusRule 4 | metadata: 5 | name: {{ include "postgresql-operator.fullname" . }} 6 | labels: 7 | {{ include "postgresql-operator.labels" . | nindent 4 }} 8 | {{ if .Values.prometheus.rules.labels }} 9 | {{ toYaml .Values.prometheus.rules.labels | nindent 4 }} 10 | {{ end }} 11 | spec: 12 | {{ toYaml .Values.prometheus.rules.spec | nindent 2 }} 13 | {{ end }} 14 | -------------------------------------------------------------------------------- /config/rbac/postgresql_postgresqldatabase_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view postgresqldatabases. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: postgresqldatabase-viewer-role 6 | rules: 7 | - apiGroups: 8 | - postgresql.easymile.com 9 | resources: 10 | - postgresqldatabases 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - postgresql.easymile.com 17 | resources: 18 | - postgresqldatabases/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/postgresql_postgresqluserrole_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view postgresqluserroles. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: postgresqluserrole-viewer-role 6 | rules: 7 | - apiGroups: 8 | - postgresql.easymile.com 9 | resources: 10 | - postgresqluserroles 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - postgresql.easymile.com 17 | resources: 18 | - postgresqluserroles/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_postgresql_postgresqldatabases.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: postgresqldatabases.postgresql.easymile.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_postgresql_postgresqluserroles.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: postgresqluserroles.postgresql.easymile.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /helm/postgresql-operator/templates/service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | name: {{ include "postgresql-operator.fullname" . }} 5 | labels: 6 | {{- include "postgresql-operator.labels" . | nindent 4 }} 7 | spec: 8 | type: "ClusterIP" 9 | ports: 10 | - port: 8080 11 | targetPort: http-metrics 12 | protocol: TCP 13 | name: http-metrics 14 | selector: 15 | app.kubernetes.io/name: {{ include "postgresql-operator.name" . }} 16 | app.kubernetes.io/instance: {{ .Release.Name }} 17 | -------------------------------------------------------------------------------- /internal/controller/utils/random.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "math/rand" 5 | "time" 6 | ) 7 | 8 | var allowedPGCharaters = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890" 9 | 10 | var seededRand = rand.New(rand.NewSource(time.Now().UnixNano())) //nolint: gosec// math rand is enough 11 | 12 | func GetRandomString(length int) string { 13 | b := make([]byte, length) 14 | for i := range b { 15 | b[i] = allowedPGCharaters[seededRand.Intn(len(allowedPGCharaters))] 16 | } 17 | 18 | return string(b) 19 | } 20 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_postgresql_postgresqlpublications.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: postgresqlpublications.postgresql.easymile.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_client_clusterrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: metrics-reader 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: postgresql-operator 9 | app.kubernetes.io/part-of: postgresql-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: metrics-reader 12 | rules: 13 | - nonResourceURLs: 14 | - "/metrics" 15 | verbs: 16 | - get 17 | -------------------------------------------------------------------------------- /helm/postgresql-operator/templates/role_binding.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.create }} 2 | kind: ClusterRoleBinding 3 | apiVersion: rbac.authorization.k8s.io/v1 4 | metadata: 5 | name: {{ include "postgresql-operator.fullname" . }}-rolebinding 6 | roleRef: 7 | kind: ClusterRole 8 | name: {{ include "postgresql-operator.fullname" . }}-role 9 | apiGroup: rbac.authorization.k8s.io 10 | subjects: 11 | - kind: ServiceAccount 12 | name: {{ template "postgresql-operator.serviceAccountName" . }} 13 | namespace: {{ .Release.Namespace }} 14 | {{- end }} 15 | -------------------------------------------------------------------------------- /config/crd/patches/webhook_in_postgresql_postgresqlengineconfigurations.yaml: -------------------------------------------------------------------------------- 1 | # The following patch enables a conversion webhook for the CRD 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | name: postgresqlengineconfigurations.postgresql.easymile.com 6 | spec: 7 | conversion: 8 | strategy: Webhook 9 | webhook: 10 | clientConfig: 11 | service: 12 | namespace: system 13 | name: webhook-service 14 | path: /convert 15 | conversionReviewVersions: 16 | - v1 17 | -------------------------------------------------------------------------------- /docs/how-to/setup-local.md: -------------------------------------------------------------------------------- 1 | # How to setup your local environment for developing ? 2 | 3 | ## Minikube (optional) 4 | 5 | You should setup a Minikube for local development. 6 | 7 | Download it if you haven't installed. 8 | 9 | Command to launch it: 10 | 11 | ```bash 12 | minikube start --kubernetes-version=v1.16.0 13 | ``` 14 | 15 | ## Install local environment 16 | 17 | - You should install CRDs first 18 | ```bash 19 | kubectl apply -f ./deploy/crds/ 20 | ``` 21 | - Install dev resources 22 | ```bash 23 | kubectl apply -f ./deploy/dev/ 24 | ``` 25 | -------------------------------------------------------------------------------- /config/rbac/postgresql_postgresqluser_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit postgresqlusers. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: postgresqluser-editor-role 6 | rules: 7 | - apiGroups: 8 | - postgresql.easymile.com 9 | resources: 10 | - postgresqlusers 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - postgresql.easymile.com 21 | resources: 22 | - postgresqlusers/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/postgresql_postgresqlengineconfiguration_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view postgresqlengineconfigurations. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: postgresqlengineconfiguration-viewer-role 6 | rules: 7 | - apiGroups: 8 | - postgresql.easymile.com 9 | resources: 10 | - postgresqlengineconfigurations 11 | verbs: 12 | - get 13 | - list 14 | - watch 15 | - apiGroups: 16 | - postgresql.easymile.com 17 | resources: 18 | - postgresqlengineconfigurations/status 19 | verbs: 20 | - get 21 | -------------------------------------------------------------------------------- /config/rbac/postgresql_postgresqldatabase_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit postgresqldatabases. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: postgresqldatabase-editor-role 6 | rules: 7 | - apiGroups: 8 | - postgresql.easymile.com 9 | resources: 10 | - postgresqldatabases 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - postgresql.easymile.com 21 | resources: 22 | - postgresqldatabases/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /config/rbac/postgresql_postgresqluserrole_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit postgresqluserroles. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: postgresqluserrole-editor-role 6 | rules: 7 | - apiGroups: 8 | - postgresql.easymile.com 9 | resources: 10 | - postgresqluserroles 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - postgresql.easymile.com 21 | resources: 22 | - postgresqluserroles/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /hack/boilerplate.go.txt: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 | */ -------------------------------------------------------------------------------- /config/rbac/postgresql_postgresqlengineconfiguration_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit postgresqlengineconfigurations. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: postgresqlengineconfiguration-editor-role 6 | rules: 7 | - apiGroups: 8 | - postgresql.easymile.com 9 | resources: 10 | - postgresqlengineconfigurations 11 | verbs: 12 | - create 13 | - delete 14 | - get 15 | - list 16 | - patch 17 | - update 18 | - watch 19 | - apiGroups: 20 | - postgresql.easymile.com 21 | resources: 22 | - postgresqlengineconfigurations/status 23 | verbs: 24 | - get 25 | -------------------------------------------------------------------------------- /helm/postgresql-operator/templates/leader_election_role_binding.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.create }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: RoleBinding 4 | metadata: 5 | name: {{ include "postgresql-operator.fullname" . }}-leader-election-rolebinding 6 | labels: 7 | {{ include "postgresql-operator.labels" . | indent 4 }} 8 | roleRef: 9 | apiGroup: rbac.authorization.k8s.io 10 | kind: Role 11 | name: {{ include "postgresql-operator.fullname" . }}-leader-election-role 12 | subjects: 13 | - kind: ServiceAccount 14 | name: {{ template "postgresql-operator.serviceAccountName" . }} 15 | namespace: {{ .Release.Namespace }} 16 | {{- end }} 17 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch", 9 | "type": "go", 10 | "request": "launch", 11 | "mode": "auto", 12 | "program": "${workspaceFolder}/cmd", 13 | "cwd": "${workspaceFolder}", 14 | "env": { 15 | "WATCH_NAMESPACE": "", 16 | "POD_NAME": "operator", 17 | "LOG_LEVEL": "debug" 18 | }, 19 | "args": [] 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /config/rbac/role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: manager-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: postgresql-operator 9 | app.kubernetes.io/part-of: postgresql-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: manager-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: manager-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /.github/semantic.yml: -------------------------------------------------------------------------------- 1 | # Always validate all commits, and ignore the PR title 2 | commitsOnly: true 3 | 4 | # List of valid scopes 5 | scopes: [] 6 | 7 | # By default types specified in commitizen/conventional-commit-types is used. 8 | # See: https://github.com/commitizen/conventional-commit-types/blob/v2.3.0/index.json 9 | types: 10 | - feat 11 | - fix 12 | - docs 13 | - style 14 | - refactor 15 | - test 16 | - ci 17 | - chore 18 | - revert 19 | 20 | # Allow use of Merge commits (eg on github: "Merge branch 'master' into feature/ride-unicorns") 21 | # this is only relevant when using commitsOnly: true (or titleAndCommits: true) 22 | allowMergeCommits: false 23 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role_binding.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRoleBinding 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrolebinding 6 | app.kubernetes.io/instance: proxy-rolebinding 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: postgresql-operator 9 | app.kubernetes.io/part-of: postgresql-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: ClusterRole 15 | name: proxy-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /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: rolebinding 6 | app.kubernetes.io/instance: leader-election-rolebinding 7 | app.kubernetes.io/component: rbac 8 | app.kubernetes.io/created-by: postgresql-operator 9 | app.kubernetes.io/part-of: postgresql-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: leader-election-rolebinding 12 | roleRef: 13 | apiGroup: rbac.authorization.k8s.io 14 | kind: Role 15 | name: leader-election-role 16 | subjects: 17 | - kind: ServiceAccount 18 | name: controller-manager 19 | namespace: system 20 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_role.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: rbac.authorization.k8s.io/v1 2 | kind: ClusterRole 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: clusterrole 6 | app.kubernetes.io/instance: proxy-role 7 | app.kubernetes.io/component: kube-rbac-proxy 8 | app.kubernetes.io/created-by: postgresql-operator 9 | app.kubernetes.io/part-of: postgresql-operator 10 | app.kubernetes.io/managed-by: kustomize 11 | name: proxy-role 12 | rules: 13 | - apiGroups: 14 | - authentication.k8s.io 15 | resources: 16 | - tokenreviews 17 | verbs: 18 | - create 19 | - apiGroups: 20 | - authorization.k8s.io 21 | resources: 22 | - subjectaccessreviews 23 | verbs: 24 | - create 25 | -------------------------------------------------------------------------------- /config/rbac/auth_proxy_service.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Service 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: service 7 | app.kubernetes.io/instance: controller-manager-metrics-service 8 | app.kubernetes.io/component: kube-rbac-proxy 9 | app.kubernetes.io/created-by: postgresql-operator 10 | app.kubernetes.io/part-of: postgresql-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: controller-manager-metrics-service 13 | namespace: system 14 | spec: 15 | ports: 16 | - name: https 17 | port: 8443 18 | protocol: TCP 19 | targetPort: https 20 | selector: 21 | control-plane: controller-manager 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | ## Description 10 | 11 | 12 | 13 | ## Expected Behavior 14 | 15 | 16 | 17 | ## Actual Behavior 18 | 19 | 20 | 21 | ## Environment 22 | 23 | - Kubernetes version: (E.g 1.12.0) 24 | - Project Version/Tag: (E.g 1.0.0) 25 | 26 | ## Steps to reproduce 27 | 28 | 29 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: enhancement 6 | assignees: "" 7 | --- 8 | 9 | ## Is your feature request related to a problem? Please describe. 10 | 11 | 12 | 13 | ## Describe the solution you'd like 14 | 15 | 16 | 17 | ## Describe alternatives you've considered 18 | 19 | 20 | 21 | ## Additional context 22 | 23 | 24 | -------------------------------------------------------------------------------- /grafana/custom-metrics/config.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | customMetrics: 3 | # - metric: # Raw custom metric (required) 4 | # type: # Metric type: counter/gauge/histogram (required) 5 | # expr: # Prom_ql for the metric (optional) 6 | # unit: # Unit of measurement, examples: s,none,bytes,percent,etc. (optional) 7 | # 8 | # 9 | # Example: 10 | # --- 11 | # customMetrics: 12 | # - metric: foo_bar 13 | # unit: none 14 | # type: histogram 15 | # expr: histogram_quantile(0.90, sum by(instance, le) (rate(foo_bar{job=\"$job\", namespace=\"$namespace\"}[5m]))) 16 | 17 | - metric: controller_runtime_reconcile_detailed_errors_total # Raw custom metric (required) 18 | type: counter # Metric type: counter/gauge/histogram (required) 19 | unit: none 20 | -------------------------------------------------------------------------------- /.github/workflows/stale.yml: -------------------------------------------------------------------------------- 1 | name: "Close stale issues" 2 | on: 3 | schedule: 4 | - cron: "0 0 * * *" 5 | 6 | jobs: 7 | stale: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/stale@v8 11 | with: 12 | repo-token: ${{ secrets.GITHUB_TOKEN }} 13 | stale-issue-message: "This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 10 days" 14 | days-before-stale: 30 15 | days-before-close: 10 16 | exempt-issue-labels: "awaiting-approval,work-in-progress,no-stale" 17 | exempt-pr-labels: "awaiting-approval,work-in-progress,no-stale" 18 | stale-issue-label: "no-issue-activity" 19 | stale-pr-label: "no-issue-activity" 20 | -------------------------------------------------------------------------------- /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 | # Comment the following 4 lines if you want to disable 13 | # the auth proxy (https://github.com/brancz/kube-rbac-proxy) 14 | # which protects your /metrics endpoint. 15 | - auth_proxy_service.yaml 16 | - auth_proxy_role.yaml 17 | - auth_proxy_role_binding.yaml 18 | - auth_proxy_client_clusterrole.yaml 19 | -------------------------------------------------------------------------------- /helm/postgresql-operator/templates/leader_election_role.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.create }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: Role 4 | metadata: 5 | name: {{ include "postgresql-operator.fullname" . }}-leader-election-role 6 | labels: 7 | {{ include "postgresql-operator.labels" . | indent 4 }} 8 | rules: 9 | - apiGroups: 10 | - "" 11 | resources: 12 | - configmaps 13 | verbs: 14 | - get 15 | - list 16 | - watch 17 | - create 18 | - update 19 | - patch 20 | - delete 21 | - apiGroups: 22 | - coordination.k8s.io 23 | resources: 24 | - leases 25 | verbs: 26 | - get 27 | - list 28 | - watch 29 | - create 30 | - update 31 | - patch 32 | - delete 33 | - apiGroups: 34 | - "" 35 | resources: 36 | - events 37 | verbs: 38 | - create 39 | - patch 40 | {{- end }} 41 | -------------------------------------------------------------------------------- /config/samples/database/schemas.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: postgresql.easymile.com/v1alpha1 2 | kind: PostgresqlDatabase 3 | metadata: 4 | name: schemas 5 | spec: 6 | # Engine configuration link 7 | engineConfiguration: 8 | # Resource name 9 | name: simple 10 | # Resource namespace 11 | # Will use resource namespace if not set 12 | # namespace: 13 | # Database name 14 | database: databasename 15 | # Schemas 16 | schemas: 17 | # List of schemas to enable 18 | list: 19 | - schema1 20 | # Should drop on delete ? 21 | # Default set to false 22 | # If set to false, removing from list won't delete schema from database 23 | dropOnDelete: true 24 | # Delete schema with cascade 25 | # Default set to false 26 | # For all elements that have used the deleted schema 27 | deleteWithCascade: true 28 | 29 | 30 | -------------------------------------------------------------------------------- /config/rbac/postgresql_postgresqlpublication_viewer_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to view postgresqlpublications. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: postgresqlpublication-viewer-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: postgresql-operator 10 | app.kubernetes.io/part-of: postgresql-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: postgresqlpublication-viewer-role 13 | rules: 14 | - apiGroups: 15 | - postgresql.easymile.com 16 | resources: 17 | - postgresqlpublications 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - apiGroups: 23 | - postgresql.easymile.com 24 | resources: 25 | - postgresqlpublications/status 26 | verbs: 27 | - get 28 | -------------------------------------------------------------------------------- /helm/postgresql-operator/templates/grafana_configmap.yaml: -------------------------------------------------------------------------------- 1 | {{- if .Values.grafanaDashboards.enabled }} 2 | {{- $dashboardFolderPath := "files/dashboards/" }} 3 | {{- range $path, $bytes := $.Files.Glob (printf "%s%s" $dashboardFolderPath "*") }} 4 | {{- $dashboardFileName := printf $path | replace $dashboardFolderPath "" }} 5 | {{- $dashboardName := printf $dashboardFileName | replace ".json" "" }} 6 | apiVersion: v1 7 | kind: ConfigMap 8 | metadata: 9 | name: {{ printf "grafana-%s" $dashboardName }} 10 | labels: 11 | {{ include "postgresql-operator.labels" $ | nindent 4 }} 12 | {{ toYaml $.Values.grafanaDashboards.labels | nindent 4 }} 13 | annotations: 14 | {{ toYaml $.Values.grafanaDashboards.annotations | nindent 4 }} 15 | data: 16 | {{ print $dashboardFileName | indent 2 }}: {{ $.Files.Get $path | toJson| indent 4 }} 17 | --- 18 | {{- end }} 19 | {{- end }} 20 | 21 | -------------------------------------------------------------------------------- /config/samples/database/extensions.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: postgresql.easymile.com/v1alpha1 2 | kind: PostgresqlDatabase 3 | metadata: 4 | name: extensions 5 | spec: 6 | # Engine configuration link 7 | engineConfiguration: 8 | # Resource name 9 | name: simple 10 | # Resource namespace 11 | # Will use resource namespace if not set 12 | # namespace: 13 | # Database name 14 | database: databasename 15 | # Extensions 16 | extensions: 17 | # List of extensions to enable 18 | list: 19 | - uuid-ossp 20 | # Should drop on delete ? 21 | # Default set to false 22 | # If set to false, removing from list won't delete extension from database 23 | dropOnDelete: true 24 | # Delete extension with cascade 25 | # Default set to false 26 | # For all elements that have used the deleted extension 27 | deleteWithCascade: true 28 | 29 | 30 | -------------------------------------------------------------------------------- /config/samples/postgresql_v1alpha1_postgresqlengineconfiguration.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: postgresql.easymile.com/v1alpha1 2 | kind: PostgresqlEngineConfiguration 3 | metadata: 4 | name: postgresqlengineconfiguration-sample 5 | spec: 6 | # Provider type 7 | # Default to "" 8 | provider: "" 9 | # PostgreSQL Hostname 10 | host: postgres 11 | # PostgreSQL Port 12 | # Default to 5432 13 | port: 5432 14 | # Secret name in the current namespace to find "user" and "password" 15 | secretName: pgenginesecrets 16 | # URI args to add for PostgreSQL URL 17 | # Default to "" 18 | uriArgs: sslmode=disabled 19 | # Default database name 20 | # Default to "postgres" 21 | defaultDatabase: postgres 22 | # Check interval 23 | # Default to 30s 24 | checkInterval: 30s 25 | # Wait for linked resource to be deleted 26 | # Default to false 27 | waitLinkedResourcesDeletion: true 28 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Pull request 3 | about: Pull request for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | ## Issue/Feature 10 | 11 | 12 | 13 | ## Additional Information 14 | 15 | 16 | 17 | ## Verification Steps 18 | 19 | 27 | 28 | ## Checklist: 29 | 30 | - [ ] Verified by team member 31 | - [ ] Comments where necessary 32 | - [ ] Documentation changes if necessary 33 | -------------------------------------------------------------------------------- /config/prometheus/monitor.yaml: -------------------------------------------------------------------------------- 1 | 2 | # Prometheus Monitor Service (Metrics) 3 | apiVersion: monitoring.coreos.com/v1 4 | kind: ServiceMonitor 5 | metadata: 6 | labels: 7 | control-plane: controller-manager 8 | app.kubernetes.io/name: servicemonitor 9 | app.kubernetes.io/instance: controller-manager-metrics-monitor 10 | app.kubernetes.io/component: metrics 11 | app.kubernetes.io/created-by: postgresql-operator 12 | app.kubernetes.io/part-of: postgresql-operator 13 | app.kubernetes.io/managed-by: kustomize 14 | name: controller-manager-metrics-monitor 15 | namespace: system 16 | spec: 17 | endpoints: 18 | - path: /metrics 19 | port: https 20 | scheme: https 21 | bearerTokenFile: /var/run/secrets/kubernetes.io/serviceaccount/token 22 | tlsConfig: 23 | insecureSkipVerify: true 24 | selector: 25 | matchLabels: 26 | control-plane: controller-manager 27 | -------------------------------------------------------------------------------- /docs/how-to/make-release.md: -------------------------------------------------------------------------------- 1 | # How to make a release ? 2 | 3 | ## Requirements 4 | 5 | - Be an administrator 6 | - The master branch must compile (`make build` and `make docker-build`) 7 | - The master branch must have linter ok (`make code/lint`) 8 | - The master branch must have tests ok (`make setup/services test`) 9 | 10 | ## Prepare the release 11 | 12 | - Make a branch from master 13 | - Look at the git log and find a good version following semver logic and git commit message types (feat, fix, BREAKING CHANGE, ...). 14 | - Run CRDs generation `make manifests generate` 15 | - Update Helm chart values for new version 16 | - Update `CHANGELOG.md` 17 | - Push all of this with commit message `chore: Release NEW_SEMVER_VERSION` 18 | 19 | ## Release 20 | 21 | - Create a pull request for all these changes 22 | - Once it is merged, put a tag on the commit previously created. In Github release, put the changelog section. 23 | -------------------------------------------------------------------------------- /config/rbac/postgresql_postgresqlpublication_editor_role.yaml: -------------------------------------------------------------------------------- 1 | # permissions for end users to edit postgresqlpublications. 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | labels: 6 | app.kubernetes.io/name: clusterrole 7 | app.kubernetes.io/instance: postgresqlpublication-editor-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: postgresql-operator 10 | app.kubernetes.io/part-of: postgresql-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: postgresqlpublication-editor-role 13 | rules: 14 | - apiGroups: 15 | - postgresql.easymile.com 16 | resources: 17 | - postgresqlpublications 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - postgresql.easymile.com 28 | resources: 29 | - postgresqlpublications/status 30 | verbs: 31 | - get 32 | -------------------------------------------------------------------------------- /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: role 7 | app.kubernetes.io/instance: leader-election-role 8 | app.kubernetes.io/component: rbac 9 | app.kubernetes.io/created-by: postgresql-operator 10 | app.kubernetes.io/part-of: postgresql-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: leader-election-role 13 | rules: 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - configmaps 18 | verbs: 19 | - get 20 | - list 21 | - watch 22 | - create 23 | - update 24 | - patch 25 | - delete 26 | - apiGroups: 27 | - coordination.k8s.io 28 | resources: 29 | - leases 30 | verbs: 31 | - get 32 | - list 33 | - watch 34 | - create 35 | - update 36 | - patch 37 | - delete 38 | - apiGroups: 39 | - "" 40 | resources: 41 | - events 42 | verbs: 43 | - create 44 | - patch 45 | -------------------------------------------------------------------------------- /config/samples/userrole/managed-simple-rotation.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: postgresql.easymile.com/v1alpha1 2 | kind: PostgresqlUserRole 3 | metadata: 4 | name: managed-simple-rotation 5 | spec: 6 | # Mode 7 | mode: MANAGED 8 | # Role prefix to be used for user created in database engine 9 | rolePrefix: "managed-simple" 10 | # User password rotation duration in order to roll user/password in secret 11 | userPasswordRotationDuration: 30s 12 | # Privileges 13 | privileges: 14 | - # Privilege for the selected database 15 | privilege: OWNER 16 | # Connection type to be used for secret generation (Can be set to BOUNCER if wanted and supported by engine configuration) 17 | connectionType: PRIMARY 18 | # Database link 19 | database: 20 | name: simple 21 | # Generated secret name with information for the selected database 22 | generatedSecretName: managed-simple-rotation 23 | # Extra connection URL Parameters 24 | extraConnectionUrlParameters: 25 | {} 26 | # param1: value1 27 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 EasyMile 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /config/default/manager_auth_proxy_patch.yaml: -------------------------------------------------------------------------------- 1 | # This patch inject a sidecar container which is a HTTP proxy for the 2 | # controller manager, it performs RBAC authorization against the Kubernetes API using SubjectAccessReviews. 3 | apiVersion: apps/v1 4 | kind: Deployment 5 | metadata: 6 | name: controller-manager 7 | namespace: system 8 | spec: 9 | template: 10 | spec: 11 | containers: 12 | - name: kube-rbac-proxy 13 | securityContext: 14 | allowPrivilegeEscalation: false 15 | capabilities: 16 | drop: 17 | - "ALL" 18 | image: gcr.io/kubebuilder/kube-rbac-proxy:v0.14.1 19 | args: 20 | - "--secure-listen-address=0.0.0.0:8443" 21 | - "--upstream=http://127.0.0.1:8080/" 22 | - "--logtostderr=true" 23 | - "--v=0" 24 | ports: 25 | - containerPort: 8443 26 | protocol: TCP 27 | name: https 28 | resources: 29 | limits: 30 | cpu: 500m 31 | memory: 128Mi 32 | requests: 33 | cpu: 5m 34 | memory: 64Mi 35 | - name: manager 36 | args: 37 | - "--health-probe-bind-address=:8081" 38 | - "--metrics-bind-address=127.0.0.1:8080" 39 | - "--leader-elect" 40 | -------------------------------------------------------------------------------- /config/manifests/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # These resources constitute the fully configured set of manifests 2 | # used to generate the 'manifests/' directory in a bundle. 3 | resources: 4 | - bases/postgresql-operator.clusterserviceversion.yaml 5 | - ../default 6 | - ../samples 7 | - ../scorecard 8 | 9 | # [WEBHOOK] To enable webhooks, uncomment all the sections with [WEBHOOK] prefix. 10 | # Do NOT uncomment sections with prefix [CERTMANAGER], as OLM does not support cert-manager. 11 | # These patches remove the unnecessary "cert" volume and its manager container volumeMount. 12 | #patchesJson6902: 13 | #- target: 14 | # group: apps 15 | # version: v1 16 | # kind: Deployment 17 | # name: controller-manager 18 | # namespace: system 19 | # patch: |- 20 | # # Remove the manager container's "cert" volumeMount, since OLM will create and mount a set of certs. 21 | # # Update the indices in this path if adding or removing containers/volumeMounts in the manager's Deployment. 22 | # - op: remove 23 | 24 | # path: /spec/template/spec/containers/0/volumeMounts/0 25 | # # Remove the "cert" volume, since OLM will create and mount a set of certs. 26 | # # Update the indices in this path if adding or removing volumes in the manager's Deployment. 27 | # - op: remove 28 | # path: /spec/template/spec/volumes/0 29 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Build the manager binary 2 | FROM golang:1.20 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 | -------------------------------------------------------------------------------- /api/postgresql/v1alpha1/groupversion_info.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 v1alpha1 contains API Schema definitions for the postgresql v1alpha1 API group 18 | // +kubebuilder:object:generate=true 19 | // +groupName=postgresql.easymile.com 20 | package v1alpha1 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: "postgresql.easymile.com", Version: "v1alpha1"} 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 | -------------------------------------------------------------------------------- /config/scorecard/patches/olm.config.yaml: -------------------------------------------------------------------------------- 1 | - op: add 2 | path: /stages/0/tests/- 3 | value: 4 | entrypoint: 5 | - scorecard-test 6 | - olm-bundle-validation 7 | image: quay.io/operator-framework/scorecard-test:v1.33.0 8 | labels: 9 | suite: olm 10 | test: olm-bundle-validation-test 11 | - op: add 12 | path: /stages/0/tests/- 13 | value: 14 | entrypoint: 15 | - scorecard-test 16 | - olm-crds-have-validation 17 | image: quay.io/operator-framework/scorecard-test:v1.33.0 18 | labels: 19 | suite: olm 20 | test: olm-crds-have-validation-test 21 | - op: add 22 | path: /stages/0/tests/- 23 | value: 24 | entrypoint: 25 | - scorecard-test 26 | - olm-crds-have-resources 27 | image: quay.io/operator-framework/scorecard-test:v1.33.0 28 | labels: 29 | suite: olm 30 | test: olm-crds-have-resources-test 31 | - op: add 32 | path: /stages/0/tests/- 33 | value: 34 | entrypoint: 35 | - scorecard-test 36 | - olm-spec-descriptors 37 | image: quay.io/operator-framework/scorecard-test:v1.33.0 38 | labels: 39 | suite: olm 40 | test: olm-spec-descriptors-test 41 | - op: add 42 | path: /stages/0/tests/- 43 | value: 44 | entrypoint: 45 | - scorecard-test 46 | - olm-status-descriptors 47 | image: quay.io/operator-framework/scorecard-test:v1.33.0 48 | labels: 49 | suite: olm 50 | test: olm-status-descriptors-test 51 | -------------------------------------------------------------------------------- /internal/controller/postgresql/postgres/azure.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "strings" 7 | ) 8 | 9 | type azurepg struct { 10 | serverName string 11 | pg 12 | } 13 | 14 | const MinUserSplit = 1 15 | 16 | func newAzurePG(postgres *pg) PG { 17 | splitUser := strings.Split(postgres.user, "@") 18 | serverName := "" 19 | 20 | if len(splitUser) > MinUserSplit { 21 | serverName = splitUser[1] 22 | } 23 | 24 | return &azurepg{ 25 | serverName, 26 | *postgres, 27 | } 28 | } 29 | 30 | func (azpg *azurepg) CreateUserRole(ctx context.Context, role, password string, attributes *RoleAttributes) (string, error) { 31 | returnedRole, err := azpg.pg.CreateUserRole(ctx, role, password, attributes) 32 | if err != nil { 33 | return "", err 34 | } 35 | 36 | return fmt.Sprintf("%s@%s", returnedRole, azpg.serverName), nil 37 | } 38 | 39 | func (azpg *azurepg) GetRoleForLogin(login string) string { 40 | splitUser := strings.Split(azpg.user, "@") 41 | if len(splitUser) > MinUserSplit { 42 | return splitUser[0] 43 | } 44 | 45 | return login 46 | } 47 | 48 | func (azpg *azurepg) CreateDB(ctx context.Context, dbname, role string) error { 49 | // Have to add the master role to the group role before we can transfer the database owner 50 | err := azpg.GrantRole(ctx, role, azpg.GetRoleForLogin(azpg.user), false) 51 | if err != nil { 52 | return err 53 | } 54 | 55 | return azpg.pg.CreateDB(ctx, dbname, role) 56 | } 57 | -------------------------------------------------------------------------------- /config/samples/postgresql_v1alpha1_postgresqluserrole.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: postgresql.easymile.com/v1alpha1 2 | kind: PostgresqlUserRole 3 | metadata: 4 | name: managed-simple-rotation 5 | spec: 6 | # Mode 7 | mode: MANAGED 8 | # Role prefix to be used for user created in database engine 9 | rolePrefix: "managed-simple" 10 | # User password rotation duration in order to roll user/password in secret 11 | userPasswordRotationDuration: 30s 12 | # Privileges 13 | privileges: 14 | - # Privilege for the selected database 15 | privilege: OWNER 16 | # Database link 17 | database: 18 | name: simple 19 | # Generated secret name with information for the selected database 20 | generatedSecretName: managed-simple-rotation 21 | # Role attributes 22 | # Note: Only attributes that aren't conflicting with operator are supported. 23 | roleAttributes: 24 | # REPLICATION attribute 25 | # Note: This can be either true, false or null (to ignore this parameter) 26 | replication: null # false / true for example 27 | # BYPASSRLS attribute 28 | # Note: This can be either true, false or null (to ignore this parameter) 29 | bypassRLS: null # false / true for example 30 | # CONNECTION LIMIT connlimit attribute 31 | # Note: This can be either -1, a number or null (to ignore this parameter) 32 | # Note: Increase your number by one because operator is using the created user to perform some operations. 33 | connectionLimit: null # 10 for example 34 | -------------------------------------------------------------------------------- /helm/postgresql-operator/templates/service_monitor.yaml: -------------------------------------------------------------------------------- 1 | {{ if .Values.prometheus.serviceMonitor.enabled }} 2 | apiVersion: monitoring.coreos.com/v1 3 | kind: ServiceMonitor 4 | metadata: 5 | name: {{ include "postgresql-operator.fullname" . }} 6 | labels: 7 | {{ include "postgresql-operator.labels" . | nindent 4 }} 8 | {{ if .Values.prometheus.serviceMonitor.labels }} 9 | {{ toYaml .Values.prometheus.serviceMonitor.labels | nindent 4 }} 10 | {{ end }} 11 | spec: 12 | endpoints: 13 | - path: /metrics 14 | port: http-metrics 15 | scheme: http 16 | {{- if .Values.prometheus.serviceMonitor.interval }} 17 | interval: {{ .Values.prometheus.serviceMonitor.interval }} 18 | {{- end }} 19 | {{- if .Values.prometheus.serviceMonitor.scrapeTimeout }} 20 | scrapeTimeout: {{ .Values.prometheus.serviceMonitor.scrapeTimeout }} 21 | {{- end }} 22 | {{- if .Values.prometheus.serviceMonitor.metricRelabelings }} 23 | metricRelabelings: 24 | {{ toYaml .Values.prometheus.serviceMonitor.metricRelabelings | nindent 8 }} 25 | {{- end }} 26 | {{- if .Values.prometheus.serviceMonitor.relabelings }} 27 | relabelings: 28 | {{ toYaml .Values.prometheus.serviceMonitor.relabelings | nindent 8 }} 29 | {{- end }} 30 | jobLabel: {{ include "postgresql-operator.fullname" . }} 31 | selector: 32 | matchLabels: 33 | app.kubernetes.io/name: {{ include "postgresql-operator.name" . }} 34 | app.kubernetes.io/instance: {{ .Release.Name }} 35 | {{ end }} -------------------------------------------------------------------------------- /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/postgresql.easymile.com_postgresqlengineconfigurations.yaml 6 | - bases/postgresql.easymile.com_postgresqldatabases.yaml 7 | - bases/postgresql.easymile.com_postgresqluserroles.yaml 8 | - bases/postgresql.easymile.com_postgresqlpublications.yaml 9 | #+kubebuilder:scaffold:crdkustomizeresource 10 | 11 | patchesStrategicMerge: 12 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix. 13 | # patches here are for enabling the conversion webhook for each CRD 14 | #- patches/webhook_in_postgresqlengineconfigurations.yaml 15 | #- patches/webhook_in_postgresqldatabases.yaml 16 | #- patches/webhook_in_postgresqluserroles.yaml 17 | #- path: patches/webhook_in_postgresqlpublications.yaml 18 | #+kubebuilder:scaffold:crdkustomizewebhookpatch 19 | 20 | # [CERTMANAGER] To enable cert-manager, uncomment all the sections with [CERTMANAGER] prefix. 21 | # patches here are for enabling the CA injection for each CRD 22 | #- patches/cainjection_in_postgresqlengineconfigurations.yaml 23 | #- patches/cainjection_in_postgresqldatabases.yaml 24 | #- patches/cainjection_in_postgresqluserroles.yaml 25 | #- path: patches/cainjection_in_postgresqlpublications.yaml 26 | #+kubebuilder:scaffold:crdkustomizecainjectionpatch 27 | 28 | # the following config is for teaching kustomize how to do kustomization for CRDs. 29 | configurations: 30 | - kustomizeconfig.yaml 31 | -------------------------------------------------------------------------------- /config/samples/userrole/managed-simple.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: postgresql.easymile.com/v1alpha1 2 | kind: PostgresqlUserRole 3 | metadata: 4 | name: managed-simple 5 | spec: 6 | # Mode 7 | mode: MANAGED 8 | # Role prefix to be used for user created in database engine 9 | rolePrefix: "managed-simple" 10 | # Privileges 11 | privileges: 12 | - # Privilege for the selected database 13 | privilege: OWNER 14 | # Connection type to be used for secret generation (Can be set to BOUNCER if wanted and supported by engine configuration) 15 | connectionType: PRIMARY 16 | # Database link 17 | database: 18 | name: simple 19 | # Generated secret name with information for the selected database 20 | generatedSecretName: managed-simple 21 | # Extra connection URL Parameters 22 | extraConnectionUrlParameters: 23 | {} 24 | # param1: value1 25 | # Role attributes 26 | # Note: Only attributes that aren't conflicting with operator are supported. 27 | roleAttributes: 28 | # REPLICATION attribute 29 | # Note: This can be either true, false or null (to ignore this parameter) 30 | replication: null # false / true for example 31 | # BYPASSRLS attribute 32 | # Note: This can be either true, false or null (to ignore this parameter) 33 | bypassRLS: null # false / true for example 34 | # CONNECTION LIMIT connlimit attribute 35 | # Note: This can be either -1, a number or null (to ignore this parameter) 36 | # Note: Increase your number by one because operator is using the created user to perform some operations. 37 | connectionLimit: null # 10 for example 38 | -------------------------------------------------------------------------------- /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: easymile.com 6 | layout: 7 | - go.kubebuilder.io/v4 8 | multigroup: true 9 | plugins: 10 | grafana.kubebuilder.io/v1-alpha: {} 11 | manifests.sdk.operatorframework.io/v2: {} 12 | scorecard.sdk.operatorframework.io/v2: {} 13 | projectName: postgresql-operator 14 | repo: github.com/easymile/postgresql-operator 15 | resources: 16 | - api: 17 | crdVersion: v1 18 | namespaced: true 19 | controller: true 20 | domain: easymile.com 21 | group: postgresql 22 | kind: PostgresqlEngineConfiguration 23 | path: github.com/easymile/postgresql-operator/apis/postgresql/v1alpha1 24 | version: v1alpha1 25 | - api: 26 | crdVersion: v1 27 | namespaced: true 28 | controller: true 29 | domain: easymile.com 30 | group: postgresql 31 | kind: PostgresqlDatabase 32 | path: github.com/easymile/postgresql-operator/apis/postgresql/v1alpha1 33 | version: v1alpha1 34 | - api: 35 | crdVersion: v1 36 | namespaced: true 37 | controller: true 38 | domain: easymile.com 39 | group: postgresql 40 | kind: PostgresqlUserRole 41 | path: github.com/easymile/postgresql-operator/apis/postgresql/v1alpha1 42 | version: v1alpha1 43 | - api: 44 | crdVersion: v1 45 | namespaced: true 46 | controller: true 47 | domain: easymile.com 48 | group: postgresql 49 | kind: PostgresqlPublication 50 | path: github.com/easymile/postgresql-operator/api/postgresql/v1alpha1 51 | version: v1alpha1 52 | version: "3" 53 | -------------------------------------------------------------------------------- /config/samples/userrole/provided-simple.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: postgresql.easymile.com/v1alpha1 2 | kind: PostgresqlUserRole 3 | metadata: 4 | name: postgresqluserrole-sample 5 | spec: 6 | # Mode 7 | mode: PROVIDED 8 | # Privileges list 9 | privileges: 10 | - # Privilege for the selected database 11 | privilege: WRITER 12 | # Connection type to be used for secret generation (Can be set to BOUNCER if wanted and supported by engine configuration) 13 | connectionType: PRIMARY 14 | # Database link 15 | database: 16 | name: simple 17 | # Generated secret name with information for the selected database 18 | generatedSecretName: simple1 19 | # Extra connection URL Parameters 20 | extraConnectionUrlParameters: 21 | {} 22 | # param1: value1 23 | # Import secret that will contain "USERNAME" and "PASSWORD" for provided mode 24 | importSecretName: provided-simple 25 | # Role attributes 26 | # Note: Only attributes that aren't conflicting with operator are supported. 27 | roleAttributes: 28 | # REPLICATION attribute 29 | # Note: This can be either true, false or null (to ignore this parameter) 30 | replication: null # false / true for example 31 | # BYPASSRLS attribute 32 | # Note: This can be either true, false or null (to ignore this parameter) 33 | bypassRLS: null # false / true for example 34 | # CONNECTION LIMIT connlimit attribute 35 | # Note: This can be either -1, a number or null (to ignore this parameter) 36 | # Note: Increase your number by one because operator is using the created user to perform some operations. 37 | connectionLimit: null # 10 for example 38 | -------------------------------------------------------------------------------- /config/samples/postgresql_v1alpha1_postgresqlpublication.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: postgresql.easymile.com/v1alpha1 2 | kind: PostgresqlPublication 3 | metadata: 4 | labels: 5 | app.kubernetes.io/name: postgresqlpublication 6 | app.kubernetes.io/instance: postgresqlpublication-sample 7 | app.kubernetes.io/part-of: postgresql-operator 8 | app.kubernetes.io/managed-by: kustomize 9 | app.kubernetes.io/created-by: postgresql-operator 10 | name: postgresqlpublication-sample 11 | spec: 12 | # Database custom resource reference 13 | database: 14 | name: postgresqldatabase-sample 15 | # Publication name in PostgreSQL 16 | name: my-publication 17 | # Drop on delete 18 | dropOnDelete: false 19 | # Enable publication for all tables in database 20 | allTables: false 21 | # Tables in schema to select for publication 22 | tablesInSchema: 23 | [] 24 | # - table1 25 | # - table2 26 | # Table selection for publication 27 | tables: 28 | - # Table name 29 | tableName: table1 30 | # Columns to select (Empty array will select all of them) 31 | columns: 32 | - id 33 | - created_at 34 | # - updated_at 35 | - number1 36 | # WHERE clause on selected table 37 | additionalWhere: number1 > 5 38 | # Publication with parameters 39 | withParameters: 40 | # Publish param 41 | # See here: https://www.postgresql.org/docs/current/sql-createpublication.html#SQL-CREATEPUBLICATION-PARAMS-WITH-PUBLISH 42 | # publish: 'TRUNCATE' 43 | # Publish via partition root param 44 | # See here: https://www.postgresql.org/docs/current/sql-createpublication.html#SQL-CREATEPUBLICATION-PARAMS-WITH-PUBLISH 45 | publishViaPartitionRoot: false 46 | -------------------------------------------------------------------------------- /config/samples/database/full.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: postgresql.easymile.com/v1alpha1 2 | kind: PostgresqlDatabase 3 | metadata: 4 | name: full 5 | spec: 6 | # Engine configuration link 7 | engineConfiguration: 8 | # Resource name 9 | name: simple 10 | # Resource namespace 11 | # Will use resource namespace if not set 12 | # namespace: 13 | # Database name 14 | database: databasename 15 | # Master role name 16 | # Master role name will be used to create top group role. 17 | # Database owner and users will be in this group role. 18 | # Default is "" 19 | masterRole: "" 20 | # Should drop on delete ? 21 | # Default set to false 22 | dropOnDelete: true 23 | # Wait for linked resource deletion to accept deletion of the current resource 24 | # See documentation for more information 25 | # Default set to false 26 | waitLinkedResourcesDeletion: true 27 | # Schemas 28 | schemas: 29 | # List of schemas to enable 30 | list: 31 | - schema1 32 | # Should drop on delete ? 33 | # Default set to false 34 | # If set to false, removing from list won't delete schema from database 35 | dropOnDelete: true 36 | # Delete schema with cascade 37 | # Default set to false 38 | # For all elements that have used the deleted schema 39 | deleteWithCascade: true 40 | # Extensions 41 | extensions: 42 | # List of extensions to enable 43 | list: 44 | - uuid-ossp 45 | # Should drop on delete ? 46 | # Default set to false 47 | # If set to false, removing from list won't delete extension from database 48 | dropOnDelete: true 49 | # Delete extension with cascade 50 | # Default set to false 51 | # For all elements that have used the deleted extension 52 | deleteWithCascade: true 53 | -------------------------------------------------------------------------------- /config/samples/postgresql_v1alpha1_postgresqldatabase.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: postgresql.easymile.com/v1alpha1 2 | kind: PostgresqlDatabase 3 | metadata: 4 | name: postgresqldatabase-sample 5 | spec: 6 | # Engine configuration link 7 | engineConfiguration: 8 | # Resource name 9 | name: simple 10 | # Resource namespace 11 | # Will use resource namespace if not set 12 | # namespace: 13 | # Database name 14 | database: databasename 15 | # Master role name 16 | # Master role name will be used to create top group role. 17 | # Database owner and users will be in this group role. 18 | # Default is "" 19 | masterRole: "" 20 | # Should drop on delete ? 21 | # Default set to false 22 | dropOnDelete: true 23 | # Wait for linked resource deletion to accept deletion of the current resource 24 | # See documentation for more information 25 | # Default set to false 26 | waitLinkedResourcesDeletion: true 27 | # Schemas 28 | schemas: 29 | # List of schemas to enable 30 | list: 31 | - schema1 32 | # Should drop on delete ? 33 | # Default set to false 34 | # If set to false, removing from list won't delete schema from database 35 | dropOnDelete: true 36 | # Delete schema with cascade 37 | # Default set to false 38 | # For all elements that have used the deleted schema 39 | deleteWithCascade: true 40 | # Extensions 41 | extensions: 42 | # List of extensions to enable 43 | list: 44 | - uuid-ossp 45 | # Should drop on delete ? 46 | # Default set to false 47 | # If set to false, removing from list won't delete extension from database 48 | dropOnDelete: true 49 | # Delete extension with cascade 50 | # Default set to false 51 | # For all elements that have used the deleted extension 52 | deleteWithCascade: true 53 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ♥ We love pull requests from everyone ! 4 | 5 | When contributing to this repository, please first discuss the change you wish 6 | to make via issue, email, or any other method with the owners of this repository 7 | before making a change. 8 | 9 | ## So all code changes happen through Pull Requests 10 | 11 | Pull requests are the best way to propose changes to the codebase. We actively 12 | welcome your pull requests: 13 | 14 | 1. Fork the repo and create your branch from `master`. 15 | 2. If you've added code that should be tested, add tests. 16 | 3. If you've added code that need documentation, update the documentation. 17 | 4. Make sure your code follows the [effective go](https://golang.org/doc/effective_go.html) guidelines as much as possible. 18 | 5. Be sure to test your modifications. 19 | 6. Write a good commit message following the [conventionalcommits](https://www.conventionalcommits.org/en/v1.0.0/) with the Angular convention. 20 | 7. Issue that pull request! 21 | 22 | ## Code of conduct 23 | 24 | Please note by participating in this project, you agree to abide by the [code of conduct]. 25 | 26 | [code of conduct]: https://github.com/easymile/postgresql-operator/blob/master/.github/CODE_OF_CONDUCT.md 27 | 28 | ## Any contributions you make will be under the MIT Software License 29 | 30 | In short, when you submit code changes, your submissions are understood to be 31 | under the same [MIT License](http://choosealicense.com/licenses/mit/) that 32 | covers the project. Feel free to contact the maintainers if that's a concern. 33 | 34 | ## Report bugs using Github's [issues](https://github.com/easymile/postgresql-operator/issues) 35 | 36 | We use GitHub issues to track public bugs. Report a bug by [opening a new 37 | issue](https://github.com/easymile/postgresql-operator/issues/new); it's that easy! 38 | -------------------------------------------------------------------------------- /helm/postgresql-operator/templates/_helpers.tpl: -------------------------------------------------------------------------------- 1 | {{/* vim: set filetype=mustache: */}} 2 | {{/* 3 | Expand the name of the chart. 4 | */}} 5 | {{- define "postgresql-operator.name" -}} 6 | {{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" -}} 7 | {{- end -}} 8 | 9 | {{/* 10 | Create a default fully qualified app name. 11 | We truncate at 63 chars because some Kubernetes name fields are limited to this (by the DNS naming spec). 12 | If release name contains chart name it will be used as a full name. 13 | */}} 14 | {{- define "postgresql-operator.fullname" -}} 15 | {{- if .Values.fullnameOverride -}} 16 | {{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" -}} 17 | {{- else -}} 18 | {{- $name := default .Chart.Name .Values.nameOverride -}} 19 | {{- if contains $name .Release.Name -}} 20 | {{- .Release.Name | trunc 63 | trimSuffix "-" -}} 21 | {{- else -}} 22 | {{- printf "%s-%s" .Release.Name $name | trunc 63 | trimSuffix "-" -}} 23 | {{- end -}} 24 | {{- end -}} 25 | {{- end -}} 26 | 27 | {{/* 28 | Create chart name and version as used by the chart label. 29 | */}} 30 | {{- define "postgresql-operator.chart" -}} 31 | {{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" -}} 32 | {{- end -}} 33 | 34 | {{/* 35 | Common labels 36 | */}} 37 | {{- define "postgresql-operator.labels" -}} 38 | app.kubernetes.io/name: {{ include "postgresql-operator.name" . }} 39 | helm.sh/chart: {{ include "postgresql-operator.chart" . }} 40 | app.kubernetes.io/instance: {{ .Release.Name }} 41 | {{- if .Chart.AppVersion }} 42 | app.kubernetes.io/version: {{ .Chart.AppVersion | quote }} 43 | {{- end }} 44 | app.kubernetes.io/managed-by: {{ .Release.Service }} 45 | {{- end -}} 46 | 47 | {{/* 48 | Create the name of the postgresql-operator service account to use 49 | */}} 50 | {{- define "postgresql-operator.serviceAccountName" -}} 51 | {{- if .Values.serviceAccount.create -}} 52 | {{ default (include "postgresql-operator.fullname" .) .Values.serviceAccount.name }} 53 | {{- else -}} 54 | {{ default "default" .Values.serviceAccount.name }} 55 | {{- end -}} 56 | {{- end -}} 57 | -------------------------------------------------------------------------------- /config/samples/engineconfiguration/full-example.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: postgresql.easymile.com/v1alpha1 2 | kind: PostgresqlEngineConfiguration 3 | metadata: 4 | name: full-example 5 | spec: 6 | # Provider type 7 | # Default to "" 8 | provider: "" 9 | # PostgreSQL Hostname 10 | host: localhost 11 | # PostgreSQL Port 12 | # Default to 5432 13 | port: 5432 14 | # Secret name in the current namespace to find "user" and "password" 15 | secretName: pgenginesecrets 16 | # URI args to add for PostgreSQL URL 17 | # Default to "" 18 | uriArgs: sslmode=disable 19 | # Default database name 20 | # Default to "postgres" 21 | defaultDatabase: postgres 22 | # Check interval 23 | # Default to 30s 24 | checkInterval: 30s 25 | # Allow grant admin on every created roles (group or user) for provided PGEC user in order to 26 | # have power to administrate those roles even with a less powered "admin" user. 27 | # Operator will create role and after grant PGEC provided user on those roles with admin option if enabled. 28 | allowGrantAdminOption: true 29 | # Wait for linked resource to be deleted 30 | # Default to false 31 | waitLinkedResourcesDeletion: true 32 | # User connections used for secret generation 33 | # That will be used to generate secret with primary server as url or 34 | # to use the pg bouncer one. 35 | # Note: Operator won't check those values. 36 | userConnections: 37 | # Primary connection is referring to the primary node connection. 38 | # If not being set, all values will be set from spec (host, port, uriArgs) 39 | primaryConnection: 40 | host: localhost 41 | uriArgs: sslmode=disable 42 | port: 5432 43 | # Bouncer connection is referring to a pg bouncer node. 44 | # bouncerConnection: 45 | # host: localhost 46 | # uriArgs: sslmode=disable 47 | # port: 6432 48 | # Replica connections are referring to the replica nodes. 49 | # replicaConnections: 50 | # - host: localhost 51 | # uriArgs: sslmode=disable 52 | # port: 5433 53 | # - host: localhost 54 | # uriArgs: sslmode=disable 55 | # port: 5434 56 | # Replica Bouncer connections are referring to pg bouncer nodes. 57 | # replicaBouncerConnections: 58 | # - host: localhost 59 | # uriArgs: sslmode=disable 60 | # port: 6432 61 | -------------------------------------------------------------------------------- /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 | - events 11 | verbs: 12 | - create 13 | - patch 14 | - apiGroups: 15 | - "" 16 | resources: 17 | - secrets 18 | verbs: 19 | - create 20 | - delete 21 | - get 22 | - list 23 | - patch 24 | - update 25 | - watch 26 | - apiGroups: 27 | - postgresql.easymile.com 28 | resources: 29 | - postgresqldatabases 30 | verbs: 31 | - create 32 | - delete 33 | - get 34 | - list 35 | - patch 36 | - update 37 | - watch 38 | - apiGroups: 39 | - postgresql.easymile.com 40 | resources: 41 | - postgresqldatabases/finalizers 42 | verbs: 43 | - update 44 | - apiGroups: 45 | - postgresql.easymile.com 46 | resources: 47 | - postgresqldatabases/status 48 | verbs: 49 | - get 50 | - patch 51 | - update 52 | - apiGroups: 53 | - postgresql.easymile.com 54 | resources: 55 | - postgresqlengineconfigurations 56 | verbs: 57 | - create 58 | - delete 59 | - get 60 | - list 61 | - patch 62 | - update 63 | - watch 64 | - apiGroups: 65 | - postgresql.easymile.com 66 | resources: 67 | - postgresqlengineconfigurations/finalizers 68 | verbs: 69 | - update 70 | - apiGroups: 71 | - postgresql.easymile.com 72 | resources: 73 | - postgresqlengineconfigurations/status 74 | verbs: 75 | - get 76 | - patch 77 | - update 78 | - apiGroups: 79 | - postgresql.easymile.com 80 | resources: 81 | - postgresqlpublications 82 | verbs: 83 | - create 84 | - delete 85 | - get 86 | - list 87 | - patch 88 | - update 89 | - watch 90 | - apiGroups: 91 | - postgresql.easymile.com 92 | resources: 93 | - postgresqlpublications/finalizers 94 | verbs: 95 | - update 96 | - apiGroups: 97 | - postgresql.easymile.com 98 | resources: 99 | - postgresqlpublications/status 100 | verbs: 101 | - get 102 | - patch 103 | - update 104 | - apiGroups: 105 | - postgresql.easymile.com 106 | resources: 107 | - postgresqluserroles 108 | verbs: 109 | - create 110 | - delete 111 | - get 112 | - list 113 | - patch 114 | - update 115 | - watch 116 | - apiGroups: 117 | - postgresql.easymile.com 118 | resources: 119 | - postgresqluserroles/finalizers 120 | verbs: 121 | - update 122 | - apiGroups: 123 | - postgresql.easymile.com 124 | resources: 125 | - postgresqluserroles/status 126 | verbs: 127 | - get 128 | - patch 129 | - update 130 | -------------------------------------------------------------------------------- /helm/postgresql-operator/values.yaml: -------------------------------------------------------------------------------- 1 | # Default values for postgresql-operator. 2 | # This is a YAML-formatted file. 3 | # Declare variables to be passed into your templates. 4 | 5 | ## Install Default RBAC roles and bindings 6 | rbac: 7 | create: true 8 | 9 | ## Service account name and whether to create it 10 | serviceAccount: 11 | create: true 12 | annotations: {} 13 | name: 14 | 15 | ## Let it empty to watch all namespaces 16 | watchNamespace: "" 17 | 18 | replicaCount: 1 19 | 20 | image: 21 | repository: easymile/postgresql-operator 22 | tag: 3.4.0 23 | pullPolicy: IfNotPresent 24 | 25 | args: 26 | - --leader-elect 27 | # - --resync-period=30s 28 | 29 | imagePullSecrets: [] 30 | nameOverride: "" 31 | fullnameOverride: "" 32 | 33 | podSecurityContext: 34 | runAsNonRoot: true 35 | 36 | securityContext: 37 | allowPrivilegeEscalation: false 38 | capabilities: 39 | drop: 40 | - "ALL" 41 | 42 | startupProbe: 43 | httpGet: 44 | path: /healthz 45 | port: 8081 46 | initialDelaySeconds: 0 47 | livenessProbe: 48 | httpGet: 49 | path: /healthz 50 | port: 8081 51 | initialDelaySeconds: 0 52 | periodSeconds: 20 53 | readinessProbe: 54 | httpGet: 55 | path: /readyz 56 | port: 8081 57 | initialDelaySeconds: 0 58 | periodSeconds: 20 59 | 60 | terminationGracePeriodSeconds: 10 61 | 62 | resources: 63 | {} 64 | # We usually recommend not to specify default resources and to leave this as a conscious 65 | # choice for the user. This also increases chances charts run on environments with little 66 | # resources, such as Minikube. If you do want to specify resources, uncomment the following 67 | # lines, adjust them as necessary, and remove the curly braces after 'resources:'. 68 | # limits: 69 | # cpu: 100m 70 | # memory: 128Mi 71 | # requests: 72 | # cpu: 100m 73 | # memory: 128Mi 74 | 75 | nodeSelector: {} 76 | 77 | tolerations: [] 78 | 79 | affinity: {} 80 | 81 | podAnnotations: {} 82 | 83 | grafanaDashboards: 84 | enabled: false 85 | labels: 86 | grafana_dashboard: "1" 87 | annotations: {} 88 | 89 | prometheus: 90 | serviceMonitor: 91 | enabled: false 92 | labels: {} 93 | interval: "" 94 | scrapeTimeout: "" 95 | metricRelabelings: [] 96 | relabelings: [] 97 | rules: 98 | enabled: false 99 | labels: {} 100 | spec: 101 | {} 102 | # groups: 103 | # - name: ./example.rules 104 | # rules: 105 | # - alert: ExampleAlert 106 | # expr: vector(1) 107 | -------------------------------------------------------------------------------- /helm/postgresql-operator/templates/role.yaml: -------------------------------------------------------------------------------- 1 | {{- if and .Values.rbac.create }} 2 | apiVersion: rbac.authorization.k8s.io/v1 3 | kind: ClusterRole 4 | metadata: 5 | name: {{ include "postgresql-operator.fullname" . }}-role 6 | labels: 7 | {{ include "postgresql-operator.labels" . | indent 4 }} 8 | rules: 9 | - apiGroups: 10 | - "" 11 | resources: 12 | - events 13 | verbs: 14 | - create 15 | - patch 16 | - apiGroups: 17 | - "" 18 | resources: 19 | - secrets 20 | verbs: 21 | - create 22 | - delete 23 | - get 24 | - list 25 | - patch 26 | - update 27 | - watch 28 | - apiGroups: 29 | - postgresql.easymile.com 30 | resources: 31 | - postgresqldatabases 32 | verbs: 33 | - create 34 | - delete 35 | - get 36 | - list 37 | - patch 38 | - update 39 | - watch 40 | - apiGroups: 41 | - postgresql.easymile.com 42 | resources: 43 | - postgresqldatabases/finalizers 44 | verbs: 45 | - update 46 | - apiGroups: 47 | - postgresql.easymile.com 48 | resources: 49 | - postgresqldatabases/status 50 | verbs: 51 | - get 52 | - patch 53 | - update 54 | - apiGroups: 55 | - postgresql.easymile.com 56 | resources: 57 | - postgresqlengineconfigurations 58 | verbs: 59 | - create 60 | - delete 61 | - get 62 | - list 63 | - patch 64 | - update 65 | - watch 66 | - apiGroups: 67 | - postgresql.easymile.com 68 | resources: 69 | - postgresqlengineconfigurations/finalizers 70 | verbs: 71 | - update 72 | - apiGroups: 73 | - postgresql.easymile.com 74 | resources: 75 | - postgresqlengineconfigurations/status 76 | verbs: 77 | - get 78 | - patch 79 | - update 80 | - apiGroups: 81 | - postgresql.easymile.com 82 | resources: 83 | - postgresqlpublications 84 | verbs: 85 | - create 86 | - delete 87 | - get 88 | - list 89 | - patch 90 | - update 91 | - watch 92 | - apiGroups: 93 | - postgresql.easymile.com 94 | resources: 95 | - postgresqlpublications/finalizers 96 | verbs: 97 | - update 98 | - apiGroups: 99 | - postgresql.easymile.com 100 | resources: 101 | - postgresqlpublications/status 102 | verbs: 103 | - get 104 | - patch 105 | - update 106 | - apiGroups: 107 | - postgresql.easymile.com 108 | resources: 109 | - postgresqluserroles 110 | verbs: 111 | - create 112 | - delete 113 | - get 114 | - list 115 | - patch 116 | - update 117 | - watch 118 | - apiGroups: 119 | - postgresql.easymile.com 120 | resources: 121 | - postgresqluserroles/finalizers 122 | verbs: 123 | - update 124 | - apiGroups: 125 | - postgresql.easymile.com 126 | resources: 127 | - postgresqluserroles/status 128 | verbs: 129 | - get 130 | - patch 131 | - update 132 | {{- end -}} 133 | -------------------------------------------------------------------------------- /helm/postgresql-operator/templates/deployment.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: apps/v1 2 | kind: Deployment 3 | metadata: 4 | name: {{ include "postgresql-operator.fullname" . }} 5 | labels: 6 | {{ include "postgresql-operator.labels" . | indent 4 }} 7 | spec: 8 | replicas: {{ .Values.replicaCount }} 9 | selector: 10 | matchLabels: 11 | app.kubernetes.io/name: {{ include "postgresql-operator.name" . }} 12 | app.kubernetes.io/instance: {{ .Release.Name }} 13 | template: 14 | metadata: 15 | labels: 16 | app.kubernetes.io/name: {{ include "postgresql-operator.name" . }} 17 | app.kubernetes.io/instance: {{ .Release.Name }} 18 | annotations: 19 | {{- toYaml .Values.podAnnotations | nindent 8 }} 20 | spec: 21 | serviceAccountName: {{ include "postgresql-operator.serviceAccountName" . }} 22 | {{- with .Values.imagePullSecrets }} 23 | imagePullSecrets: 24 | {{- toYaml . | nindent 8 }} 25 | {{- end }} 26 | terminationGracePeriodSeconds: {{ .Values.terminationGracePeriodSeconds }} 27 | securityContext: 28 | {{- toYaml .Values.podSecurityContext | nindent 8 }} 29 | containers: 30 | - name: {{ .Chart.Name }} 31 | image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}" 32 | imagePullPolicy: {{ .Values.image.pullPolicy }} 33 | {{- if .Values.args }} 34 | args: 35 | {{- range $key, $value := .Values.args }} 36 | - {{ $value }} 37 | {{- end }} 38 | {{- end }} 39 | securityContext: 40 | {{- toYaml .Values.securityContext | nindent 12 }} 41 | env: 42 | - name: WATCH_NAMESPACE 43 | value: {{ .Values.watchNamespace }} 44 | - name: POD_NAME 45 | valueFrom: 46 | fieldRef: 47 | fieldPath: metadata.name 48 | - name: OPERATOR_NAME 49 | value: {{ include "postgresql-operator.fullname" . }} 50 | ports: 51 | - name: http-metrics 52 | containerPort: 8080 53 | protocol: TCP 54 | livenessProbe: 55 | {{- toYaml .Values.livenessProbe | nindent 12 }} 56 | readinessProbe: 57 | {{- toYaml .Values.readinessProbe | nindent 12 }} 58 | startupProbe: 59 | {{- toYaml .Values.startupProbe | nindent 12 }} 60 | resources: 61 | {{- toYaml .Values.resources | nindent 12 }} 62 | {{- with .Values.nodeSelector }} 63 | nodeSelector: 64 | {{- toYaml . | nindent 8 }} 65 | {{- end }} 66 | {{- with .Values.affinity }} 67 | affinity: 68 | {{- toYaml . | nindent 8 }} 69 | {{- end }} 70 | {{- with .Values.tolerations }} 71 | tolerations: 72 | {{- toYaml . | nindent 8 }} 73 | {{- end }} 74 | -------------------------------------------------------------------------------- /internal/controller/postgresql/postgres/create-publication-builder.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type CreatePublicationBuilder struct { 9 | name string 10 | tablesPart string 11 | allTables string 12 | withPart string 13 | owner string 14 | tables []string 15 | schemaList []string 16 | } 17 | 18 | func NewCreatePublicationBuilder() *CreatePublicationBuilder { 19 | return &CreatePublicationBuilder{} 20 | } 21 | 22 | func (b *CreatePublicationBuilder) Build() { 23 | if b.allTables != "" { 24 | b.tablesPart = b.allTables 25 | 26 | return 27 | } 28 | 29 | // Build 30 | res := "FOR " 31 | 32 | // Check if tables are set 33 | if len(b.tables) != 0 { 34 | res += "TABLE " + strings.Join(b.tables, ", ") 35 | } 36 | 37 | // Check if schema are set 38 | if len(b.schemaList) != 0 { 39 | // Check if tables were added 40 | if len(b.tables) != 0 { 41 | // Append 42 | res += ", " 43 | } 44 | 45 | res += "TABLES IN SCHEMA " + strings.Join(b.schemaList, ", ") 46 | } 47 | 48 | // Save 49 | b.tablesPart = res 50 | } 51 | 52 | func (b *CreatePublicationBuilder) AddTable(name string, columns *[]string, additionalWhere *string) *CreatePublicationBuilder { 53 | res := name 54 | 55 | // Manage columns 56 | if columns != nil { 57 | res += " (" + strings.Join(*columns, ", ") + ")" 58 | } 59 | 60 | // Add where is set 61 | if additionalWhere != nil { 62 | res += " WHERE (" + *additionalWhere + ")" 63 | } 64 | 65 | // Save 66 | b.tables = append(b.tables, res) 67 | 68 | return b 69 | } 70 | 71 | func (b *CreatePublicationBuilder) SetTablesInSchema(schemaList []string) *CreatePublicationBuilder { 72 | b.schemaList = schemaList 73 | 74 | return b 75 | } 76 | 77 | func (b *CreatePublicationBuilder) SetForAllTables() *CreatePublicationBuilder { 78 | b.allTables = "FOR ALL TABLES" 79 | 80 | return b 81 | } 82 | 83 | func (b *CreatePublicationBuilder) SetOwner(n string) *CreatePublicationBuilder { 84 | b.owner = n 85 | 86 | return b 87 | } 88 | 89 | func (b *CreatePublicationBuilder) SetName(n string) *CreatePublicationBuilder { 90 | b.name = n 91 | 92 | return b 93 | } 94 | 95 | func (b *CreatePublicationBuilder) SetWith(publish string, publishViaPartitionRoot *bool) *CreatePublicationBuilder { 96 | var with string 97 | // Check if publish is set 98 | if publish != "" { 99 | with += "publish = '" + publish + "'" 100 | } 101 | // Check publish via partition root 102 | if publishViaPartitionRoot != nil { 103 | // Check if there is already a with set 104 | if with != "" { 105 | with += ", " 106 | } 107 | // Manage bool 108 | with += "publish_via_partition_root = " 109 | if *publishViaPartitionRoot { 110 | with += "true" 111 | } else { 112 | with += "false" 113 | } 114 | } 115 | 116 | // Check if there isn't something 117 | if with != "" { 118 | // Save 119 | b.withPart = fmt.Sprintf("WITH (%s)", with) 120 | } 121 | 122 | return b 123 | } 124 | -------------------------------------------------------------------------------- /internal/controller/postgresql/postgres/update-publication-builder.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | ) 7 | 8 | type UpdatePublicationBuilder struct { 9 | newName string 10 | withPart string 11 | tablesPart string 12 | tables []string 13 | schemaList []string 14 | } 15 | 16 | func NewUpdatePublicationBuilder() *UpdatePublicationBuilder { 17 | return &UpdatePublicationBuilder{} 18 | } 19 | 20 | func (b *UpdatePublicationBuilder) Build() { 21 | // Build 22 | var res string 23 | 24 | // Check if tables are set 25 | if len(b.tables) != 0 { 26 | res += "TABLE " + strings.Join(b.tables, ", ") 27 | } 28 | 29 | // Check if schema are set 30 | if len(b.schemaList) != 0 { 31 | // Check if tables were added 32 | if len(b.tables) != 0 { 33 | // Append 34 | res += ", " 35 | } 36 | 37 | res += "TABLES IN SCHEMA " + strings.Join(b.schemaList, ", ") 38 | } 39 | 40 | // Save 41 | b.tablesPart = res 42 | } 43 | 44 | func (b *UpdatePublicationBuilder) AddSetTable(name string, columns *[]string, additionalWhere *string) *UpdatePublicationBuilder { 45 | res := name 46 | 47 | // Manage columns 48 | if columns != nil { 49 | res += " (" + strings.Join(*columns, ", ") + ")" 50 | } 51 | 52 | // Add where is set 53 | if additionalWhere != nil { 54 | res += " WHERE (" + *additionalWhere + ")" 55 | } 56 | 57 | // Save 58 | b.tables = append(b.tables, res) 59 | 60 | return b 61 | } 62 | 63 | func (b *UpdatePublicationBuilder) SetTablesInSchema(schemaList []string) *UpdatePublicationBuilder { 64 | b.schemaList = schemaList 65 | 66 | return b 67 | } 68 | 69 | func (b *UpdatePublicationBuilder) RenameTo(newName string) *UpdatePublicationBuilder { 70 | b.newName = newName 71 | 72 | return b 73 | } 74 | 75 | func (b *UpdatePublicationBuilder) SetWith(publish string, publishViaPartitionRoot *bool) *UpdatePublicationBuilder { 76 | var with string 77 | // Check if publish is set 78 | if publish != "" { 79 | with += "publish = '" + publish + "'" 80 | } else { 81 | // Set default for reconcile cases 82 | with += "publish = 'insert, update, delete, truncate'" 83 | } 84 | 85 | // Check publish via partition root 86 | if publishViaPartitionRoot != nil { 87 | // Check if there is already a with set 88 | if with != "" { 89 | with += ", " 90 | } 91 | // Manage bool 92 | with += "publish_via_partition_root = " 93 | if *publishViaPartitionRoot { 94 | with += "true" 95 | } else { 96 | with += "false" 97 | } 98 | } else { 99 | // Check if there is already a with set 100 | if with != "" { 101 | with += ", " 102 | } 103 | // Set default for reconcile cases 104 | with += "publish_via_partition_root = false" 105 | } 106 | 107 | // Save 108 | b.withPart = fmt.Sprintf(" (%s)", with) 109 | 110 | return b 111 | } 112 | 113 | func (b *UpdatePublicationBuilder) SetDefaultWith() *UpdatePublicationBuilder { 114 | fV := false 115 | // Call other method without parameters to inject default values 116 | b.SetWith("", &fV) 117 | 118 | return b 119 | } 120 | -------------------------------------------------------------------------------- /.github/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. 6 | 7 | ## Our Standards 8 | 9 | Examples of behavior that contributes to creating a positive environment include: 10 | 11 | - Using welcoming and inclusive language 12 | - Being respectful of differing viewpoints and experiences 13 | - Gracefully accepting constructive criticism 14 | - Focusing on what is best for the community 15 | - Showing empathy towards other community members 16 | 17 | Examples of unacceptable behavior by participants include: 18 | 19 | - The use of sexualized language or imagery and unwelcome sexual attention or advances 20 | - Trolling, insulting/derogatory comments, and personal or political attacks 21 | - Public or private harassment 22 | - Publishing others' private information, such as a physical or electronic address, without explicit permission 23 | - Other conduct which could reasonably be considered inappropriate in a professional setting 24 | 25 | ## Our Responsibilities 26 | 27 | Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. 28 | 29 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. 30 | 31 | ## Scope 32 | 33 | This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. 34 | 35 | ## Enforcement 36 | 37 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. 38 | 39 | Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. 40 | 41 | ## Attribution 42 | 43 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] 44 | 45 | [homepage]: http://contributor-covenant.org 46 | [version]: http://contributor-covenant.org/version/1/4/ 47 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/easymile/postgresql-operator 2 | 3 | go 1.20 4 | 5 | require ( 6 | github.com/go-logr/logr v1.2.4 7 | github.com/lib/pq v1.10.9 8 | github.com/onsi/ginkgo/v2 v2.9.5 9 | github.com/onsi/gomega v1.27.7 10 | github.com/prometheus/client_golang v1.15.1 11 | github.com/samber/lo v1.47.0 12 | github.com/thoas/go-funk v0.9.3 13 | k8s.io/api v0.27.2 14 | k8s.io/apimachinery v0.27.2 15 | k8s.io/client-go v0.27.2 16 | sigs.k8s.io/controller-runtime v0.15.0 17 | ) 18 | 19 | require ( 20 | github.com/beorn7/perks v1.0.1 // indirect 21 | github.com/cespare/xxhash/v2 v2.2.0 // indirect 22 | github.com/davecgh/go-spew v1.1.1 // indirect 23 | github.com/emicklei/go-restful/v3 v3.9.0 // indirect 24 | github.com/evanphx/json-patch/v5 v5.6.0 // indirect 25 | github.com/fsnotify/fsnotify v1.6.0 // indirect 26 | github.com/go-logr/zapr v1.2.4 // indirect 27 | github.com/go-openapi/jsonpointer v0.19.6 // indirect 28 | github.com/go-openapi/jsonreference v0.20.1 // indirect 29 | github.com/go-openapi/swag v0.22.3 // indirect 30 | github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect 31 | github.com/gogo/protobuf v1.3.2 // indirect 32 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect 33 | github.com/golang/protobuf v1.5.3 // indirect 34 | github.com/google/gnostic v0.5.7-v3refs // indirect 35 | github.com/google/go-cmp v0.6.0 // indirect 36 | github.com/google/gofuzz v1.1.0 // indirect 37 | github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1 // indirect 38 | github.com/google/uuid v1.3.0 // indirect 39 | github.com/imdario/mergo v0.3.6 // indirect 40 | github.com/josharian/intern v1.0.0 // indirect 41 | github.com/json-iterator/go v1.1.12 // indirect 42 | github.com/mailru/easyjson v0.7.7 // indirect 43 | github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect 44 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 45 | github.com/modern-go/reflect2 v1.0.2 // indirect 46 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 47 | github.com/pkg/errors v0.9.1 // indirect 48 | github.com/prometheus/client_model v0.4.0 // indirect 49 | github.com/prometheus/common v0.42.0 // indirect 50 | github.com/prometheus/procfs v0.9.0 // indirect 51 | github.com/spf13/pflag v1.0.5 // indirect 52 | go.uber.org/atomic v1.7.0 // indirect 53 | go.uber.org/multierr v1.6.0 // indirect 54 | go.uber.org/zap v1.24.0 // indirect 55 | golang.org/x/net v0.25.0 // indirect 56 | golang.org/x/oauth2 v0.5.0 // indirect 57 | golang.org/x/sys v0.20.0 // indirect 58 | golang.org/x/term v0.20.0 // indirect 59 | golang.org/x/text v0.16.0 // indirect 60 | golang.org/x/time v0.3.0 // indirect 61 | golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect 62 | gomodules.xyz/jsonpatch/v2 v2.3.0 // indirect 63 | google.golang.org/appengine v1.6.7 // indirect 64 | google.golang.org/protobuf v1.30.0 // indirect 65 | gopkg.in/inf.v0 v0.9.1 // indirect 66 | gopkg.in/yaml.v2 v2.4.0 // indirect 67 | gopkg.in/yaml.v3 v3.0.1 // indirect 68 | k8s.io/apiextensions-apiserver v0.27.2 // indirect 69 | k8s.io/component-base v0.27.2 // indirect 70 | k8s.io/klog/v2 v2.90.1 // indirect 71 | k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect 72 | k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect 73 | sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect 74 | sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect 75 | sigs.k8s.io/yaml v1.3.0 // indirect 76 | ) 77 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | on: 3 | push: 4 | branches-ignore: 5 | - "github-pages" 6 | pull_request: {} 7 | jobs: 8 | changes: 9 | runs-on: ubuntu-latest 10 | outputs: 11 | go: ${{ steps.filter.outputs.go }} 12 | config: ${{ steps.filter.outputs.config }} 13 | docker: ${{ steps.filter.outputs.docker }} 14 | steps: 15 | - uses: actions/checkout@v4 16 | - uses: dorny/paths-filter@v3 17 | id: filter 18 | with: 19 | token: ${{ secrets.GITHUB_TOKEN }} 20 | filters: | 21 | go: 22 | - '**/*.go' 23 | - 'go.mod' 24 | - 'go.sum' 25 | config: 26 | - '.github/workflows/ci.yml' 27 | - '.golangci.yaml' 28 | - 'Makefile' 29 | docker: 30 | - 'Dockerfile' 31 | lint: 32 | if: | 33 | (needs.changes.outputs.go == 'true') || 34 | (needs.changes.outputs.config == 'true') 35 | runs-on: ubuntu-latest 36 | needs: 37 | - changes 38 | steps: 39 | - uses: actions/checkout@v4 40 | - uses: actions/setup-go@v5 41 | with: 42 | go-version: "1.22.2" 43 | - uses: golangci/golangci-lint-action@v3 44 | with: 45 | # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version 46 | version: v1.56.2 47 | 48 | # Optional: working directory, useful for monorepos 49 | # working-directory: somedir 50 | 51 | # Optional: golangci-lint command line arguments. 52 | args: --timeout=3600s 53 | 54 | # Optional: show only new issues if it's a pull request. The default value is `false`. 55 | # only-new-issues: true 56 | 57 | # Optional: if set to true then the action will use pre-installed Go. 58 | # skip-go-installation: true 59 | 60 | # Optional: if set to true then the action don't cache or restore ~/go/pkg. 61 | # skip-pkg-cache: true 62 | 63 | # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. 64 | # skip-build-cache: true 65 | build: 66 | if: | 67 | (needs.changes.outputs.go == 'true') || 68 | (needs.changes.outputs.config == 'true') 69 | runs-on: ubuntu-latest 70 | needs: 71 | - changes 72 | - lint 73 | steps: 74 | - uses: actions/checkout@v4 75 | - uses: actions/setup-go@v5 76 | with: 77 | go-version: "1.22.2" 78 | - run: make build 79 | - run: make docker-build 80 | test: 81 | if: | 82 | (needs.changes.outputs.go == 'true') || 83 | (needs.changes.outputs.config == 'true') || 84 | (needs.changes.outputs.docker == 'true') 85 | runs-on: ubuntu-latest 86 | needs: 87 | - changes 88 | - lint 89 | - build 90 | steps: 91 | - uses: actions/checkout@v4 92 | - uses: actions/setup-go@v5 93 | with: 94 | go-version: "1.22.2" 95 | - run: | 96 | if [ -f /usr/local/bin/dockerize ]; then 97 | echo "dockerize found; skipping installation"; 98 | else 99 | wget https://github.com/jwilder/dockerize/releases/download/v0.6.0/dockerize-linux-amd64-v0.6.0.tar.gz \ 100 | && sudo tar -C /usr/local/bin -xzvf dockerize-linux-amd64-v0.6.0.tar.gz \ 101 | && rm dockerize-linux-amd64-v0.6.0.tar.gz; 102 | fi 103 | - run: make setup/services 104 | - run: dockerize -wait tcp://localhost:5432 -timeout 5m 105 | - run: docker logs postgres 106 | - run: make test/all 107 | - run: make test/coverage 108 | -------------------------------------------------------------------------------- /config/manager/manager.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: Namespace 3 | metadata: 4 | labels: 5 | control-plane: controller-manager 6 | app.kubernetes.io/name: namespace 7 | app.kubernetes.io/instance: system 8 | app.kubernetes.io/component: manager 9 | app.kubernetes.io/created-by: postgresql-operator 10 | app.kubernetes.io/part-of: postgresql-operator 11 | app.kubernetes.io/managed-by: kustomize 12 | name: system 13 | --- 14 | apiVersion: apps/v1 15 | kind: Deployment 16 | metadata: 17 | name: controller-manager 18 | namespace: system 19 | labels: 20 | control-plane: controller-manager 21 | app.kubernetes.io/name: deployment 22 | app.kubernetes.io/instance: controller-manager 23 | app.kubernetes.io/component: manager 24 | app.kubernetes.io/created-by: postgresql-operator 25 | app.kubernetes.io/part-of: postgresql-operator 26 | app.kubernetes.io/managed-by: kustomize 27 | spec: 28 | selector: 29 | matchLabels: 30 | control-plane: controller-manager 31 | replicas: 1 32 | template: 33 | metadata: 34 | annotations: 35 | kubectl.kubernetes.io/default-container: manager 36 | labels: 37 | control-plane: controller-manager 38 | spec: 39 | # TODO(user): Uncomment the following code to configure the nodeAffinity expression 40 | # according to the platforms which are supported by your solution. 41 | # It is considered best practice to support multiple architectures. You can 42 | # build your manager image using the makefile target docker-buildx. 43 | # affinity: 44 | # nodeAffinity: 45 | # requiredDuringSchedulingIgnoredDuringExecution: 46 | # nodeSelectorTerms: 47 | # - matchExpressions: 48 | # - key: kubernetes.io/arch 49 | # operator: In 50 | # values: 51 | # - amd64 52 | # - arm64 53 | # - ppc64le 54 | # - s390x 55 | # - key: kubernetes.io/os 56 | # operator: In 57 | # values: 58 | # - linux 59 | securityContext: 60 | runAsNonRoot: true 61 | # TODO(user): For common cases that do not require escalating privileges 62 | # it is recommended to ensure that all your Pods/Containers are restrictive. 63 | # More info: https://kubernetes.io/docs/concepts/security/pod-security-standards/#restricted 64 | # Please uncomment the following code if your project does NOT have to work on old Kubernetes 65 | # versions < 1.19 or on vendors versions which do NOT support this field by default (i.e. Openshift < 4.11 ). 66 | # seccompProfile: 67 | # type: RuntimeDefault 68 | containers: 69 | - command: 70 | - /manager 71 | args: 72 | - --leader-elect 73 | image: controller:latest 74 | name: manager 75 | securityContext: 76 | allowPrivilegeEscalation: false 77 | capabilities: 78 | drop: 79 | - "ALL" 80 | livenessProbe: 81 | httpGet: 82 | path: /healthz 83 | port: 8081 84 | initialDelaySeconds: 15 85 | periodSeconds: 20 86 | readinessProbe: 87 | httpGet: 88 | path: /readyz 89 | port: 8081 90 | initialDelaySeconds: 5 91 | periodSeconds: 10 92 | # TODO(user): Configure the resources accordingly based on the project requirements. 93 | # More info: https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ 94 | resources: 95 | limits: 96 | cpu: 500m 97 | memory: 128Mi 98 | requests: 99 | cpu: 10m 100 | memory: 64Mi 101 | serviceAccountName: controller-manager 102 | terminationGracePeriodSeconds: 10 103 | -------------------------------------------------------------------------------- /internal/controller/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "context" 5 | "crypto/sha256" 6 | "encoding/hex" 7 | "encoding/json" 8 | 9 | "github.com/easymile/postgresql-operator/api/postgresql/common" 10 | postgresqlv1alpha1 "github.com/easymile/postgresql-operator/api/postgresql/v1alpha1" 11 | "github.com/easymile/postgresql-operator/internal/controller/postgresql/postgres" 12 | "github.com/go-logr/logr" 13 | corev1 "k8s.io/api/core/v1" 14 | "k8s.io/apimachinery/pkg/types" 15 | "sigs.k8s.io/controller-runtime/pkg/client" 16 | ) 17 | 18 | func CalculateHash(spec interface{}) (string, error) { 19 | // Json marshal spec 20 | bytes, err := json.Marshal(spec) 21 | if err != nil { 22 | return "", err 23 | } 24 | // Sha on bytes array 25 | sha256Res := sha256.Sum256(bytes) 26 | sha256Bytes := sha256Res[:] 27 | // Transform it to string 28 | return hex.EncodeToString(sha256Bytes), nil 29 | } 30 | 31 | func CreatePgInstance( 32 | reqLogger logr.Logger, 33 | secretData map[string][]byte, 34 | pgec *postgresqlv1alpha1.PostgresqlEngineConfiguration, 35 | ) postgres.PG { 36 | spec := pgec.Spec 37 | user := string(secretData["user"]) 38 | password := string(secretData["password"]) 39 | 40 | return postgres.NewPG( 41 | CreateNameKeyForSavedPools(pgec.Name, pgec.Namespace), 42 | spec.Host, 43 | user, 44 | password, 45 | spec.URIArgs, 46 | spec.DefaultDatabase, 47 | spec.Port, 48 | spec.Provider, 49 | reqLogger, 50 | ) 51 | } 52 | 53 | func GetSecret(ctx context.Context, cl client.Client, name, namespace string) (*corev1.Secret, error) { 54 | secret := &corev1.Secret{} 55 | err := cl.Get(ctx, types.NamespacedName{Name: name, Namespace: namespace}, secret) 56 | 57 | return secret, err 58 | } 59 | 60 | func FindSecretPgEngineCfg( 61 | ctx context.Context, 62 | cl client.Client, 63 | instance *postgresqlv1alpha1.PostgresqlEngineConfiguration, 64 | ) (*corev1.Secret, error) { 65 | secret := &corev1.Secret{} 66 | err := cl.Get(ctx, types.NamespacedName{Name: instance.Spec.SecretName, Namespace: instance.Namespace}, secret) 67 | 68 | return secret, err 69 | } 70 | 71 | func CloseDatabaseSavedPoolsForName(instance *postgresqlv1alpha1.PostgresqlDatabase, database string) error { 72 | // Try to get namespace from spec 73 | namespace := instance.Spec.EngineConfiguration.Namespace 74 | if namespace == "" { 75 | // Namespace not found, take it from instance namespace 76 | namespace = instance.Namespace 77 | } 78 | 79 | return postgres.CloseDatabaseSavedPoolsForName( 80 | CreateNameKeyForSavedPools(instance.Spec.EngineConfiguration.Name, namespace), 81 | database, 82 | ) 83 | } 84 | 85 | func CreateNameKey(name, namespace, instanceNamespace string) string { 86 | res := "" 87 | 88 | if namespace != "" { 89 | res += namespace 90 | } else { 91 | res += instanceNamespace 92 | } 93 | 94 | res += "/" + name 95 | 96 | return res 97 | } 98 | 99 | func CreateNameKeyForSavedPools(pgecName, pgecNamespace string) string { 100 | return pgecNamespace + "/" + pgecName 101 | } 102 | 103 | func FindPgEngineCfg( 104 | ctx context.Context, 105 | cl client.Client, 106 | instance *postgresqlv1alpha1.PostgresqlDatabase, 107 | ) (*postgresqlv1alpha1.PostgresqlEngineConfiguration, error) { 108 | // Try to get namespace from spec 109 | namespace := instance.Spec.EngineConfiguration.Namespace 110 | if namespace == "" { 111 | // Namespace not found, take it from instance namespace 112 | namespace = instance.Namespace 113 | } 114 | 115 | pgEngineCfg := &postgresqlv1alpha1.PostgresqlEngineConfiguration{} 116 | err := cl.Get(ctx, client.ObjectKey{ 117 | Name: instance.Spec.EngineConfiguration.Name, 118 | Namespace: namespace, 119 | }, pgEngineCfg) 120 | 121 | return pgEngineCfg, err 122 | } 123 | 124 | func FindPgDatabaseFromLink( 125 | ctx context.Context, 126 | cl client.Client, 127 | link *common.CRLink, 128 | instanceNamespace string, 129 | ) (*postgresqlv1alpha1.PostgresqlDatabase, error) { 130 | // Try to get namespace from spec 131 | namespace := link.Namespace 132 | if namespace == "" { 133 | // Namespace not found, take it from instance namespace 134 | namespace = instanceNamespace 135 | } 136 | 137 | pgDatabase := &postgresqlv1alpha1.PostgresqlDatabase{} 138 | err := cl.Get(ctx, client.ObjectKey{ 139 | Name: link.Name, 140 | Namespace: namespace, 141 | }, pgDatabase) 142 | 143 | return pgDatabase, err 144 | } 145 | -------------------------------------------------------------------------------- /internal/controller/postgresql/postgres/aws.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | 7 | "github.com/lib/pq" 8 | ) 9 | 10 | const ( 11 | CreateDBWithoutOwnerSQLTemplate = `CREATE DATABASE "%s"` 12 | AlterDBOwnerSQLTemplate = `ALTER DATABASE "%s" OWNER TO "%s"` 13 | ) 14 | 15 | type awspg struct { 16 | pg 17 | } 18 | 19 | func newAWSPG(postgres *pg) PG { 20 | return &awspg{ 21 | *postgres, 22 | } 23 | } 24 | 25 | func (c *awspg) AlterDefaultLoginRole(ctx context.Context, role, setRole string) error { 26 | // On AWS RDS the postgres user isn't really superuser so he doesn't have permissions 27 | // to ALTER USER unless he belongs to both roles 28 | err := c.GrantRole(ctx, role, c.user, false) 29 | if err != nil { 30 | return err 31 | } 32 | 33 | defer func() { 34 | err := c.RevokeRole(ctx, role, c.user) 35 | // Check error 36 | if err != nil { 37 | c.log.Error(err, "error in revoke role") 38 | } 39 | }() 40 | 41 | return c.pg.AlterDefaultLoginRole(ctx, role, setRole) 42 | } 43 | 44 | func (c *awspg) CreateDB(ctx context.Context, dbname, role string) error { 45 | err := c.connect(c.defaultDatabase) 46 | if err != nil { 47 | return err 48 | } 49 | 50 | _, err = c.db.ExecContext(ctx, fmt.Sprintf(CreateDBWithoutOwnerSQLTemplate, dbname)) 51 | if err != nil { 52 | // eat DUPLICATE DATABASE ERROR 53 | // Try to cast error 54 | pqErr, ok := err.(*pq.Error) 55 | if !ok || pqErr.Code != DuplicateDatabaseErrorCode { 56 | return err 57 | } 58 | } 59 | 60 | _, err = c.db.ExecContext(ctx, fmt.Sprintf(AlterDBOwnerSQLTemplate, dbname, role)) 61 | if err != nil { 62 | return err 63 | } 64 | 65 | return nil 66 | } 67 | 68 | func (c *awspg) DropRoleAndDropAndChangeOwnedBy(ctx context.Context, role, newOwner, database string) error { 69 | // On AWS RDS the postgres user isn't really superuser so he doesn't have permissions 70 | // to REASSIGN OWNED BY unless he belongs to both roles 71 | err := c.GrantRole(ctx, role, c.user, false) 72 | // Check error 73 | if err != nil { 74 | // Try to cast error 75 | pqErr, ok := err.(*pq.Error) 76 | if !ok { 77 | return err 78 | } 79 | 80 | if pqErr.Code == RoleNotFoundErrorCode { 81 | return nil 82 | } 83 | 84 | if pqErr.Code != InvalidGrantOperationErrorCode { 85 | return err 86 | } 87 | } 88 | 89 | err = c.GrantRole(ctx, newOwner, c.user, false) 90 | // Check error 91 | if err != nil { 92 | // Try to cast error 93 | pqErr, ok := err.(*pq.Error) 94 | if !ok { 95 | return err 96 | } 97 | 98 | if pqErr.Code == RoleNotFoundErrorCode { 99 | // The group role does not exist, no point of granting roles 100 | c.log.Info(fmt.Sprintf("not granting %s to %s as %s does not exist", role, newOwner, newOwner)) 101 | 102 | return nil 103 | } 104 | 105 | if pqErr.Code != InvalidGrantOperationErrorCode { 106 | return err 107 | } 108 | } 109 | 110 | defer func() { 111 | err := c.RevokeRole(ctx, newOwner, c.user) 112 | if err != nil { 113 | c.log.Error(err, "error in revoke role") 114 | } 115 | }() 116 | 117 | return c.pg.DropRoleAndDropAndChangeOwnedBy(ctx, role, newOwner, database) 118 | } 119 | 120 | func (c *awspg) ChangeAndDropOwnedBy(ctx context.Context, role, newOwner, database string) error { 121 | // On AWS RDS the postgres user isn't really superuser so he doesn't have permissions 122 | // to REASSIGN OWNED BY unless he belongs to both roles 123 | err := c.GrantRole(ctx, role, c.user, false) 124 | // Check error 125 | if err != nil { 126 | // Try to cast error 127 | pqErr, ok := err.(*pq.Error) 128 | if !ok { 129 | return err 130 | } 131 | 132 | if pqErr.Code == RoleNotFoundErrorCode { 133 | return nil 134 | } 135 | 136 | if pqErr.Code != InvalidGrantOperationErrorCode { 137 | return err 138 | } 139 | } 140 | 141 | err = c.GrantRole(ctx, newOwner, c.user, false) 142 | // Check error 143 | if err != nil { 144 | // Try to cast error 145 | pqErr, ok := err.(*pq.Error) 146 | if !ok { 147 | return err 148 | } 149 | 150 | if pqErr.Code == RoleNotFoundErrorCode { 151 | // The group role does not exist, no point of granting roles 152 | c.log.Info(fmt.Sprintf("not granting %s to %s as %s does not exist", role, newOwner, newOwner)) 153 | 154 | return nil 155 | } 156 | 157 | if pqErr.Code != InvalidGrantOperationErrorCode { 158 | return err 159 | } 160 | } 161 | 162 | defer func() { 163 | err := c.RevokeRole(ctx, newOwner, c.user) 164 | if err != nil { 165 | c.log.Error(err, "error in revoke role") 166 | } 167 | }() 168 | 169 | return c.pg.ChangeAndDropOwnedBy(ctx, role, newOwner, database) 170 | } 171 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 3.4.2 4 | 5 | ### Fixes 6 | 7 | - postgres:database : Fix check conditions on database owner 8 | 9 | ## 3.4.1 10 | 11 | ### Fixes 12 | 13 | - Fix with linter returns 14 | - postgresqlpublication : Fix update & create with parameters builders, add forgotten auto heal on publications 15 | - postgresqlpublication: Add forgotten management of publication ownership 16 | - Remove misleading log 17 | - Change resync default period & reconcile default timeout to avoid postgresql high load 18 | - postgresqldatabase: Avoid some alter if possible 19 | 20 | ## 3.4.0 21 | 22 | ### Features 23 | 24 | - Add timeout support for reconcile loops 25 | - postgresqluserrole: Add support for extra connection url parameters 26 | 27 | ### Fixes 28 | 29 | - postgresqldatabase: Avoid closing connections multiple time in row and close them when needed 30 | - postgresqldatabase: Avoid alters on tables & types when owner is already the right one 31 | - postgresqldatabase: Improve deletion by checking if roles or database are existing in engine before trying to delete them. This will avoid dead lock scenarios 32 | 33 | ### Refactor 34 | 35 | - Use context on all SQL calls 36 | 37 | ## 3.3.0 38 | 39 | ### Features 40 | 41 | - Add support for PostgreSQL Publication 42 | - Add support for role attributes in UserRole objects 43 | 44 | ## 3.2.0 45 | 46 | ### Features 47 | 48 | - Add support for type ownership recover in database management 49 | 50 | ### Bugs 51 | 52 | - Remove duplicated metadata in leader election role in Helm chart 53 | 54 | ## 3.1.0 55 | 56 | ### Features 57 | 58 | - Add support for grant role with admin option if asked 59 | - Add public schema by default in PostgreSQLDatabase objects if nothing is set 60 | - Add support for replica urls (with bouncer supported) 61 | 62 | ### Bugs 63 | 64 | - Ensure database owner is set correctly 65 | - Ensure all tables under listed schema have the right owner 66 | 67 | ## 3.0.0 68 | 69 | ### Breaking change 70 | 71 | - Do not support anymore postgresql user. Switch to postgresqluserrole is now mandatory 72 | 73 | ### Features 74 | 75 | - Improve Helm chart with new features and code 76 | - Add support for PGBouncer in PosgtreSQL Engine Configurations and PostgreSQL User Roles 77 | - Add custom metric to count reconcile errors in a detailed manner. This is including resource name and namespace in labels where the default provided isn't 78 | 79 | ### Bugs 80 | 81 | - Fix application port in Helm chart 82 | - Fix unnecessary requeue 83 | 84 | ## 2.1.2 85 | 86 | ### Bugs 87 | 88 | - Change deletion algorithm and add some security to avoid status flush 89 | - Fix pgdb deletion not checking is pgur exists 90 | 91 | ## 2.1.1 92 | 93 | ### Bugs 94 | 95 | - Fix to add support for args in helm chart 96 | - Fix to avoid doing requeue on success run and prefer a full resync 97 | 98 | ## 2.1.0 99 | 100 | ### Deprecation notice 101 | 102 | - Deprecate PostgresqlUser custom resource in favor or PostgresqlUserRole 103 | - This new resource will allow more thing and a greater stability 104 | 105 | ### Feature 106 | 107 | - Add support for PostgresqlUserRole custom resource 108 | 109 | ### Bugs 110 | 111 | - Fix Helm chart CRD and structure 112 | 113 | ## 2.0.0 114 | 115 | ### Feature 116 | 117 | - Complete rework and upgrade of operator-sdk to latest version 118 | 119 | ### Tests 120 | 121 | - Add tests for all controllers 122 | 123 | ### Bugs 124 | 125 | - Patch bugs detected with tests 126 | 127 | ## 1.1.2 128 | 129 | ### Bugs 130 | 131 | - Fix group cannot be used to create a database because admin user isn't in that group 132 | 133 | ## 1.1.1 134 | 135 | ### Bugs 136 | 137 | - Check if errors exist before logging 138 | - Create database with owner directly to avoid having databases with wrong owner 139 | - Fix potential race between default values save and current run of reconciler 140 | 141 | ## 1.1.0 142 | 143 | ### Bugs 144 | 145 | - Fix on dev resources 146 | - Fix autoheal on schema and extensions in databases 147 | - Fix typo in database user secret 148 | 149 | ### Features 150 | 151 | - Keep all pools in memory to avoid recreating them at each synchronization loop 152 | - Check if roles and database don't already exist before trying to create them 153 | 154 | ## 1.0.1 155 | 156 | ### Bugs 157 | 158 | - Fix possible too long name generated for roles (PostgreSQL only support 63 characters maximum for identifiers) 159 | 160 | ## 1.0.0 161 | 162 | ### Features 163 | 164 | - Add support for PostgresqlEngineConfiguration 165 | - Add support for PostgresqlDatabase 166 | - Add support for PostgresqlUser 167 | - Create or update Databases with extensions and schemas 168 | - Create or update Users with rights (Owner, Writer or Reader) 169 | - Connections to multiple PostgreSQL Engines 170 | - Generate secrets for User login and password 171 | - Allow to change User password based on time (e.g: Each 30 days) 172 | -------------------------------------------------------------------------------- /internal/controller/postgresql/postgres/pool_manager.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "database/sql" 5 | "sync" 6 | "time" 7 | ) 8 | 9 | const ( 10 | maxOpenConnections = 5 11 | maxIdleConnections = 1 12 | maxLifeTimeSecond = 60 * time.Second //nolint:revive // Continue with the suffix Second 13 | ) 14 | 15 | // Pool saved structure per postgres engine configuration. 16 | type poolSaved struct { 17 | // This map will save all pools per database 18 | pools *sync.Map 19 | // Username and password are saved because this comes from secret 20 | username string 21 | password string 22 | } 23 | 24 | // Pool manager map per pgec. 25 | var poolManagerStorage = sync.Map{} 26 | 27 | func getOrOpenPool(p *pg, database string) (*sql.DB, error) { 28 | // Create result 29 | var db *sql.DB 30 | 31 | // Check if there is a saved pool in the storage 32 | savInt, ok := poolManagerStorage.Load(p.GetName()) 33 | // Check if this is found 34 | if !ok { 35 | // Open connection 36 | sqlDB, err := openConnection(p, database) 37 | // Check error 38 | if err != nil { 39 | return nil, err 40 | } 41 | 42 | // Create saved pool map with the selected database 43 | psMap := &sync.Map{} 44 | psMap.Store(database, sqlDB) 45 | // Add it to storage 46 | poolManagerStorage.Store(p.GetName(), &poolSaved{ 47 | username: p.GetUser(), 48 | password: p.GetPassword(), 49 | pools: psMap, 50 | }) 51 | 52 | // Result 53 | db = sqlDB 54 | } else { 55 | // Cast saved pool object 56 | sav, _ := savInt.(*poolSaved) 57 | // Check if username and password haven't changed, if yes, close pools and recreate current 58 | if sav.username != p.GetUser() || sav.password != p.GetPassword() { 59 | // Close all pools 60 | err := CloseAllSavedPoolsForName(p.GetName()) 61 | // Check error 62 | if err != nil { 63 | return nil, err 64 | } 65 | } 66 | // Check if we can found a pool for this database 67 | sqlDBInt, ok := sav.pools.Load(database) 68 | // Check if it isn't found 69 | if !ok { 70 | // Open connection 71 | sqlDB, err := openConnection(p, database) 72 | // Check error 73 | if err != nil { 74 | return nil, err 75 | } 76 | 77 | // Save it in pool manager storage 78 | sav.pools.Store(database, sqlDB) 79 | 80 | // Result 81 | db = sqlDB 82 | } else { 83 | // Result 84 | db, _ = sqlDBInt.(*sql.DB) 85 | } 86 | } 87 | 88 | return db, nil 89 | } 90 | 91 | func openConnection(p *pg, database string) (*sql.DB, error) { 92 | // Generate url 93 | pgURL := TemplatePostgresqlURLWithArgs( 94 | p.GetHost(), 95 | p.GetUser(), 96 | p.GetPassword(), 97 | p.GetArgs(), 98 | database, 99 | p.GetPort(), 100 | ) 101 | // Connect 102 | db, err := sql.Open("postgres", pgURL) 103 | // Check error 104 | if err != nil { 105 | return nil, err 106 | } 107 | 108 | // Set sql parameters 109 | // Force connections to 60s max lifetime because operator shouldn't take a slot too longer 110 | db.SetConnMaxLifetime(maxLifeTimeSecond) 111 | // Operator shouldn't take too much slots 112 | db.SetMaxIdleConns(maxIdleConnections) 113 | // Operator shouldn't take too much slots 114 | db.SetMaxOpenConns(maxOpenConnections) 115 | 116 | return db, nil 117 | } 118 | 119 | func CloseDatabaseSavedPoolsForName(name, database string) error { 120 | // Get pool saved 121 | psInt, ok := poolManagerStorage.Load(name) 122 | // Check if it exists 123 | if !ok { 124 | return nil 125 | } 126 | 127 | // Cast pool saved 128 | ps, _ := psInt.(*poolSaved) 129 | 130 | // Get entry 131 | enInt, ok := ps.pools.Load(database) 132 | // Check if it isn't present 133 | if !ok { 134 | return nil 135 | } 136 | 137 | // Cast db 138 | en, _ := enInt.(*sql.DB) 139 | 140 | // Close pool 141 | err := en.Close() 142 | // Check error 143 | if err != nil { 144 | return err 145 | } 146 | 147 | // Clean entry 148 | ps.pools.Delete(database) 149 | 150 | return nil 151 | } 152 | 153 | func CloseAllSavedPoolsForName(name string) error { 154 | // Get pool saved 155 | psInt, ok := poolManagerStorage.Load(name) 156 | // Check if it exists 157 | if !ok { 158 | return nil 159 | } 160 | 161 | // Cast pool saved 162 | ps, _ := psInt.(*poolSaved) 163 | 164 | // Save all keys to be removed 165 | keysToBeRemoved := make([]interface{}, 0) 166 | // Error 167 | var err error 168 | // Loop over pools 169 | ps.pools.Range(func(k, val interface{}) bool { 170 | // Cast sql db 171 | v, _ := val.(*sql.DB) 172 | // Close pool 173 | err = v.Close() 174 | // Check error 175 | if err != nil { 176 | return false 177 | } 178 | 179 | // Save key to be removed 180 | keysToBeRemoved = append(keysToBeRemoved, k) 181 | 182 | // Default 183 | return true 184 | }) 185 | // Loop over keys to remove 186 | for _, v := range keysToBeRemoved { 187 | // Delete key 188 | ps.pools.Delete(v) 189 | } 190 | // Check error 191 | if err != nil { 192 | return err 193 | } 194 | 195 | // Clean main entry 196 | poolManagerStorage.Delete(name) 197 | 198 | return nil 199 | } 200 | -------------------------------------------------------------------------------- /config/default/kustomization.yaml: -------------------------------------------------------------------------------- 1 | # Adds namespace to all resources. 2 | namespace: postgresql-operator-system 3 | 4 | # Value of this field is prepended to the 5 | # names of all resources, e.g. a deployment named 6 | # "wordpress" becomes "alices-wordpress". 7 | # Note that it should also match with the prefix (text before '-') of the namespace 8 | # field above. 9 | namePrefix: postgresql-operator- 10 | 11 | # Labels to add to all resources and selectors. 12 | #labels: 13 | #- includeSelectors: true 14 | # pairs: 15 | # someName: someValue 16 | 17 | resources: 18 | - ../crd 19 | - ../rbac 20 | - ../manager 21 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 22 | # crd/kustomization.yaml 23 | #- ../webhook 24 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 'WEBHOOK' components are required. 25 | #- ../certmanager 26 | # [PROMETHEUS] To enable prometheus monitor, uncomment all sections with 'PROMETHEUS'. 27 | #- ../prometheus 28 | 29 | patchesStrategicMerge: 30 | # Protect the /metrics endpoint by putting it behind auth. 31 | # If you want your controller-manager to expose the /metrics 32 | # endpoint w/o any authn/z, please comment the following line. 33 | - manager_auth_proxy_patch.yaml 34 | 35 | 36 | 37 | # [WEBHOOK] To enable webhook, uncomment all the sections with [WEBHOOK] prefix including the one in 38 | # crd/kustomization.yaml 39 | #- manager_webhook_patch.yaml 40 | 41 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER'. 42 | # Uncomment 'CERTMANAGER' sections in crd/kustomization.yaml to enable the CA injection in the admission webhooks. 43 | # 'CERTMANAGER' needs to be enabled to use ca injection 44 | #- webhookcainjection_patch.yaml 45 | 46 | # [CERTMANAGER] To enable cert-manager, uncomment all sections with 'CERTMANAGER' prefix. 47 | # Uncomment the following replacements to add the cert-manager CA injection annotations 48 | #replacements: 49 | # - source: # Add cert-manager annotation to ValidatingWebhookConfiguration, MutatingWebhookConfiguration and CRDs 50 | # kind: Certificate 51 | # group: cert-manager.io 52 | # version: v1 53 | # name: serving-cert # this name should match the one in certificate.yaml 54 | # fieldPath: .metadata.namespace # namespace of the certificate CR 55 | # targets: 56 | # - select: 57 | # kind: ValidatingWebhookConfiguration 58 | # fieldPaths: 59 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 60 | # options: 61 | # delimiter: '/' 62 | # index: 0 63 | # create: true 64 | # - select: 65 | # kind: MutatingWebhookConfiguration 66 | # fieldPaths: 67 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 68 | # options: 69 | # delimiter: '/' 70 | # index: 0 71 | # create: true 72 | # - select: 73 | # kind: CustomResourceDefinition 74 | # fieldPaths: 75 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 76 | # options: 77 | # delimiter: '/' 78 | # index: 0 79 | # create: true 80 | # - source: 81 | # kind: Certificate 82 | # group: cert-manager.io 83 | # version: v1 84 | # name: serving-cert # this name should match the one in certificate.yaml 85 | # fieldPath: .metadata.name 86 | # targets: 87 | # - select: 88 | # kind: ValidatingWebhookConfiguration 89 | # fieldPaths: 90 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 91 | # options: 92 | # delimiter: '/' 93 | # index: 1 94 | # create: true 95 | # - select: 96 | # kind: MutatingWebhookConfiguration 97 | # fieldPaths: 98 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 99 | # options: 100 | # delimiter: '/' 101 | # index: 1 102 | # create: true 103 | # - select: 104 | # kind: CustomResourceDefinition 105 | # fieldPaths: 106 | # - .metadata.annotations.[cert-manager.io/inject-ca-from] 107 | # options: 108 | # delimiter: '/' 109 | # index: 1 110 | # create: true 111 | # - source: # Add cert-manager annotation to the webhook Service 112 | # kind: Service 113 | # version: v1 114 | # name: webhook-service 115 | # fieldPath: .metadata.name # namespace of the service 116 | # targets: 117 | # - select: 118 | # kind: Certificate 119 | # group: cert-manager.io 120 | # version: v1 121 | # fieldPaths: 122 | # - .spec.dnsNames.0 123 | # - .spec.dnsNames.1 124 | # options: 125 | # delimiter: '.' 126 | # index: 0 127 | # create: true 128 | # - source: 129 | # kind: Service 130 | # version: v1 131 | # name: webhook-service 132 | # fieldPath: .metadata.namespace # namespace of the service 133 | # targets: 134 | # - select: 135 | # kind: Certificate 136 | # group: cert-manager.io 137 | # version: v1 138 | # fieldPaths: 139 | # - .spec.dnsNames.0 140 | # - .spec.dnsNames.1 141 | # options: 142 | # delimiter: '.' 143 | # index: 1 144 | # create: true 145 | -------------------------------------------------------------------------------- /api/postgresql/v1alpha1/postgresqldatabase_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 v1alpha1 18 | 19 | import ( 20 | "github.com/easymile/postgresql-operator/api/postgresql/common" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 25 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 26 | 27 | // PostgresqlDatabaseSpec defines the desired state of PostgresqlDatabase. 28 | type PostgresqlDatabaseSpec struct { 29 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 30 | // Important: Run "make" to regenerate code after modifying this file 31 | 32 | // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html 33 | 34 | // Database name 35 | // +required 36 | // +kubebuilder:validation:Required 37 | // +kubebuilder:validation:MinLength=1 38 | Database string `json:"database"` 39 | // Master role name will be used to create top group role. 40 | // Database owner and users will be in this group role. 41 | // +optional 42 | MasterRole string `json:"masterRole,omitempty"` 43 | // Should drop database on Custom Resource deletion ? 44 | // +optional 45 | DropOnDelete bool `json:"dropOnDelete,omitempty"` 46 | // Wait for linked resource to be deleted 47 | // +optional 48 | WaitLinkedResourcesDeletion bool `json:"waitLinkedResourcesDeletion,omitempty"` 49 | // Schema to create in database 50 | // +optional 51 | Schemas DatabaseModulesList `json:"schemas,omitempty"` 52 | // Extensions to enable 53 | // +optional 54 | Extensions DatabaseModulesList `json:"extensions,omitempty"` 55 | // Postgresql Engine Configuration link 56 | // +required 57 | // +kubebuilder:validation:Required 58 | EngineConfiguration *common.CRLink `json:"engineConfiguration"` 59 | } 60 | 61 | type DatabaseModulesList struct { 62 | // Modules list 63 | // +optional 64 | // +listType=set 65 | List []string `json:"list,omitempty"` 66 | // Should drop on delete ? 67 | // +optional 68 | DropOnOnDelete bool `json:"dropOnDelete,omitempty"` 69 | // Should drop with cascade ? 70 | // +optional 71 | DeleteWithCascade bool `json:"deleteWithCascade,omitempty"` 72 | } 73 | 74 | type DatabaseStatusPhase string 75 | 76 | const DatabaseNoPhase DatabaseStatusPhase = "" 77 | const DatabaseFailedPhase DatabaseStatusPhase = "Failed" 78 | const DatabaseCreatedPhase DatabaseStatusPhase = "Created" 79 | 80 | // PostgresqlDatabaseStatus defines the observed state of PostgresqlDatabase. 81 | type PostgresqlDatabaseStatus struct { 82 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 83 | // Important: Run "make" to regenerate code after modifying this file 84 | // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html 85 | 86 | // Current phase of the operator 87 | Phase DatabaseStatusPhase `json:"phase"` 88 | // Human-readable message indicating details about current operator phase or error. 89 | // +optional 90 | Message string `json:"message"` 91 | // True if all resources are in a ready state and all work is done. 92 | // +optional 93 | Ready bool `json:"ready"` 94 | // Created database 95 | // +optional 96 | Database string `json:"database"` 97 | // Already created roles for database 98 | // +optional 99 | Roles StatusPostgresRoles `json:"roles"` 100 | // Already created schemas 101 | // +optional 102 | // +listType=set 103 | Schemas []string `json:"schemas,omitempty"` 104 | // Already extensions added 105 | // +optional 106 | // +listType=set 107 | Extensions []string `json:"extensions,omitempty"` 108 | } 109 | 110 | // StatusPostgresRoles stores the different group roles already created for database 111 | // +k8s:openapi-gen=true 112 | type StatusPostgresRoles struct { 113 | Owner string `json:"owner"` 114 | Reader string `json:"reader"` 115 | Writer string `json:"writer"` 116 | } 117 | 118 | //+kubebuilder:object:root=true 119 | // +kubebuilder:subresource:status 120 | // +kubebuilder:resource:path=postgresqldatabases,scope=Namespaced,shortName=pgdb 121 | // +kubebuilder:printcolumn:name="Database",type=string,description="Database name",JSONPath=".status.database" 122 | // +kubebuilder:printcolumn:name="Schemas",type=string,description="Schemas",JSONPath=".status.schemas" 123 | // +kubebuilder:printcolumn:name="Extensions",type=string,description="Extensions",JSONPath=".status.extensions" 124 | // +kubebuilder:printcolumn:name="Phase",type=string,description="Status phase",JSONPath=".status.phase" 125 | // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" 126 | 127 | // PostgresqlDatabase is the Schema for the postgresqldatabases API. 128 | type PostgresqlDatabase struct { 129 | metav1.TypeMeta `json:",inline"` 130 | metav1.ObjectMeta `json:"metadata,omitempty"` 131 | 132 | Spec PostgresqlDatabaseSpec `json:"spec,omitempty"` 133 | Status PostgresqlDatabaseStatus `json:"status,omitempty"` 134 | } 135 | 136 | //+kubebuilder:object:root=true 137 | 138 | // PostgresqlDatabaseList contains a list of PostgresqlDatabase. 139 | type PostgresqlDatabaseList struct { 140 | metav1.TypeMeta `json:",inline"` 141 | metav1.ListMeta `json:"metadata,omitempty"` 142 | Items []PostgresqlDatabase `json:"items"` 143 | } 144 | 145 | func init() { 146 | SchemeBuilder.Register(&PostgresqlDatabase{}, &PostgresqlDatabaseList{}) 147 | } 148 | -------------------------------------------------------------------------------- /api/postgresql/v1alpha1/postgresqlpublication_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 v1alpha1 18 | 19 | import ( 20 | "github.com/easymile/postgresql-operator/api/postgresql/common" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 25 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 26 | 27 | // PostgresqlPublicationSpec defines the desired state of PostgresqlPublication. 28 | type PostgresqlPublicationSpec struct { 29 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 30 | // Important: Run "make" to regenerate code after modifying this file 31 | 32 | // Postgresql Database 33 | // +required 34 | // +kubebuilder:validation:Required 35 | Database *common.CRLink `json:"database"` 36 | // Postgresql Publication name 37 | // +required 38 | // +kubebuilder:validation:Required 39 | Name string `json:"name"` 40 | // Postgresql replication slot name 41 | // Default value will the publication name 42 | // +optional 43 | ReplicationSlotName string `json:"replicationSlotName,omitempty"` 44 | // Postgresql replication slot plugin 45 | // Default value will be "pgoutput" 46 | // +optional 47 | ReplicationSlotPlugin string `json:"replicationSlotPlugin,omitempty"` 48 | // Should drop database on Custom Resource deletion ? 49 | // +optional 50 | DropOnDelete bool `json:"dropOnDelete,omitempty"` 51 | // Publication for all tables 52 | // Note: This is mutually exclusive with "tablesInSchema" & "tables" 53 | // +optional 54 | AllTables bool `json:"allTables,omitempty"` 55 | // Publication for tables in schema 56 | // Note: This is a list of schema 57 | // +optional 58 | TablesInSchema []string `json:"tablesInSchema,omitempty"` 59 | // Publication for selected tables 60 | // +optional 61 | Tables []*PostgresqlPublicationTable `json:"tables,omitempty"` 62 | // Publication with parameters 63 | // +optional 64 | WithParameters *PostgresqlPublicationWith `json:"withParameters,omitempty"` 65 | } 66 | 67 | type PostgresqlPublicationTable struct { 68 | // Table name to use for publication 69 | TableName string `json:"tableName"` 70 | // Columns to export 71 | Columns *[]string `json:"columns,omitempty"` 72 | // Additional WHERE for table 73 | AdditionalWhere *string `json:"additionalWhere,omitempty"` 74 | } 75 | 76 | type PostgresqlPublicationWith struct { 77 | // Publish param 78 | // See here: https://www.postgresql.org/docs/current/sql-createpublication.html#SQL-CREATEPUBLICATION-PARAMS-WITH-PUBLISH 79 | Publish string `json:"publish"` 80 | // Publish via partition root param 81 | // See here: https://www.postgresql.org/docs/current/sql-createpublication.html#SQL-CREATEPUBLICATION-PARAMS-WITH-PUBLISH 82 | PublishViaPartitionRoot *bool `json:"publishViaPartitionRoot,omitempty"` 83 | } 84 | 85 | type PublicationStatusPhase string 86 | 87 | const PublicationNoPhase PublicationStatusPhase = "" 88 | const PublicationFailedPhase PublicationStatusPhase = "Failed" 89 | const PublicationCreatedPhase PublicationStatusPhase = "Created" 90 | 91 | // PostgresqlPublicationStatus defines the observed state of PostgresqlPublication. 92 | type PostgresqlPublicationStatus struct { 93 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 94 | // Important: Run "make" to regenerate code after modifying this file 95 | 96 | // Current phase of the operator 97 | Phase PublicationStatusPhase `json:"phase"` 98 | // Human-readable message indicating details about current operator phase or error. 99 | // +optional 100 | Message string `json:"message"` 101 | // True if all resources are in a ready state and all work is done. 102 | // +optional 103 | Ready bool `json:"ready"` 104 | // Created publication name 105 | // +optional 106 | Name string `json:"name,omitempty"` 107 | // Created replication slot name 108 | // +optional 109 | ReplicationSlotName string `json:"replicationSlotName,omitempty"` 110 | // Created replication slot plugin 111 | // +optional 112 | ReplicationSlotPlugin string `json:"replicationSlotPlugin,omitempty"` 113 | // Marker for save 114 | // +optional 115 | AllTables *bool `json:"allTables,omitempty"` 116 | // Resource Spec hash 117 | // +optional 118 | Hash string `json:"hash,omitempty"` 119 | } 120 | 121 | //+kubebuilder:object:root=true 122 | //+kubebuilder:subresource:status 123 | //+kubebuilder:resource:path=postgresqlpublications,scope=Namespaced,shortName=pgpublication;pgpub 124 | //+kubebuilder:printcolumn:name="Publication",type=string,description="Publication",JSONPath=".status.name" 125 | //+kubebuilder:printcolumn:name="Replication slot name",type=string,description="Status phase",JSONPath=".status.replicationSlotName" 126 | //+kubebuilder:printcolumn:name="Replication slot plugin",type=string,description="Status phase",JSONPath=".status.replicationSlotPlugin" 127 | //+kubebuilder:printcolumn:name="Phase",type=string,description="Status phase",JSONPath=".status.phase" 128 | 129 | // PostgresqlPublication is the Schema for the postgresqlpublications API. 130 | type PostgresqlPublication struct { 131 | metav1.TypeMeta `json:",inline"` 132 | metav1.ObjectMeta `json:"metadata,omitempty"` 133 | 134 | Spec PostgresqlPublicationSpec `json:"spec,omitempty"` 135 | Status PostgresqlPublicationStatus `json:"status,omitempty"` 136 | } 137 | 138 | //+kubebuilder:object:root=true 139 | 140 | // PostgresqlPublicationList contains a list of PostgresqlPublication. 141 | type PostgresqlPublicationList struct { 142 | metav1.TypeMeta `json:",inline"` 143 | metav1.ListMeta `json:"metadata,omitempty"` 144 | Items []PostgresqlPublication `json:"items"` 145 | } 146 | 147 | func init() { 148 | SchemeBuilder.Register(&PostgresqlPublication{}, &PostgresqlPublicationList{}) 149 | } 150 | -------------------------------------------------------------------------------- /grafana/custom-metrics/custom-metrics-dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_PROMETHEUS", 5 | "label": "Prometheus", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | } 11 | ], 12 | "__requires": [ 13 | { 14 | "type": "datasource", 15 | "id": "prometheus", 16 | "name": "Prometheus", 17 | "version": "1.0.0" 18 | } 19 | ], 20 | "annotations": { 21 | "list": [ 22 | { 23 | "builtIn": 1, 24 | "datasource": "-- Grafana --", 25 | "enable": true, 26 | "hide": true, 27 | "iconColor": "rgba(0, 211, 255, 1)", 28 | "name": "Annotations & Alerts", 29 | "target": { 30 | "limit": 100, 31 | "matchAny": false, 32 | "tags": [], 33 | "type": "dashboard" 34 | }, 35 | "type": "dashboard" 36 | } 37 | ] 38 | }, 39 | "editable": true, 40 | "fiscalYearStartMonth": 0, 41 | "graphTooltip": 0, 42 | "links": [], 43 | "liveNow": false, 44 | "panels": [ 45 | { 46 | "datasource": "${DS_PROMETHEUS}", 47 | "fieldConfig": { 48 | "defaults": { 49 | "color": { 50 | "mode": "continuous-GrYlRd" 51 | }, 52 | "custom": { 53 | "axisLabel": "", 54 | "axisPlacement": "auto", 55 | "barAlignment": 0, 56 | "drawStyle": "line", 57 | "fillOpacity": 20, 58 | "gradientMode": "scheme", 59 | "hideFrom": { 60 | "legend": false, 61 | "tooltip": false, 62 | "viz": false 63 | }, 64 | "lineInterpolation": "smooth", 65 | "lineWidth": 3, 66 | "pointSize": 5, 67 | "scaleDistribution": { 68 | "type": "linear" 69 | }, 70 | "showPoints": "auto", 71 | "spanNulls": false, 72 | "stacking": { 73 | "group": "A", 74 | "mode": "none" 75 | }, 76 | "thresholdsStyle": { 77 | "mode": "off" 78 | } 79 | }, 80 | "mappings": [], 81 | "thresholds": { 82 | "mode": "absolute", 83 | "steps": [ 84 | { 85 | "color": "green", 86 | "value": null 87 | }, 88 | { 89 | "color": "red", 90 | "value": 80 91 | } 92 | ] 93 | }, 94 | "unit": "none" 95 | }, 96 | "overrides": [] 97 | }, 98 | "gridPos": { 99 | "h": 7, 100 | "w": 24 101 | }, 102 | "interval": "1m", 103 | "links": [], 104 | "options": { 105 | "legend": { 106 | "calcs": [], 107 | "displayMode": "list", 108 | "placement": "bottom" 109 | }, 110 | "tooltip": { 111 | "mode": "single", 112 | "sort": "none" 113 | } 114 | }, 115 | "pluginVersion": "8.4.3", 116 | "targets": [ 117 | { 118 | "datasource": "${DS_PROMETHEUS}", 119 | "exemplar": true, 120 | "expr": "sum(rate(controller_runtime_reconcile_detailed_errors_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, pod)", 121 | "format": "time_series", 122 | "interval": "", 123 | "intervalFactor": 2, 124 | "refId": "A", 125 | "step": 10 126 | } 127 | ], 128 | "title": "controller_runtime_reconcile_detailed_errors_total (counter)", 129 | "type": "timeseries" 130 | } 131 | ], 132 | "refresh": "", 133 | "style": "dark", 134 | "tags": [], 135 | "templating": { 136 | "list": [ 137 | { 138 | "datasource": "${DS_PROMETHEUS}", 139 | "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)", 140 | "hide": 0, 141 | "includeAll": false, 142 | "multi": false, 143 | "name": "job", 144 | "options": [], 145 | "query": { 146 | "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)", 147 | "refId": "StandardVariableQuery" 148 | }, 149 | "refresh": 2, 150 | "regex": "", 151 | "skipUrlSync": false, 152 | "sort": 0, 153 | "type": "query" 154 | }, 155 | { 156 | "current": { 157 | "selected": false, 158 | "text": "observability", 159 | "value": "observability" 160 | }, 161 | "datasource": "${DS_PROMETHEUS}", 162 | "definition": "label_values(controller_runtime_reconcile_total, namespace)", 163 | "hide": 0, 164 | "includeAll": false, 165 | "multi": false, 166 | "name": "namespace", 167 | "options": [], 168 | "query": { 169 | "query": "label_values(controller_runtime_reconcile_total, namespace)", 170 | "refId": "StandardVariableQuery" 171 | }, 172 | "refresh": 1, 173 | "regex": "", 174 | "skipUrlSync": false, 175 | "sort": 0, 176 | "type": "query" 177 | }, 178 | { 179 | "current": { 180 | "selected": false, 181 | "text": "All", 182 | "value": "$__all" 183 | }, 184 | "datasource": "${DS_PROMETHEUS}", 185 | "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)", 186 | "hide": 2, 187 | "includeAll": true, 188 | "label": "pod", 189 | "multi": true, 190 | "name": "pod", 191 | "options": [], 192 | "query": { 193 | "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)", 194 | "refId": "StandardVariableQuery" 195 | }, 196 | "refresh": 2, 197 | "regex": "", 198 | "skipUrlSync": false, 199 | "sort": 0, 200 | "type": "query" 201 | } 202 | ] 203 | }, 204 | "time": { 205 | "from": "now-15m", 206 | "to": "now" 207 | }, 208 | "timepicker": {}, 209 | "timezone": "", 210 | "title": "Custom-Metrics", 211 | "weekStart": "" 212 | } 213 | -------------------------------------------------------------------------------- /helm/postgresql-operator/files/dashboards/custom-metrics-dashboard.json: -------------------------------------------------------------------------------- 1 | { 2 | "__inputs": [ 3 | { 4 | "name": "DS_PROMETHEUS", 5 | "label": "Prometheus", 6 | "description": "", 7 | "type": "datasource", 8 | "pluginId": "prometheus", 9 | "pluginName": "Prometheus" 10 | } 11 | ], 12 | "__requires": [ 13 | { 14 | "type": "datasource", 15 | "id": "prometheus", 16 | "name": "Prometheus", 17 | "version": "1.0.0" 18 | } 19 | ], 20 | "annotations": { 21 | "list": [ 22 | { 23 | "builtIn": 1, 24 | "datasource": "-- Grafana --", 25 | "enable": true, 26 | "hide": true, 27 | "iconColor": "rgba(0, 211, 255, 1)", 28 | "name": "Annotations & Alerts", 29 | "target": { 30 | "limit": 100, 31 | "matchAny": false, 32 | "tags": [], 33 | "type": "dashboard" 34 | }, 35 | "type": "dashboard" 36 | } 37 | ] 38 | }, 39 | "editable": true, 40 | "fiscalYearStartMonth": 0, 41 | "graphTooltip": 0, 42 | "links": [], 43 | "liveNow": false, 44 | "panels": [ 45 | { 46 | "datasource": "${DS_PROMETHEUS}", 47 | "fieldConfig": { 48 | "defaults": { 49 | "color": { 50 | "mode": "continuous-GrYlRd" 51 | }, 52 | "custom": { 53 | "axisLabel": "", 54 | "axisPlacement": "auto", 55 | "barAlignment": 0, 56 | "drawStyle": "line", 57 | "fillOpacity": 20, 58 | "gradientMode": "scheme", 59 | "hideFrom": { 60 | "legend": false, 61 | "tooltip": false, 62 | "viz": false 63 | }, 64 | "lineInterpolation": "smooth", 65 | "lineWidth": 3, 66 | "pointSize": 5, 67 | "scaleDistribution": { 68 | "type": "linear" 69 | }, 70 | "showPoints": "auto", 71 | "spanNulls": false, 72 | "stacking": { 73 | "group": "A", 74 | "mode": "none" 75 | }, 76 | "thresholdsStyle": { 77 | "mode": "off" 78 | } 79 | }, 80 | "mappings": [], 81 | "thresholds": { 82 | "mode": "absolute", 83 | "steps": [ 84 | { 85 | "color": "green", 86 | "value": null 87 | }, 88 | { 89 | "color": "red", 90 | "value": 80 91 | } 92 | ] 93 | }, 94 | "unit": "none" 95 | }, 96 | "overrides": [] 97 | }, 98 | "gridPos": { 99 | "h": 7, 100 | "w": 24 101 | }, 102 | "interval": "1m", 103 | "links": [], 104 | "options": { 105 | "legend": { 106 | "calcs": [], 107 | "displayMode": "list", 108 | "placement": "bottom" 109 | }, 110 | "tooltip": { 111 | "mode": "single", 112 | "sort": "none" 113 | } 114 | }, 115 | "pluginVersion": "8.4.3", 116 | "targets": [ 117 | { 118 | "datasource": "${DS_PROMETHEUS}", 119 | "exemplar": true, 120 | "expr": "sum(rate(controller_runtime_reconcile_detailed_errors_total{job=\"$job\", namespace=\"$namespace\"}[5m])) by (instance, pod)", 121 | "format": "time_series", 122 | "interval": "", 123 | "intervalFactor": 2, 124 | "refId": "A", 125 | "step": 10 126 | } 127 | ], 128 | "title": "controller_runtime_reconcile_detailed_errors_total (counter)", 129 | "type": "timeseries" 130 | } 131 | ], 132 | "refresh": "", 133 | "style": "dark", 134 | "tags": [], 135 | "templating": { 136 | "list": [ 137 | { 138 | "datasource": "${DS_PROMETHEUS}", 139 | "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)", 140 | "hide": 0, 141 | "includeAll": false, 142 | "multi": false, 143 | "name": "job", 144 | "options": [], 145 | "query": { 146 | "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\"}, job)", 147 | "refId": "StandardVariableQuery" 148 | }, 149 | "refresh": 2, 150 | "regex": "", 151 | "skipUrlSync": false, 152 | "sort": 0, 153 | "type": "query" 154 | }, 155 | { 156 | "current": { 157 | "selected": false, 158 | "text": "observability", 159 | "value": "observability" 160 | }, 161 | "datasource": "${DS_PROMETHEUS}", 162 | "definition": "label_values(controller_runtime_reconcile_total, namespace)", 163 | "hide": 0, 164 | "includeAll": false, 165 | "multi": false, 166 | "name": "namespace", 167 | "options": [], 168 | "query": { 169 | "query": "label_values(controller_runtime_reconcile_total, namespace)", 170 | "refId": "StandardVariableQuery" 171 | }, 172 | "refresh": 1, 173 | "regex": "", 174 | "skipUrlSync": false, 175 | "sort": 0, 176 | "type": "query" 177 | }, 178 | { 179 | "current": { 180 | "selected": false, 181 | "text": "All", 182 | "value": "$__all" 183 | }, 184 | "datasource": "${DS_PROMETHEUS}", 185 | "definition": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)", 186 | "hide": 2, 187 | "includeAll": true, 188 | "label": "pod", 189 | "multi": true, 190 | "name": "pod", 191 | "options": [], 192 | "query": { 193 | "query": "label_values(controller_runtime_reconcile_total{namespace=~\"$namespace\", job=~\"$job\"}, pod)", 194 | "refId": "StandardVariableQuery" 195 | }, 196 | "refresh": 2, 197 | "regex": "", 198 | "skipUrlSync": false, 199 | "sort": 0, 200 | "type": "query" 201 | } 202 | ] 203 | }, 204 | "time": { 205 | "from": "now-15m", 206 | "to": "now" 207 | }, 208 | "timepicker": {}, 209 | "timezone": "", 210 | "title": "Custom-Metrics", 211 | "weekStart": "" 212 | } 213 | -------------------------------------------------------------------------------- /api/postgresql/v1alpha1/postgresqlengineconfiguration_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 v1alpha1 18 | 19 | import ( 20 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 21 | ) 22 | 23 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 24 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 25 | 26 | type ProviderType string 27 | 28 | const NoProvider ProviderType = "" 29 | const AWSProvider ProviderType = "AWS" 30 | const AzureProvider ProviderType = "AZURE" 31 | 32 | // PostgresqlEngineConfigurationSpec defines the desired state of PostgresqlEngineConfiguration. 33 | type PostgresqlEngineConfigurationSpec struct { 34 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 35 | // Important: Run "make" to regenerate code after modifying this file 36 | 37 | // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html 38 | 39 | // Provider 40 | // +kubebuilder:validation:Enum="";AWS;AZURE 41 | Provider ProviderType `json:"provider,omitempty"` 42 | // Hostname 43 | // +required 44 | // +kubebuilder:validation:Required 45 | // +kubebuilder:validation:MinLength=1 46 | Host string `json:"host"` 47 | // Port 48 | Port int `json:"port,omitempty"` 49 | // URI args like sslmode, ... 50 | URIArgs string `json:"uriArgs,omitempty"` 51 | // Default database 52 | DefaultDatabase string `json:"defaultDatabase,omitempty"` 53 | // Duration between two checks for valid engine 54 | CheckInterval string `json:"checkInterval,omitempty"` 55 | // Allow grant admin on every created roles (group or user) for provided PGEC user in order to 56 | // have power to administrate those roles even with a less powered "admin" user. 57 | // Operator will create role and after grant PGEC provided user on those roles with admin option if enabled. 58 | AllowGrantAdminOption bool `json:"allowGrantAdminOption,omitempty"` 59 | // Wait for linked resource to be deleted 60 | WaitLinkedResourcesDeletion bool `json:"waitLinkedResourcesDeletion,omitempty"` 61 | // User and password secret 62 | // +required 63 | // +kubebuilder:validation:Required 64 | // +kubebuilder:validation:MinLength=1 65 | SecretName string `json:"secretName"` 66 | // User connections used for secret generation 67 | // That will be used to generate secret with primary server as url or 68 | // to use the pg bouncer one. 69 | // Note: Operator won't check those values. 70 | // +optional 71 | UserConnections *UserConnections `json:"userConnections"` 72 | } 73 | 74 | type UserConnections struct { 75 | // Primary connection is referring to the primary node connection. 76 | // +optional 77 | PrimaryConnection *GenericUserConnection `json:"primaryConnection,omitempty"` 78 | // Bouncer connection is referring to a pg bouncer node. 79 | // +optional 80 | BouncerConnection *GenericUserConnection `json:"bouncerConnection,omitempty"` 81 | // Replica connections are referring to the replica nodes. 82 | // +optional 83 | ReplicaConnections []*GenericUserConnection `json:"replicaConnections,omitempty"` 84 | // Replica Bouncer connections are referring to pg bouncer nodes. 85 | // +optional 86 | ReplicaBouncerConnections []*GenericUserConnection `json:"replicaBouncerConnections,omitempty"` 87 | } 88 | 89 | type GenericUserConnection struct { 90 | // Hostname 91 | // +required 92 | // +kubebuilder:validation:Required 93 | Host string `json:"host"` 94 | // URI args like sslmode, ... 95 | URIArgs string `json:"uriArgs"` 96 | // Port 97 | Port int `json:"port,omitempty"` 98 | } 99 | 100 | type EngineStatusPhase string 101 | 102 | const EngineNoPhase EngineStatusPhase = "" 103 | const EngineFailedPhase EngineStatusPhase = "Failed" 104 | const EngineValidatedPhase EngineStatusPhase = "Validated" 105 | 106 | // PostgresqlEngineConfigurationStatus defines the observed state of PostgresqlEngineConfiguration. 107 | type PostgresqlEngineConfigurationStatus struct { 108 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 109 | // Important: Run "make" to regenerate code after modifying this file 110 | 111 | // Add custom validation using kubebuilder tags: https://book-v1.book.kubebuilder.io/beyond_basics/generating_crd.html 112 | 113 | // Current phase of the operator 114 | Phase EngineStatusPhase `json:"phase"` 115 | // Human-readable message indicating details about current operator phase or error. 116 | // +optional 117 | Message string `json:"message"` 118 | // True if all resources are in a ready state and all work is done. 119 | // +optional 120 | Ready bool `json:"ready"` 121 | // Last validated time 122 | // +optional 123 | LastValidatedTime string `json:"lastValidatedTime"` 124 | // Resource Spec hash 125 | // +optional 126 | Hash string `json:"hash"` 127 | } 128 | 129 | //+kubebuilder:object:root=true 130 | //+kubebuilder:subresource:status 131 | // +kubebuilder:resource:path=postgresqlengineconfigurations,scope=Namespaced,shortName=pgengcfg;pgec 132 | // +kubebuilder:printcolumn:name="Last Validation",type=date,description="Last time validated",JSONPath=".status.lastValidatedTime" 133 | // +kubebuilder:printcolumn:name="Phase",type=string,description="Status phase",JSONPath=".status.phase" 134 | // +kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" 135 | 136 | // PostgresqlEngineConfiguration is the Schema for the postgresqlengineconfigurations API. 137 | type PostgresqlEngineConfiguration struct { 138 | metav1.TypeMeta `json:",inline"` 139 | metav1.ObjectMeta `json:"metadata,omitempty"` 140 | 141 | Spec PostgresqlEngineConfigurationSpec `json:"spec,omitempty"` 142 | Status PostgresqlEngineConfigurationStatus `json:"status,omitempty"` 143 | } 144 | 145 | //+kubebuilder:object:root=true 146 | 147 | // PostgresqlEngineConfigurationList contains a list of PostgresqlEngineConfiguration. 148 | type PostgresqlEngineConfigurationList struct { 149 | metav1.TypeMeta `json:",inline"` 150 | metav1.ListMeta `json:"metadata,omitempty"` 151 | Items []PostgresqlEngineConfiguration `json:"items"` 152 | } 153 | 154 | func init() { 155 | SchemeBuilder.Register(&PostgresqlEngineConfiguration{}, &PostgresqlEngineConfigurationList{}) 156 | } 157 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

PostgreSQL Operator

2 | 3 |

4 | Go Doc 5 | Github Actions 6 | Go Report Card 7 | Docker Pulls 8 | GitHub license 9 | GitHub release (latest by date) 10 |

11 | 12 | ## Features 13 | 14 | - Create or update Databases with extensions and schemas 15 | - Create or update Users with rights (Owner, Writer or Reader) 16 | - Connections to multiple PostgreSQL Engines 17 | - Generate secrets for User login and password 18 | - Allow to change User password based on time (e.g: Each 30 days) 19 | 20 | ## Concepts 21 | 22 | When we speak about `Engines`, we speak about PostgreSQL Database Servers. It isn't the same as Databases. Databases will store tables, ... 23 | 24 | In this operator, Users are linked to Databases and doesn't exist without it. They are "children" of databases. 25 | 26 | Moreover, a single User can only have rights to one Database. 27 | 28 | ## Supported Custom Resources 29 | 30 | | CustomResourceDefinition | Description | 31 | | --------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | 32 | | [PostgresqlEngineConfiguration](docs/crds/PostgresqlEngineConfiguration.md) | Represents a PostgreSQL Engine Configuration with all necessary data to connect it | 33 | | [PostgresqlDatabase](docs/crds/PostgresqlDatabase.md) | Represents a PostgreSQL Database | 34 | | [PostgresqlUserRole](docs/crds/PostgresqlUserRole.md) | Represents a PostgreSQL User Role | 35 | | [PostgresqlPublication](docs/crds/PostgresqlPublication.md) | Represents a PostgreSQL Publication | 36 | 37 | ## How to deploy ? 38 | 39 | ### Using Helm 40 | 41 | #### From EasyMile Helm Chart Repository 42 | 43 | ```bash 44 | helm repo add easymile https://easymile.github.io/helm-charts/ 45 | ``` 46 | 47 | And then deploy: 48 | 49 | ```bash 50 | helm install postgresql-operator easymile/postgresql-operator 51 | ``` 52 | 53 | #### From Git 54 | 55 | The project has a Helm 3 chart located in `deploy/helm/postgresql-operator`. 56 | 57 | It will deploy the operator running the command: 58 | 59 | ```bash 60 | helm install postgresql-operator ./helm/postgresql-operator 61 | ``` 62 | 63 | ## Getting Started 64 | 65 | You’ll need a Kubernetes cluster to run against. You can use [KIND](https://sigs.k8s.io/kind) to get a local cluster for testing, or run against a remote cluster. 66 | **Note:** Your controller will automatically use the current context in your kubeconfig file (i.e. whatever cluster `kubectl cluster-info` shows). 67 | 68 | Read how to setup your environment [here](./docs/how-to/setup-local.md) 69 | 70 | ### Running on the cluster 71 | 72 | 1. Install Instances of Custom Resources: 73 | 74 | ```sh 75 | kubectl apply -f config/samples/ 76 | ``` 77 | 78 | 2. Build and push your image to the location specified by `IMG`: 79 | 80 | ```sh 81 | make docker-build docker-push IMG=/postgresql-operator:tag 82 | ``` 83 | 84 | 3. Deploy the controller to the cluster with the image specified by `IMG`: 85 | 86 | ```sh 87 | make deploy IMG=/postgresql-operator:tag 88 | ``` 89 | 90 | ### Uninstall CRDs 91 | 92 | To delete the CRDs from the cluster: 93 | 94 | ```sh 95 | make uninstall 96 | ``` 97 | 98 | ### Undeploy controller 99 | 100 | UnDeploy the controller to the cluster: 101 | 102 | ```sh 103 | make undeploy 104 | ``` 105 | 106 | ## Contributing 107 | 108 | Read the [CONTRIBUTING guide](./CONTRIBUTING.md) 109 | 110 | ### How it works 111 | 112 | This project aims to follow the Kubernetes [Operator pattern](https://kubernetes.io/docs/concepts/extend-kubernetes/operator/) 113 | 114 | It uses [Controllers](https://kubernetes.io/docs/concepts/architecture/controller/) 115 | which provides a reconcile function responsible for synchronizing resources untile the desired state is reached on the cluster 116 | 117 | ### Test It Out 118 | 119 | 1. Install the CRDs into the cluster: 120 | 121 | ```sh 122 | make install 123 | ``` 124 | 125 | 2. Run your controller (this will run in the foreground, so switch to a new terminal if you want to leave it running): 126 | 127 | ```sh 128 | make run 129 | ``` 130 | 131 | **NOTE:** You can also run this in one step by running: `make install run` 132 | 133 | ### Modifying the API definitions 134 | 135 | If you are editing the API definitions, generate the manifests such as CRs or CRDs using: 136 | 137 | ```sh 138 | make manifests 139 | ``` 140 | 141 | **NOTE:** Run `make --help` for more information on all potential `make` targets 142 | 143 | More information can be found via the [Kubebuilder Documentation](https://book.kubebuilder.io/introduction.html) 144 | 145 | ## License 146 | 147 | Copyright 2022. 148 | 149 | Licensed under the Apache License, Version 2.0 (the "License"); 150 | you may not use this file except in compliance with the License. 151 | You may obtain a copy of the License at 152 | 153 | http://www.apache.org/licenses/LICENSE-2.0 154 | 155 | Unless required by applicable law or agreed to in writing, software 156 | distributed under the License is distributed on an "AS IS" BASIS, 157 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 158 | See the License for the specific language governing permissions and 159 | limitations under the License. 160 | -------------------------------------------------------------------------------- /config/crd/bases/postgresql.easymile.com_postgresqldatabases.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.14.0 7 | name: postgresqldatabases.postgresql.easymile.com 8 | spec: 9 | group: postgresql.easymile.com 10 | names: 11 | kind: PostgresqlDatabase 12 | listKind: PostgresqlDatabaseList 13 | plural: postgresqldatabases 14 | shortNames: 15 | - pgdb 16 | singular: postgresqldatabase 17 | scope: Namespaced 18 | versions: 19 | - additionalPrinterColumns: 20 | - description: Database name 21 | jsonPath: .status.database 22 | name: Database 23 | type: string 24 | - description: Schemas 25 | jsonPath: .status.schemas 26 | name: Schemas 27 | type: string 28 | - description: Extensions 29 | jsonPath: .status.extensions 30 | name: Extensions 31 | type: string 32 | - description: Status phase 33 | jsonPath: .status.phase 34 | name: Phase 35 | type: string 36 | - jsonPath: .metadata.creationTimestamp 37 | name: Age 38 | type: date 39 | name: v1alpha1 40 | schema: 41 | openAPIV3Schema: 42 | description: PostgresqlDatabase is the Schema for the postgresqldatabases 43 | API. 44 | properties: 45 | apiVersion: 46 | description: |- 47 | APIVersion defines the versioned schema of this representation of an object. 48 | Servers should convert recognized schemas to the latest internal value, and 49 | may reject unrecognized values. 50 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 51 | type: string 52 | kind: 53 | description: |- 54 | Kind is a string value representing the REST resource this object represents. 55 | Servers may infer this from the endpoint the client submits requests to. 56 | Cannot be updated. 57 | In CamelCase. 58 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 59 | type: string 60 | metadata: 61 | type: object 62 | spec: 63 | description: PostgresqlDatabaseSpec defines the desired state of PostgresqlDatabase. 64 | properties: 65 | database: 66 | description: Database name 67 | minLength: 1 68 | type: string 69 | dropOnDelete: 70 | description: Should drop database on Custom Resource deletion ? 71 | type: boolean 72 | engineConfiguration: 73 | description: Postgresql Engine Configuration link 74 | properties: 75 | name: 76 | description: Custom resource name 77 | type: string 78 | namespace: 79 | description: Custom resource namespace 80 | type: string 81 | required: 82 | - name 83 | type: object 84 | extensions: 85 | description: Extensions to enable 86 | properties: 87 | deleteWithCascade: 88 | description: Should drop with cascade ? 89 | type: boolean 90 | dropOnDelete: 91 | description: Should drop on delete ? 92 | type: boolean 93 | list: 94 | description: Modules list 95 | items: 96 | type: string 97 | type: array 98 | x-kubernetes-list-type: set 99 | type: object 100 | masterRole: 101 | description: |- 102 | Master role name will be used to create top group role. 103 | Database owner and users will be in this group role. 104 | type: string 105 | schemas: 106 | description: Schema to create in database 107 | properties: 108 | deleteWithCascade: 109 | description: Should drop with cascade ? 110 | type: boolean 111 | dropOnDelete: 112 | description: Should drop on delete ? 113 | type: boolean 114 | list: 115 | description: Modules list 116 | items: 117 | type: string 118 | type: array 119 | x-kubernetes-list-type: set 120 | type: object 121 | waitLinkedResourcesDeletion: 122 | description: Wait for linked resource to be deleted 123 | type: boolean 124 | required: 125 | - database 126 | - engineConfiguration 127 | type: object 128 | status: 129 | description: PostgresqlDatabaseStatus defines the observed state of PostgresqlDatabase. 130 | properties: 131 | database: 132 | description: Created database 133 | type: string 134 | extensions: 135 | description: Already extensions added 136 | items: 137 | type: string 138 | type: array 139 | x-kubernetes-list-type: set 140 | message: 141 | description: Human-readable message indicating details about current 142 | operator phase or error. 143 | type: string 144 | phase: 145 | description: Current phase of the operator 146 | type: string 147 | ready: 148 | description: True if all resources are in a ready state and all work 149 | is done. 150 | type: boolean 151 | roles: 152 | description: Already created roles for database 153 | properties: 154 | owner: 155 | type: string 156 | reader: 157 | type: string 158 | writer: 159 | type: string 160 | required: 161 | - owner 162 | - reader 163 | - writer 164 | type: object 165 | schemas: 166 | description: Already created schemas 167 | items: 168 | type: string 169 | type: array 170 | x-kubernetes-list-type: set 171 | required: 172 | - phase 173 | type: object 174 | type: object 175 | served: true 176 | storage: true 177 | subresources: 178 | status: {} 179 | -------------------------------------------------------------------------------- /internal/controller/postgresql/postgres/postgres.go: -------------------------------------------------------------------------------- 1 | package postgres 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "fmt" 7 | 8 | "github.com/easymile/postgresql-operator/api/postgresql/v1alpha1" 9 | "github.com/go-logr/logr" 10 | ) 11 | 12 | const MaxIdentifierLength = 63 13 | 14 | type SetRoleOnDatabaseRoleSetting struct { 15 | Role string 16 | Database string 17 | } 18 | 19 | type TableOwnership struct { 20 | TableName string 21 | Owner string 22 | } 23 | 24 | type TypeOwnership struct { 25 | TypeName string 26 | Owner string 27 | } 28 | 29 | type PG interface { //nolint:interfacebloat // This is needed 30 | CreateDB(ctx context.Context, dbname, username string) error 31 | GetDatabaseOwner(ctx context.Context, dbname string) (string, error) 32 | ChangeDBOwner(ctx context.Context, dbname, owner string) error 33 | IsDatabaseExist(ctx context.Context, dbname string) (bool, error) 34 | RenameDatabase(ctx context.Context, oldname, newname string) error 35 | CreateSchema(ctx context.Context, db, role, schema string) error 36 | CreateExtension(ctx context.Context, db, extension string) error 37 | CreateGroupRole(ctx context.Context, role string) error 38 | CreateUserRole(ctx context.Context, role, password string, attributes *RoleAttributes) (string, error) 39 | AlterRoleAttributes(ctx context.Context, role string, attributes *RoleAttributes) error 40 | GetRoleAttributes(ctx context.Context, role string) (*RoleAttributes, error) 41 | IsRoleExist(ctx context.Context, role string) (bool, error) 42 | RenameRole(ctx context.Context, oldname, newname string) error 43 | UpdatePassword(ctx context.Context, role, password string) error 44 | GrantRole(ctx context.Context, role, grantee string, withAdminOption bool) error 45 | SetSchemaPrivileges(ctx context.Context, db, creator, role, schema, privs string) error 46 | RevokeRole(ctx context.Context, role, userRole string) error 47 | AlterDefaultLoginRole(ctx context.Context, role, setRole string) error 48 | AlterDefaultLoginRoleOnDatabase(ctx context.Context, role, setRole, database string) error 49 | RevokeUserSetRoleOnDatabase(ctx context.Context, role, database string) error 50 | DoesRoleHaveActiveSession(ctx context.Context, role string) (bool, error) 51 | DropDatabase(ctx context.Context, db string) error 52 | DropRoleAndDropAndChangeOwnedBy(ctx context.Context, role, newOwner, database string) error 53 | ChangeAndDropOwnedBy(ctx context.Context, role, newOwner, database string) error 54 | GetSetRoleOnDatabasesRoleSettings(ctx context.Context, role string) ([]*SetRoleOnDatabaseRoleSetting, error) 55 | DropRole(ctx context.Context, role string) error 56 | DropSchema(ctx context.Context, database, schema string, cascade bool) error 57 | ListSchema(ctx context.Context, database string) ([]string, error) 58 | ListExtensions(ctx context.Context, database string) ([]string, error) 59 | DropExtension(ctx context.Context, database, extension string, cascade bool) error 60 | GetRoleMembership(ctx context.Context, role string) ([]string, error) 61 | GetTablesInSchema(ctx context.Context, db, schema string) ([]*TableOwnership, error) 62 | ChangeTableOwner(ctx context.Context, db, table, owner string) error 63 | GetTypesInSchema(ctx context.Context, db, schema string) ([]*TypeOwnership, error) 64 | ChangeTypeOwnerInSchema(ctx context.Context, db, schema, typeName, owner string) error 65 | DropPublication(ctx context.Context, dbname, name string) error 66 | RenamePublication(ctx context.Context, dbname, oldname, newname string) error 67 | GetPublication(ctx context.Context, dbname, name string) (*PublicationResult, error) 68 | CreatePublication(ctx context.Context, dbname string, builder *CreatePublicationBuilder) error 69 | UpdatePublication(ctx context.Context, dbname, publicationName string, builder *UpdatePublicationBuilder) error 70 | ChangePublicationOwner(ctx context.Context, dbname string, publicationName string, owner string) error 71 | GetPublicationTablesDetails(ctx context.Context, db, publicationName string) ([]*PublicationTableDetail, error) 72 | DropReplicationSlot(ctx context.Context, name string) error 73 | CreateReplicationSlot(ctx context.Context, dbname, name, plugin string) error 74 | GetReplicationSlot(ctx context.Context, name string) (*ReplicationSlotResult, error) 75 | GetColumnNamesFromTable(ctx context.Context, database string, schemaName string, tableName string) ([]string, error) 76 | GetUser() string 77 | GetHost() string 78 | GetPort() int 79 | GetDefaultDatabase() string 80 | GetArgs() string 81 | Ping(ctx context.Context) error 82 | } 83 | 84 | type pg struct { 85 | db *sql.DB 86 | log logr.Logger 87 | host string 88 | user string 89 | pass string 90 | args string 91 | defaultDatabase string 92 | name string 93 | port int 94 | } 95 | 96 | func NewPG( 97 | name, 98 | host, 99 | user, 100 | password, 101 | args, 102 | defaultDatabase string, 103 | port int, 104 | cloudType v1alpha1.ProviderType, 105 | logger logr.Logger, 106 | ) PG { 107 | postgres := &pg{ 108 | log: logger, 109 | host: host, 110 | port: port, 111 | user: user, 112 | pass: password, 113 | args: args, 114 | defaultDatabase: defaultDatabase, 115 | name: name, 116 | } 117 | 118 | switch cloudType { 119 | case v1alpha1.AWSProvider: 120 | return newAWSPG(postgres) 121 | case v1alpha1.AzureProvider: 122 | return newAzurePG(postgres) 123 | default: 124 | return postgres 125 | } 126 | } 127 | 128 | func (c *pg) GetUser() string { 129 | return c.user 130 | } 131 | 132 | func (c *pg) GetHost() string { 133 | return c.host 134 | } 135 | 136 | func (c *pg) GetArgs() string { 137 | return c.args 138 | } 139 | 140 | func (c *pg) GetPort() int { 141 | return c.port 142 | } 143 | 144 | func (c *pg) GetDefaultDatabase() string { 145 | return c.defaultDatabase 146 | } 147 | 148 | func (c *pg) GetName() string { 149 | return c.name 150 | } 151 | 152 | func (c *pg) GetPassword() string { 153 | return c.pass 154 | } 155 | 156 | func (c *pg) connect(database string) error { 157 | // Open or create pool 158 | db, err := getOrOpenPool(c, database) 159 | // Check error 160 | if err != nil { 161 | return err 162 | } 163 | // Save db 164 | c.db = db 165 | 166 | return nil 167 | } 168 | 169 | func (c *pg) Ping(ctx context.Context) error { 170 | err := c.connect(c.defaultDatabase) 171 | if err != nil { 172 | return err 173 | } 174 | 175 | err = c.db.PingContext(ctx) 176 | if err != nil { 177 | return err 178 | } 179 | 180 | return nil 181 | } 182 | 183 | func TemplatePostgresqlURLWithArgs(host, user, password, uriArgs, database string, port int) string { 184 | return fmt.Sprintf("postgresql://%s:%s@%s:%d/%s?%s", user, password, host, port, database, uriArgs) 185 | } 186 | 187 | func TemplatePostgresqlURL(host, user, password, database string, port int) string { 188 | return fmt.Sprintf("postgresql://%s:%s@%s:%d/%s", user, password, host, port, database) 189 | } 190 | -------------------------------------------------------------------------------- /helm/postgresql-operator/crds/postgresql.easymile.com_postgresqldatabases.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.14.0 7 | name: postgresqldatabases.postgresql.easymile.com 8 | spec: 9 | group: postgresql.easymile.com 10 | names: 11 | kind: PostgresqlDatabase 12 | listKind: PostgresqlDatabaseList 13 | plural: postgresqldatabases 14 | shortNames: 15 | - pgdb 16 | singular: postgresqldatabase 17 | scope: Namespaced 18 | versions: 19 | - additionalPrinterColumns: 20 | - description: Database name 21 | jsonPath: .status.database 22 | name: Database 23 | type: string 24 | - description: Schemas 25 | jsonPath: .status.schemas 26 | name: Schemas 27 | type: string 28 | - description: Extensions 29 | jsonPath: .status.extensions 30 | name: Extensions 31 | type: string 32 | - description: Status phase 33 | jsonPath: .status.phase 34 | name: Phase 35 | type: string 36 | - jsonPath: .metadata.creationTimestamp 37 | name: Age 38 | type: date 39 | name: v1alpha1 40 | schema: 41 | openAPIV3Schema: 42 | description: PostgresqlDatabase is the Schema for the postgresqldatabases 43 | API. 44 | properties: 45 | apiVersion: 46 | description: |- 47 | APIVersion defines the versioned schema of this representation of an object. 48 | Servers should convert recognized schemas to the latest internal value, and 49 | may reject unrecognized values. 50 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 51 | type: string 52 | kind: 53 | description: |- 54 | Kind is a string value representing the REST resource this object represents. 55 | Servers may infer this from the endpoint the client submits requests to. 56 | Cannot be updated. 57 | In CamelCase. 58 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 59 | type: string 60 | metadata: 61 | type: object 62 | spec: 63 | description: PostgresqlDatabaseSpec defines the desired state of PostgresqlDatabase. 64 | properties: 65 | database: 66 | description: Database name 67 | minLength: 1 68 | type: string 69 | dropOnDelete: 70 | description: Should drop database on Custom Resource deletion ? 71 | type: boolean 72 | engineConfiguration: 73 | description: Postgresql Engine Configuration link 74 | properties: 75 | name: 76 | description: Custom resource name 77 | type: string 78 | namespace: 79 | description: Custom resource namespace 80 | type: string 81 | required: 82 | - name 83 | type: object 84 | extensions: 85 | description: Extensions to enable 86 | properties: 87 | deleteWithCascade: 88 | description: Should drop with cascade ? 89 | type: boolean 90 | dropOnDelete: 91 | description: Should drop on delete ? 92 | type: boolean 93 | list: 94 | description: Modules list 95 | items: 96 | type: string 97 | type: array 98 | x-kubernetes-list-type: set 99 | type: object 100 | masterRole: 101 | description: |- 102 | Master role name will be used to create top group role. 103 | Database owner and users will be in this group role. 104 | type: string 105 | schemas: 106 | description: Schema to create in database 107 | properties: 108 | deleteWithCascade: 109 | description: Should drop with cascade ? 110 | type: boolean 111 | dropOnDelete: 112 | description: Should drop on delete ? 113 | type: boolean 114 | list: 115 | description: Modules list 116 | items: 117 | type: string 118 | type: array 119 | x-kubernetes-list-type: set 120 | type: object 121 | waitLinkedResourcesDeletion: 122 | description: Wait for linked resource to be deleted 123 | type: boolean 124 | required: 125 | - database 126 | - engineConfiguration 127 | type: object 128 | status: 129 | description: PostgresqlDatabaseStatus defines the observed state of PostgresqlDatabase. 130 | properties: 131 | database: 132 | description: Created database 133 | type: string 134 | extensions: 135 | description: Already extensions added 136 | items: 137 | type: string 138 | type: array 139 | x-kubernetes-list-type: set 140 | message: 141 | description: Human-readable message indicating details about current 142 | operator phase or error. 143 | type: string 144 | phase: 145 | description: Current phase of the operator 146 | type: string 147 | ready: 148 | description: True if all resources are in a ready state and all work 149 | is done. 150 | type: boolean 151 | roles: 152 | description: Already created roles for database 153 | properties: 154 | owner: 155 | type: string 156 | reader: 157 | type: string 158 | writer: 159 | type: string 160 | required: 161 | - owner 162 | - reader 163 | - writer 164 | type: object 165 | schemas: 166 | description: Already created schemas 167 | items: 168 | type: string 169 | type: array 170 | x-kubernetes-list-type: set 171 | required: 172 | - phase 173 | type: object 174 | type: object 175 | served: true 176 | storage: true 177 | subresources: 178 | status: {} 179 | -------------------------------------------------------------------------------- /api/postgresql/v1alpha1/postgresqluserrole_types.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2022. 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 v1alpha1 18 | 19 | import ( 20 | "github.com/easymile/postgresql-operator/api/postgresql/common" 21 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 22 | ) 23 | 24 | // EDIT THIS FILE! THIS IS SCAFFOLDING FOR YOU TO OWN! 25 | // NOTE: json tags are required. Any new fields you add must have json tags for the fields to be serialized. 26 | 27 | type PrivilegesSpecEnum string 28 | 29 | const OwnerPrivilege PrivilegesSpecEnum = "OWNER" 30 | const ReaderPrivilege PrivilegesSpecEnum = "READER" 31 | const WriterPrivilege PrivilegesSpecEnum = "WRITER" 32 | 33 | type ConnectionTypesSpecEnum string 34 | 35 | const PrimaryConnectionType ConnectionTypesSpecEnum = "PRIMARY" 36 | const BouncerConnectionType ConnectionTypesSpecEnum = "BOUNCER" 37 | 38 | type PostgresqlUserRolePrivilege struct { 39 | // User Connection type. 40 | // This is referring to the user connection type needed for this user. 41 | // +optional 42 | // +kubebuilder:default=PRIMARY 43 | // +kubebuilder:validation:Enum=PRIMARY;BOUNCER 44 | ConnectionType ConnectionTypesSpecEnum `json:"connectionType,omitempty"` 45 | // User privileges 46 | // +required 47 | // +kubebuilder:validation:Required 48 | // +kubebuilder:validation:Enum=OWNER;WRITER;READER 49 | Privilege PrivilegesSpecEnum `json:"privilege"` 50 | // Postgresql Database 51 | // +required 52 | // +kubebuilder:validation:Required 53 | Database *common.CRLink `json:"database"` 54 | // Generated secret name prefix 55 | // +required 56 | // +kubebuilder:validation:Required 57 | // +kubebuilder:validation:MinLength=1 58 | GeneratedSecretName string `json:"generatedSecretName"` 59 | // Extra connection URL Parameters 60 | ExtraConnectionURLParameters map[string]string `json:"extraConnectionUrlParameters,omitempty"` 61 | } 62 | 63 | type PostgresqlUserRoleAttributes struct { 64 | // REPLICATION attribute 65 | // Note: This can be either true, false or null (to ignore this parameter) 66 | Replication *bool `json:"replication,omitempty"` 67 | // BYPASSRLS attribute 68 | // Note: This can be either true, false or null (to ignore this parameter) 69 | BypassRLS *bool `json:"bypassRLS,omitempty"` //nolint:tagliatelle 70 | // CONNECTION LIMIT connlimit attribute 71 | // Note: This can be either -1, a number or null (to ignore this parameter) 72 | // Note: Increase your number by one because operator is using the created user to perform some operations. 73 | ConnectionLimit *int `json:"connectionLimit,omitempty"` 74 | } 75 | 76 | type ModeEnum string 77 | 78 | const ProvidedMode ModeEnum = "PROVIDED" 79 | const ManagedMode ModeEnum = "MANAGED" 80 | 81 | // PostgresqlUserRoleSpec defines the desired state of PostgresqlUserRole. 82 | type PostgresqlUserRoleSpec struct { 83 | // INSERT ADDITIONAL SPEC FIELDS - desired state of cluster 84 | // Important: Run "make" to regenerate code after modifying this file 85 | 86 | // User mode 87 | // +required 88 | // +kubebuilder:validation:Required 89 | // +kubebuilder:validation:Enum=PROVIDED;MANAGED 90 | Mode ModeEnum `json:"mode,omitempty"` 91 | // Privileges 92 | // +required 93 | // +kubebuilder:validation:Required 94 | // +kubebuilder:validation:Min=1 95 | Privileges []*PostgresqlUserRolePrivilege `json:"privileges"` 96 | // User role prefix 97 | // +optional 98 | RolePrefix string `json:"rolePrefix,omitempty"` 99 | // User password rotation duration 100 | // +optional 101 | UserPasswordRotationDuration string `json:"userPasswordRotationDuration,omitempty"` 102 | // Simple user password tuple generated secret name 103 | // +optional 104 | WorkGeneratedSecretName string `json:"workGeneratedSecretName"` 105 | // Import secret name 106 | // +optional 107 | ImportSecretName string `json:"importSecretName,omitempty"` 108 | // Role attributes 109 | // Note: Only attributes that aren't conflicting with operator are supported. 110 | // +optional 111 | RoleAttributes *PostgresqlUserRoleAttributes `json:"roleAttributes,omitempty"` 112 | } 113 | 114 | type UserRoleStatusPhase string 115 | 116 | const UserRoleNoPhase UserRoleStatusPhase = "" 117 | const UserRoleFailedPhase UserRoleStatusPhase = "Failed" 118 | const UserRoleCreatedPhase UserRoleStatusPhase = "Created" 119 | 120 | // PostgresqlUserRoleStatus defines the observed state of PostgresqlUserRole. 121 | type PostgresqlUserRoleStatus struct { 122 | // INSERT ADDITIONAL STATUS FIELD - define observed state of cluster 123 | // Important: Run "make" to regenerate code after modifying this file 124 | 125 | // Current phase of the operator 126 | Phase UserRoleStatusPhase `json:"phase"` 127 | // Human-readable message indicating details about current operator phase or error. 128 | // +optional 129 | Message string `json:"message"` 130 | // True if all resources are in a ready state and all work is done. 131 | // +optional 132 | Ready bool `json:"ready"` 133 | // User role 134 | // +optional 135 | RolePrefix string `json:"roleName"` 136 | // Postgres role for user 137 | // +optional 138 | PostgresRole string `json:"postgresRole"` 139 | // Postgres old roles to cleanup 140 | // +optional 141 | OldPostgresRoles []string `json:"oldPostgresRoles"` 142 | // Last password changed time 143 | // +optional 144 | LastPasswordChangedTime string `json:"lastPasswordChangedTime"` 145 | } 146 | 147 | //+kubebuilder:object:root=true 148 | //+kubebuilder:subresource:status 149 | //+kubebuilder:resource:path=postgresqluserroles,scope=Namespaced,shortName=pguserrole;pgur 150 | //+kubebuilder:printcolumn:name="User role",type=string,description="User role",JSONPath=".status.postgresRole" 151 | //+kubebuilder:printcolumn:name="Last Password Change",type=date,description="Last time the password was changed",JSONPath=".status.lastPasswordChangedTime" 152 | //+kubebuilder:printcolumn:name="Phase",type=string,description="Status phase",JSONPath=".status.phase" 153 | //+kubebuilder:printcolumn:name="Age",type="date",JSONPath=".metadata.creationTimestamp" 154 | 155 | // PostgresqlUserRole is the Schema for the postgresqluserroles API. 156 | type PostgresqlUserRole struct { 157 | metav1.TypeMeta `json:",inline"` 158 | metav1.ObjectMeta `json:"metadata,omitempty"` 159 | 160 | Spec PostgresqlUserRoleSpec `json:"spec,omitempty"` 161 | Status PostgresqlUserRoleStatus `json:"status,omitempty"` 162 | } 163 | 164 | //+kubebuilder:object:root=true 165 | 166 | // PostgresqlUserRoleList contains a list of PostgresqlUserRole. 167 | type PostgresqlUserRoleList struct { 168 | metav1.TypeMeta `json:",inline"` 169 | metav1.ListMeta `json:"metadata,omitempty"` 170 | Items []PostgresqlUserRole `json:"items"` 171 | } 172 | 173 | func init() { 174 | SchemeBuilder.Register(&PostgresqlUserRole{}, &PostgresqlUserRoleList{}) 175 | } 176 | -------------------------------------------------------------------------------- /config/crd/bases/postgresql.easymile.com_postgresqlpublications.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.14.0 7 | name: postgresqlpublications.postgresql.easymile.com 8 | spec: 9 | group: postgresql.easymile.com 10 | names: 11 | kind: PostgresqlPublication 12 | listKind: PostgresqlPublicationList 13 | plural: postgresqlpublications 14 | shortNames: 15 | - pgpublication 16 | - pgpub 17 | singular: postgresqlpublication 18 | scope: Namespaced 19 | versions: 20 | - additionalPrinterColumns: 21 | - description: Publication 22 | jsonPath: .status.name 23 | name: Publication 24 | type: string 25 | - description: Status phase 26 | jsonPath: .status.replicationSlotName 27 | name: Replication slot name 28 | type: string 29 | - description: Status phase 30 | jsonPath: .status.replicationSlotPlugin 31 | name: Replication slot plugin 32 | type: string 33 | - description: Status phase 34 | jsonPath: .status.phase 35 | name: Phase 36 | type: string 37 | name: v1alpha1 38 | schema: 39 | openAPIV3Schema: 40 | description: PostgresqlPublication is the Schema for the postgresqlpublications 41 | API. 42 | properties: 43 | apiVersion: 44 | description: |- 45 | APIVersion defines the versioned schema of this representation of an object. 46 | Servers should convert recognized schemas to the latest internal value, and 47 | may reject unrecognized values. 48 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 49 | type: string 50 | kind: 51 | description: |- 52 | Kind is a string value representing the REST resource this object represents. 53 | Servers may infer this from the endpoint the client submits requests to. 54 | Cannot be updated. 55 | In CamelCase. 56 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 57 | type: string 58 | metadata: 59 | type: object 60 | spec: 61 | description: PostgresqlPublicationSpec defines the desired state of PostgresqlPublication. 62 | properties: 63 | allTables: 64 | description: |- 65 | Publication for all tables 66 | Note: This is mutually exclusive with "tablesInSchema" & "tables" 67 | type: boolean 68 | database: 69 | description: Postgresql Database 70 | properties: 71 | name: 72 | description: Custom resource name 73 | type: string 74 | namespace: 75 | description: Custom resource namespace 76 | type: string 77 | required: 78 | - name 79 | type: object 80 | dropOnDelete: 81 | description: Should drop database on Custom Resource deletion ? 82 | type: boolean 83 | name: 84 | description: Postgresql Publication name 85 | type: string 86 | replicationSlotName: 87 | description: |- 88 | Postgresql replication slot name 89 | Default value will the publication name 90 | type: string 91 | replicationSlotPlugin: 92 | description: |- 93 | Postgresql replication slot plugin 94 | Default value will be "pgoutput" 95 | type: string 96 | tables: 97 | description: Publication for selected tables 98 | items: 99 | properties: 100 | additionalWhere: 101 | description: Additional WHERE for table 102 | type: string 103 | columns: 104 | description: Columns to export 105 | items: 106 | type: string 107 | type: array 108 | tableName: 109 | description: Table name to use for publication 110 | type: string 111 | required: 112 | - tableName 113 | type: object 114 | type: array 115 | tablesInSchema: 116 | description: |- 117 | Publication for tables in schema 118 | Note: This is a list of schema 119 | items: 120 | type: string 121 | type: array 122 | withParameters: 123 | description: Publication with parameters 124 | properties: 125 | publish: 126 | description: |- 127 | Publish param 128 | See here: https://www.postgresql.org/docs/current/sql-createpublication.html#SQL-CREATEPUBLICATION-PARAMS-WITH-PUBLISH 129 | type: string 130 | publishViaPartitionRoot: 131 | description: |- 132 | Publish via partition root param 133 | See here: https://www.postgresql.org/docs/current/sql-createpublication.html#SQL-CREATEPUBLICATION-PARAMS-WITH-PUBLISH 134 | type: boolean 135 | required: 136 | - publish 137 | type: object 138 | required: 139 | - database 140 | - name 141 | type: object 142 | status: 143 | description: PostgresqlPublicationStatus defines the observed state of 144 | PostgresqlPublication. 145 | properties: 146 | allTables: 147 | description: Marker for save 148 | type: boolean 149 | hash: 150 | description: Resource Spec hash 151 | type: string 152 | message: 153 | description: Human-readable message indicating details about current 154 | operator phase or error. 155 | type: string 156 | name: 157 | description: Created publication name 158 | type: string 159 | phase: 160 | description: Current phase of the operator 161 | type: string 162 | ready: 163 | description: True if all resources are in a ready state and all work 164 | is done. 165 | type: boolean 166 | replicationSlotName: 167 | description: Created replication slot name 168 | type: string 169 | replicationSlotPlugin: 170 | description: Created replication slot plugin 171 | type: string 172 | required: 173 | - phase 174 | type: object 175 | type: object 176 | served: true 177 | storage: true 178 | subresources: 179 | status: {} 180 | -------------------------------------------------------------------------------- /helm/postgresql-operator/crds/postgresql.easymile.com_postgresqlpublications.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.14.0 7 | name: postgresqlpublications.postgresql.easymile.com 8 | spec: 9 | group: postgresql.easymile.com 10 | names: 11 | kind: PostgresqlPublication 12 | listKind: PostgresqlPublicationList 13 | plural: postgresqlpublications 14 | shortNames: 15 | - pgpublication 16 | - pgpub 17 | singular: postgresqlpublication 18 | scope: Namespaced 19 | versions: 20 | - additionalPrinterColumns: 21 | - description: Publication 22 | jsonPath: .status.name 23 | name: Publication 24 | type: string 25 | - description: Status phase 26 | jsonPath: .status.replicationSlotName 27 | name: Replication slot name 28 | type: string 29 | - description: Status phase 30 | jsonPath: .status.replicationSlotPlugin 31 | name: Replication slot plugin 32 | type: string 33 | - description: Status phase 34 | jsonPath: .status.phase 35 | name: Phase 36 | type: string 37 | name: v1alpha1 38 | schema: 39 | openAPIV3Schema: 40 | description: PostgresqlPublication is the Schema for the postgresqlpublications 41 | API. 42 | properties: 43 | apiVersion: 44 | description: |- 45 | APIVersion defines the versioned schema of this representation of an object. 46 | Servers should convert recognized schemas to the latest internal value, and 47 | may reject unrecognized values. 48 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 49 | type: string 50 | kind: 51 | description: |- 52 | Kind is a string value representing the REST resource this object represents. 53 | Servers may infer this from the endpoint the client submits requests to. 54 | Cannot be updated. 55 | In CamelCase. 56 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 57 | type: string 58 | metadata: 59 | type: object 60 | spec: 61 | description: PostgresqlPublicationSpec defines the desired state of PostgresqlPublication. 62 | properties: 63 | allTables: 64 | description: |- 65 | Publication for all tables 66 | Note: This is mutually exclusive with "tablesInSchema" & "tables" 67 | type: boolean 68 | database: 69 | description: Postgresql Database 70 | properties: 71 | name: 72 | description: Custom resource name 73 | type: string 74 | namespace: 75 | description: Custom resource namespace 76 | type: string 77 | required: 78 | - name 79 | type: object 80 | dropOnDelete: 81 | description: Should drop database on Custom Resource deletion ? 82 | type: boolean 83 | name: 84 | description: Postgresql Publication name 85 | type: string 86 | replicationSlotName: 87 | description: |- 88 | Postgresql replication slot name 89 | Default value will the publication name 90 | type: string 91 | replicationSlotPlugin: 92 | description: |- 93 | Postgresql replication slot plugin 94 | Default value will be "pgoutput" 95 | type: string 96 | tables: 97 | description: Publication for selected tables 98 | items: 99 | properties: 100 | additionalWhere: 101 | description: Additional WHERE for table 102 | type: string 103 | columns: 104 | description: Columns to export 105 | items: 106 | type: string 107 | type: array 108 | tableName: 109 | description: Table name to use for publication 110 | type: string 111 | required: 112 | - tableName 113 | type: object 114 | type: array 115 | tablesInSchema: 116 | description: |- 117 | Publication for tables in schema 118 | Note: This is a list of schema 119 | items: 120 | type: string 121 | type: array 122 | withParameters: 123 | description: Publication with parameters 124 | properties: 125 | publish: 126 | description: |- 127 | Publish param 128 | See here: https://www.postgresql.org/docs/current/sql-createpublication.html#SQL-CREATEPUBLICATION-PARAMS-WITH-PUBLISH 129 | type: string 130 | publishViaPartitionRoot: 131 | description: |- 132 | Publish via partition root param 133 | See here: https://www.postgresql.org/docs/current/sql-createpublication.html#SQL-CREATEPUBLICATION-PARAMS-WITH-PUBLISH 134 | type: boolean 135 | required: 136 | - publish 137 | type: object 138 | required: 139 | - database 140 | - name 141 | type: object 142 | status: 143 | description: PostgresqlPublicationStatus defines the observed state of 144 | PostgresqlPublication. 145 | properties: 146 | allTables: 147 | description: Marker for save 148 | type: boolean 149 | hash: 150 | description: Resource Spec hash 151 | type: string 152 | message: 153 | description: Human-readable message indicating details about current 154 | operator phase or error. 155 | type: string 156 | name: 157 | description: Created publication name 158 | type: string 159 | phase: 160 | description: Current phase of the operator 161 | type: string 162 | ready: 163 | description: True if all resources are in a ready state and all work 164 | is done. 165 | type: boolean 166 | replicationSlotName: 167 | description: Created replication slot name 168 | type: string 169 | replicationSlotPlugin: 170 | description: Created replication slot plugin 171 | type: string 172 | required: 173 | - phase 174 | type: object 175 | type: object 176 | served: true 177 | storage: true 178 | subresources: 179 | status: {} 180 | -------------------------------------------------------------------------------- /config/crd/bases/postgresql.easymile.com_postgresqluserroles.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | apiVersion: apiextensions.k8s.io/v1 3 | kind: CustomResourceDefinition 4 | metadata: 5 | annotations: 6 | controller-gen.kubebuilder.io/version: v0.14.0 7 | name: postgresqluserroles.postgresql.easymile.com 8 | spec: 9 | group: postgresql.easymile.com 10 | names: 11 | kind: PostgresqlUserRole 12 | listKind: PostgresqlUserRoleList 13 | plural: postgresqluserroles 14 | shortNames: 15 | - pguserrole 16 | - pgur 17 | singular: postgresqluserrole 18 | scope: Namespaced 19 | versions: 20 | - additionalPrinterColumns: 21 | - description: User role 22 | jsonPath: .status.postgresRole 23 | name: User role 24 | type: string 25 | - description: Last time the password was changed 26 | jsonPath: .status.lastPasswordChangedTime 27 | name: Last Password Change 28 | type: date 29 | - description: Status phase 30 | jsonPath: .status.phase 31 | name: Phase 32 | type: string 33 | - jsonPath: .metadata.creationTimestamp 34 | name: Age 35 | type: date 36 | name: v1alpha1 37 | schema: 38 | openAPIV3Schema: 39 | description: PostgresqlUserRole is the Schema for the postgresqluserroles 40 | API. 41 | properties: 42 | apiVersion: 43 | description: |- 44 | APIVersion defines the versioned schema of this representation of an object. 45 | Servers should convert recognized schemas to the latest internal value, and 46 | may reject unrecognized values. 47 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources 48 | type: string 49 | kind: 50 | description: |- 51 | Kind is a string value representing the REST resource this object represents. 52 | Servers may infer this from the endpoint the client submits requests to. 53 | Cannot be updated. 54 | In CamelCase. 55 | More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds 56 | type: string 57 | metadata: 58 | type: object 59 | spec: 60 | description: PostgresqlUserRoleSpec defines the desired state of PostgresqlUserRole. 61 | properties: 62 | importSecretName: 63 | description: Import secret name 64 | type: string 65 | mode: 66 | description: User mode 67 | enum: 68 | - PROVIDED 69 | - MANAGED 70 | type: string 71 | privileges: 72 | description: Privileges 73 | items: 74 | properties: 75 | connectionType: 76 | default: PRIMARY 77 | description: |- 78 | User Connection type. 79 | This is referring to the user connection type needed for this user. 80 | enum: 81 | - PRIMARY 82 | - BOUNCER 83 | type: string 84 | database: 85 | description: Postgresql Database 86 | properties: 87 | name: 88 | description: Custom resource name 89 | type: string 90 | namespace: 91 | description: Custom resource namespace 92 | type: string 93 | required: 94 | - name 95 | type: object 96 | extraConnectionUrlParameters: 97 | additionalProperties: 98 | type: string 99 | description: Extra connection URL Parameters 100 | type: object 101 | generatedSecretName: 102 | description: Generated secret name prefix 103 | minLength: 1 104 | type: string 105 | privilege: 106 | description: User privileges 107 | enum: 108 | - OWNER 109 | - WRITER 110 | - READER 111 | type: string 112 | required: 113 | - database 114 | - generatedSecretName 115 | - privilege 116 | type: object 117 | type: array 118 | roleAttributes: 119 | description: |- 120 | Role attributes 121 | Note: Only attributes that aren't conflicting with operator are supported. 122 | properties: 123 | bypassRLS: 124 | description: |- 125 | BYPASSRLS attribute 126 | Note: This can be either true, false or null (to ignore this parameter) 127 | type: boolean 128 | connectionLimit: 129 | description: |- 130 | CONNECTION LIMIT connlimit attribute 131 | Note: This can be either -1, a number or null (to ignore this parameter) 132 | Note: Increase your number by one because operator is using the created user to perform some operations. 133 | type: integer 134 | replication: 135 | description: |- 136 | REPLICATION attribute 137 | Note: This can be either true, false or null (to ignore this parameter) 138 | type: boolean 139 | type: object 140 | rolePrefix: 141 | description: User role prefix 142 | type: string 143 | userPasswordRotationDuration: 144 | description: User password rotation duration 145 | type: string 146 | workGeneratedSecretName: 147 | description: Simple user password tuple generated secret name 148 | type: string 149 | required: 150 | - privileges 151 | type: object 152 | status: 153 | description: PostgresqlUserRoleStatus defines the observed state of PostgresqlUserRole. 154 | properties: 155 | lastPasswordChangedTime: 156 | description: Last password changed time 157 | type: string 158 | message: 159 | description: Human-readable message indicating details about current 160 | operator phase or error. 161 | type: string 162 | oldPostgresRoles: 163 | description: Postgres old roles to cleanup 164 | items: 165 | type: string 166 | type: array 167 | phase: 168 | description: Current phase of the operator 169 | type: string 170 | postgresRole: 171 | description: Postgres role for user 172 | type: string 173 | ready: 174 | description: True if all resources are in a ready state and all work 175 | is done. 176 | type: boolean 177 | roleName: 178 | description: User role 179 | type: string 180 | required: 181 | - phase 182 | type: object 183 | type: object 184 | served: true 185 | storage: true 186 | subresources: 187 | status: {} 188 | --------------------------------------------------------------------------------