Merge branch 'master' into feat/configurable-pooler-securitycontext

This commit is contained in:
Mitch Murphy 2026-06-13 12:42:43 -04:00 committed by GitHub
commit 34bb815e6c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
46 changed files with 890 additions and 2181 deletions

View File

@ -101,3 +101,15 @@ jobs:
build-args: BASE_IMAGE=ubuntu:22.04
tags: "${{ steps.image_lb.outputs.BACKUP_IMAGE }}"
platforms: linux/amd64,linux/arm64
- name: Build and push postgres-operator chart to ghcr
run: |
helm package charts/postgres-operator
helm push postgres-operator-*.tgz oci://${{ env.REGISTRY }}/zalando/charts
rm -rf postgres-operator-*.tgz
- name: Build and push postgres-operator-ui chart to ghcr
run: |
helm package charts/postgres-operator-ui
helm push postgres-operator-ui-*.tgz oci://${{ env.REGISTRY }}/zalando/charts
rm -rf postgres-operator-ui-*.tgz

1
.gitignore vendored
View File

@ -26,7 +26,6 @@ _testmain.go
*.test
*.prof
/vendor/
/kubectl-pg/vendor/
/build/
/docker/build/
/github.com/

View File

@ -610,6 +610,18 @@ spec:
enable_replica_pooler_load_balancer:
type: boolean
default: false
enable_master_node_port:
type: boolean
default: false
enable_master_pooler_node_port:
type: boolean
default: false
enable_replica_node_port:
type: boolean
default: false
enable_replica_pooler_node_port:
type: boolean
default: false
external_traffic_policy:
type: string
enum:

View File

@ -107,7 +107,7 @@ spec:
description: load balancers' source ranges are the same for master
and replica services
items:
pattern: ^(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])$
pattern: ^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])|(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9]))$
type: string
nullable: true
type: array
@ -273,14 +273,26 @@ spec:
vars that enable load balancers are pointers because it is important to know if any of them is omitted from the Postgres manifest
in that case the var evaluates to nil and the value is taken from the operator config
type: boolean
enableMasterNodePort:
description: |-
vars to enable and configure nodeport services
set ports to 0 or nil to let kubernetes decide which port to use
overrides loadbalancer configuration
type: boolean
enableMasterPoolerLoadBalancer:
type: boolean
enableMasterPoolerNodePort:
type: boolean
enableReplicaConnectionPooler:
type: boolean
enableReplicaLoadBalancer:
type: boolean
enableReplicaNodePort:
type: boolean
enableReplicaPoolerLoadBalancer:
type: boolean
enableReplicaPoolerNodePort:
type: boolean
enableShmVolume:
type: boolean
env:
@ -3408,6 +3420,12 @@ spec:
pattern: '^\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))-((2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\ *$'
type: string
type: array
masterNodePort:
format: int32
type: integer
masterPoolerNodePort:
format: int32
type: integer
masterServiceAnnotations:
additionalProperties:
type: string
@ -3712,6 +3730,12 @@ spec:
replicaLoadBalancer:
description: deprecated
type: boolean
replicaNodePort:
format: int32
type: integer
replicaPoolerNodePort:
format: int32
type: integer
replicaServiceAnnotations:
additionalProperties:
type: string

View File

@ -928,6 +928,41 @@ For the `external-dns.alpha.kubernetes.io/hostname` annotation the `-pooler`
suffix will be appended to the cluster name used in the template which is
defined in `master|replica_dns_name_format`.
## Node Ports
Alternatively to Load Balancers Node Ports can be used. Kubernetes services with type
`NodePort` redirect traffic from a specified port on your kubernetes nodes to your service.
To expose your services to an external network with NodePorts you can set `enableMasterNodePort` and/or `enableReplicaNodePort` to `true`
in your cluster manifest. In the case any of these variables are omitted from the manifest, the operator configuration settings `enable_master_node_port` and `enable_replica_node_port` apply.
Note that the operator settings affect all Postgresql services running in all namespaces watched
by the operator.
**Enabling a NodePort configuration will override the corresponding LoadBalancer configuration.**
There are multiple options to specify service annotations that will be merged
with each other and override in the following order (where latter take
precedence):
1. Globally configured `custom_service_annotations`
2. `serviceAnnotations` specified in the cluster manifest
3. `masterServiceAnnotations` and `replicaServiceAnnotations` specified in the cluster manifest
Load-Balancer specific annotations are not applied.
Node port services can also be configured for the [connection pooler](user.md#connection-pooler) pods
with the manifest flags `enableMasterPoolerNodePort` and/or `enableReplicaPoolerNodePort` or in the operator configuration with `enable_master_pooler_node_port`
and/or `enable_replica_pooler_node_port`.
To configure which ports Kubernetes should use for your NodePort service you can configure ports in your cluster manifest
for each type:
- masterNodePort
- masterPoolerNodePort
- replicaNodePort
- replicaPoolerNodePort
When not defined or set to 0 kubernetes will choose a port for you from [your kubernetes cluster's configured range](https://kubernetes.io/docs/concepts/services-networking/service/#type-nodeport).
## Running periodic 'autorepair' scans of K8s objects
The Postgres Operator periodically scans all K8s objects belonging to each

View File

@ -113,10 +113,53 @@ These parameters are grouped directly under the `spec` key in the manifest.
* **allowedSourceRanges**
when one or more load balancers are enabled for the cluster, this parameter
defines the comma-separated range of IP networks (in CIDR-notation). The
corresponding load balancer is accessible only to the networks defined by
this parameter. Optional, when empty the load balancer service becomes
inaccessible from outside of the Kubernetes cluster.
defines the comma-separated range of IP networks (in CIDR-notation). Both
IPv4 (e.g. `192.168.1.0/24`) and IPv6 (e.g. `fd01::/48`) CIDR ranges are
supported. The corresponding load balancer is accessible only to the networks
defined by this parameter. Optional, when empty the load balancer service
becomes inaccessible from outside of the Kubernetes cluster.
* **enableMasterNodePort**
boolean flag to override the operator defaults (set by the
`enable_master_node_port` parameter) to define whether to enable the node
port pointing to the Postgres primary. Optional. Overrides `enableMasterLoadBalancer`.
* **enableMasterPoolerNodePort**
boolean flag to override the operator defaults (set by the
`enable_master_pooler_node_port` parameter) to define whether to enable
the node port for master pooler pods pointing to the Postgres primary.
Optional. Overrides `enableMasterPoolerLoadBalancer`.
* **enableReplicaNodePort**
boolean flag to override the operator defaults (set by the
`enable_replica_node_port` parameter) to define whether to enable the node
port pointing to the Postgres standby instances. Optional. Overrides `enableReplicaLoadBalancer`.
* **enableReplicaPoolerNodePort**
boolean flag to override the operator defaults (set by the
`enable_replica_pooler_node_port` parameter) to define whether to enable
the node port for replica pooler pods pointing to the Postgres standby
instances. Optional. Overrides `enableReplicaPoolerLoadBalancer`.
* **masterNodePort**
integer flag to specify a port number for the node port to the Postgres primary.
Only used when `enableMasterNodePort` or `enable_master_node_port` are enabled.
Optional. Kubernetes will provide a port number for you if not specified.
* **masterPoolerNodePort**
integer flag to specify a port number for the node port for the master pooler pods pointing to the Postgres primary.
Only used when `enableMasterPoolerNodePort` or `enable_master_pooler_node_port` are enabled.
Optional. Kubernetes will provide a port number for you if not specified.
* **replicaNodePort**
integer flag to specify a port number for the node port pointing to the Postgres standby instances.
Only used when `enableReplicaNodePort` or `enable_replica_node_port` are enabled.
Optional. Kubernetes will provide a port number for you if not specified.
* **replicaPoolerNodePort**
integer flag to specify a port number for the node port for the replica pooler pods pointing to the Postgres standby instances
Only used when `enableReplicaPoolerNodePort` or `enable_replica_pooler_node_port` are enabled.
Optional. Kubernetes will provide a port number for you if not specified.
* **maintenanceWindows**
a list which defines specific time frames when certain maintenance operations

View File

@ -240,7 +240,7 @@ class K8s:
time.sleep(self.RETRY_TIMEOUT_SEC)
def get_logical_backup_job(self, namespace='default'):
return self.api.batch_v1.list_namespaced_cron_job(namespace, label_selector="application=spilo")
return self.api.batch_v1.list_namespaced_cron_job(namespace, label_selector="application=spilo-logical-backup")
def wait_for_logical_backup_job(self, expected_num_of_jobs):
while (len(self.get_logical_backup_job().items) != expected_num_of_jobs):

2
go.mod
View File

@ -6,7 +6,7 @@ require (
github.com/Masterminds/semver v1.5.0
github.com/aws/aws-sdk-go v1.55.8
github.com/golang/mock v1.6.0
github.com/lib/pq v1.11.2
github.com/lib/pq v1.12.3
github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d
github.com/pkg/errors v0.9.1
github.com/r3labs/diff v1.1.0

4
go.sum
View File

@ -71,8 +71,8 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=

View File

@ -1,137 +0,0 @@
# Kubectl Plugin for Zalando's Postgres Operator
This plugin is a prototype developed as a part of **Google Summer of Code 2019** under the [Postgres Operator](https://summerofcode.withgoogle.com/archive/2019/organizations/6187982082539520/) organization.
## Installation of kubectl pg plugin
This project uses Go Modules for dependency management to build locally.
Install go and enable go modules with ```export GO111MODULE=on```.
From Go >=1.13 Go modules will be enabled by default.
```bash
# Assumes you have a working KUBECONFIG
$ GO111MODULE="on"
$ GOPATH/src/github.com/zalando/postgres-operator/kubectl-pg go mod vendor
# This generate a vendor directory with all dependencies needed by the plugin.
$ $GOPATH/src/github.com/zalando/postgres-operator/kubectl-pg go install
# This will place the kubectl-pg binary in your $GOPATH/bin
```
## Before using the kubectl pg plugin make sure to set KUBECONFIG env variable
Ideally KUBECONFIG is found in $HOME/.kube/config else specify the KUBECONFIG path here.
```export KUBECONFIG=$HOME/.kube/config```
## List all commands available in kubectl pg
```kubectl pg --help``` (or) ```kubectl pg```
## Check if Postgres Operator is installed and running
```kubectl pg check```
## Create a new cluster using manifest file
```kubectl pg create -f acid-minimal-cluster.yaml```
## List postgres resources
```kubectl pg list```
List clusters across namespaces
```kubectl pg list all```
## Update existing cluster using manifest file
```kubectl pg update -f acid-minimal-cluster.yaml```
## Delete existing cluster
Using the manifest file:
```kubectl pg delete -f acid-minimal-cluster.yaml```
Or by specifying the cluster name:
```kubectl pg delete acid-minimal-cluster```
Use `--namespace` or `-n` flag if your cluster is in a different namespace to where your current context is pointing to:
```kubectl pg delete acid-minimal-cluster -n namespace01```
## Adding manifest roles to an existing cluster
```kubectl pg add-user USER01 -p CREATEDB,LOGIN -c acid-minimal-cluster```
Privileges can only be [SUPERUSER, REPLICATION, INHERIT, LOGIN, NOLOGIN, CREATEROLE, CREATEDB, BYPASSRLS]
Note: By default, a LOGIN user is created (unless NOLOGIN is specified).
## Adding databases to an existing cluster
You have to specify an owner of the new database and this role must already exist in the cluster:
```kubectl pg add-db DB01 -o OWNER01 -c acid-minimal-cluster```
## Extend the volume of an existing pg cluster
```kubectl pg ext-volume 2Gi -c acid-minimal-cluster```
## Print the version of Postgres Operator and kubectl pg plugin
```kubectl pg version```
## Connect to the shell of a postgres pod
Connect to the master pod:
```kubectl pg connect -c CLUSTER -m```
Connect to a random replica pod:
```kubectl pg connect -c CLUSTER```
Connect to a certain replica pod:
```kubectl pg connect -c CLUSTER -r 0```
## Connect to a database via psql
Adding the `-p` flag allows you to directly connect to a given database with the psql client.
With `-u` you specify the user. If left out the name of the current OS user is taken.
`-d` lets you specify the database. If no database is specified, it will be the same as the user name.
Connect to `app_db` database on the master with role `app_user`:
```kubectl pg connect -c CLUSTER -m -p -u app_user -d app_db```
Connect to the `postgres` database on a random replica with role `postgres`:
```kubectl pg connect -c CLUSTER -p -u postgres```
Connect to a certain replica assuming name of OS user, database role and name are all the same:
```kubectl pg connect -c CLUSTER -r 0 -p```
## Access Postgres Operator logs
```kubectl pg logs -o```
## Access Patroni logs of different database pods
Fetch logs of master:
```kubectl pg logs -c CLUSTER -m```
Fetch logs of a random replica pod:
```kubectl pg logs -c CLUSTER```
Fetch logs of specified replica
```kubectl pg logs -c CLUSTER -r 2```
## Development
When making changes to the plugin make sure to change the (major/patch) version of plugin in `build.sh` script and run `./build.sh`.
## Google Summer of Code 2019
### GSoC Proposal
[kubectl pg proposal](https://docs.google.com/document/d/1-WMy9HkfZ1XnnMbzplMe9rCzKrRMGaMz4owLVXXPb7w/edit)
### Weekly Reports
https://github.com/VineethReddy02/GSoC-Kubectl-Plugin-for-Postgres-Operator-tracker
### Final Project Report
https://gist.github.com/VineethReddy02/159283bd368a710379eaf0f6bd60a40a

View File

@ -1,4 +0,0 @@
VERSION=1.0
sed -i "s/KubectlPgVersion string = \"[^\"]*\"/KubectlPgVersion string = \"${VERSION}\"/" cmd/version.go
go install

View File

@ -1,114 +0,0 @@
/*
Copyright © 2019 Vineeth Pothulapati <vineethpothulapati@outlook.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"context"
"encoding/json"
"fmt"
"log"
"github.com/spf13/cobra"
PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
// addDbCmd represents the addDb command
var addDbCmd = &cobra.Command{
Use: "add-db",
Short: "Adds a DB and its owner to a Postgres cluster. The owner role is created if it does not exist",
Long: `Adds a new DB to the Postgres cluster. Owner needs to be specified by the -o flag, cluster with -c flag.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) > 0 {
dbName := args[0]
dbOwner, _ := cmd.Flags().GetString("owner")
clusterName, _ := cmd.Flags().GetString("cluster")
addDb(dbName, dbOwner, clusterName)
} else {
fmt.Println("database name can't be empty. Use kubectl pg add-db [-h | --help] for more info")
}
},
Example: `
kubectl pg add-db db01 -o owner01 -c cluster01
`,
}
// add db and it's owner to the cluster
func addDb(dbName string, dbOwner string, clusterName string) {
config := getConfig()
postgresConfig, err := PostgresqlLister.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
namespace := getCurrentNamespace()
postgresql, err := postgresConfig.Postgresqls(namespace).Get(context.TODO(), clusterName, metav1.GetOptions{})
if err != nil {
log.Fatal(err)
}
var dbOwnerExists bool
dbUsers := postgresql.Spec.Users
for key := range dbUsers {
if key == dbOwner {
dbOwnerExists = true
}
}
var patch []byte
// validating reserved DB names
if dbOwnerExists && dbName != "postgres" && dbName != "template0" && dbName != "template1" {
patch = dbPatch(dbName, dbOwner)
} else if !dbOwnerExists {
log.Fatal("The provided db-owner doesn't exist")
} else {
log.Fatal("The provided db-name is reserved by postgres")
}
updatedPostgres, err := postgresConfig.Postgresqls(namespace).Patch(context.TODO(), postgresql.Name, types.MergePatchType, patch, metav1.PatchOptions{})
if err != nil {
log.Fatal(err)
}
if updatedPostgres.ResourceVersion != postgresql.ResourceVersion {
fmt.Printf("Created new database %s with owner %s in PostgreSQL cluster %s.\n", dbName, dbOwner, updatedPostgres.Name)
} else {
fmt.Printf("postgresql %s is unchanged.\n", updatedPostgres.Name)
}
}
func dbPatch(dbname string, owner string) []byte {
ins := map[string]map[string]map[string]string{"spec": {"databases": {dbname: owner}}}
patchInstances, err := json.Marshal(ins)
if err != nil {
log.Fatal(err, "unable to parse patch for add-db")
}
return patchInstances
}
func init() {
addDbCmd.Flags().StringP("owner", "o", "", "provide owner of the database.")
addDbCmd.Flags().StringP("cluster", "c", "", "provide a postgres cluster name.")
rootCmd.AddCommand(addDbCmd)
}

View File

@ -1,144 +0,0 @@
/*
Copyright © 2019 Vineeth Pothulapati <vineethpothulapati@outlook.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"context"
"encoding/json"
"fmt"
"log"
"strings"
"github.com/spf13/cobra"
PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
var allowedPrivileges = []string{"SUPERUSER", "REPLICATION", "INHERIT", "LOGIN", "NOLOGIN", "CREATEROLE", "CREATEDB", "BYPASSRLS"}
// addUserCmd represents the addUser command
var addUserCmd = &cobra.Command{
Use: "add-user",
Short: "Adds a user to the postgres cluster with given privileges",
Long: `Adds a user to the postgres cluster. You can add privileges as well with -p flag.`,
Run: func(cmd *cobra.Command, args []string) {
clusterName, _ := cmd.Flags().GetString("cluster")
privileges, _ := cmd.Flags().GetString("privileges")
if len(args) > 0 {
user := args[0]
var permissions []string
var perms []string
if privileges != "" {
parsedRoles := strings.Replace(privileges, ",", " ", -1)
parsedRoles = strings.ToUpper(parsedRoles)
permissions = strings.Fields(parsedRoles)
var invalidPerms []string
for _, userPrivilege := range permissions {
validPerm := false
for _, privilege := range allowedPrivileges {
if privilege == userPrivilege {
perms = append(perms, userPrivilege)
validPerm = true
}
}
if !validPerm {
invalidPerms = append(invalidPerms, userPrivilege)
}
}
if len(invalidPerms) > 0 {
fmt.Printf("Invalid privileges %s\n", invalidPerms)
return
}
}
addUser(user, clusterName, perms)
}
},
Example: `
kubectl pg add-user user01 -p login,createdb -c cluster01
`,
}
// add user to the cluster with provided permissions
func addUser(user string, clusterName string, permissions []string) {
config := getConfig()
postgresConfig, err := PostgresqlLister.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
namespace := getCurrentNamespace()
postgresql, err := postgresConfig.Postgresqls(namespace).Get(context.TODO(), clusterName, metav1.GetOptions{})
if err != nil {
log.Fatal(err)
}
setUsers := make(map[string]bool)
for _, k := range permissions {
setUsers[k] = true
}
if existingRoles, key := postgresql.Spec.Users[user]; key {
for _, k := range existingRoles {
setUsers[k] = true
}
}
Privileges := []string{}
for keys, values := range setUsers {
if values {
Privileges = append(Privileges, keys)
}
}
patch := applyUserPatch(user, Privileges)
updatedPostgresql, err := postgresConfig.Postgresqls(namespace).Patch(context.TODO(), postgresql.Name, types.MergePatchType, patch, metav1.PatchOptions{})
if err != nil {
log.Fatal(err)
}
if updatedPostgresql.ResourceVersion != postgresql.ResourceVersion {
fmt.Printf("postgresql %s is updated with new user %s and with privileges %s.\n", updatedPostgresql.Name, user, permissions)
} else {
fmt.Printf("postgresql %s is unchanged.\n", updatedPostgresql.Name)
}
}
func applyUserPatch(user string, value []string) []byte {
ins := map[string]map[string]map[string][]string{"spec": {"users": {user: value}}}
patchInstances, err := json.Marshal(ins)
if err != nil {
log.Fatal(err, "unable to parse number of instances json")
}
return patchInstances
}
func init() {
addUserCmd.Flags().StringP("cluster", "c", "", "add user to the provided cluster.")
addUserCmd.Flags().StringP("privileges", "p", "", "add privileges separated by commas without spaces")
rootCmd.AddCommand(addUserCmd)
}

View File

@ -1,74 +0,0 @@
/*
Copyright © 2019 Vineeth Pothulapati <vineethpothulapati@outlook.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"context"
"fmt"
"log"
"github.com/spf13/cobra"
postgresConstants "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
v1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
apiextv1 "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/typed/apiextensions/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// checkCmd represent kubectl pg check.
var checkCmd = &cobra.Command{
Use: "check",
Short: "Checks the Postgres operator is installed in the k8s cluster",
Long: `Checks that the Postgres CRD is registered in a k8s cluster.
This means that the operator pod was able to start normally.`,
Run: func(cmd *cobra.Command, args []string) {
check()
},
Example: `
kubectl pg check
`,
}
// check validates postgresql CRD registered or not.
func check() *v1.CustomResourceDefinition {
config := getConfig()
apiExtClient, err := apiextv1.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
crdInfo, err := apiExtClient.CustomResourceDefinitions().Get(context.TODO(), postgresConstants.PostgresCRDResouceName, metav1.GetOptions{})
if err != nil {
log.Fatal(err)
}
if crdInfo.Name == postgresConstants.PostgresCRDResouceName {
fmt.Printf("Postgres Operator is installed in the k8s cluster.\n")
} else {
fmt.Printf("Postgres Operator is not installed in the k8s cluster.\n")
}
return crdInfo
}
func init() {
rootCmd.AddCommand(checkCmd)
}

View File

@ -1,144 +0,0 @@
/*
Copyright © 2019 Vineeth Pothulapati <vineethpothulapati@outlook.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"context"
"log"
"os"
user "os/user"
"github.com/spf13/cobra"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"k8s.io/client-go/tools/remotecommand"
)
// connectCmd represents the kubectl pg connect command
var connectCmd = &cobra.Command{
Use: "connect",
Short: "Connects to the shell prompt, psql prompt of postgres cluster",
Long: `Connects to the shell prompt, psql prompt of postgres cluster and also to specified replica or master.`,
Run: func(cmd *cobra.Command, args []string) {
clusterName, _ := cmd.Flags().GetString("cluster")
master, _ := cmd.Flags().GetBool("master")
replica, _ := cmd.Flags().GetString("replica")
psql, _ := cmd.Flags().GetBool("psql")
userName, _ := cmd.Flags().GetString("user")
dbName, _ := cmd.Flags().GetString("database")
if psql {
if userName == "" {
userInfo, err := user.Current()
if err != nil {
log.Fatal(err)
}
userName = userInfo.Username
}
}
if dbName == "" {
dbName = userName
}
connect(clusterName, master, replica, psql, userName, dbName)
},
Example: `
#connects to the master of postgres cluster
kubectl pg connect -c cluster -m
#connects to the random replica of postgres cluster
kubectl pg connect -c cluster
#connects to the provided replica number of postgres cluster
kubectl pg connect -c cluster -r 2
#connects to psql prompt of master for provided postgres cluster with current shell user
kubectl pg connect -c cluster -p -m
#connects to psql prompt of random replica for provided postgres cluster with provided user and db
kubectl pg connect -c cluster -p -u user01 -d db01
`,
}
func connect(clusterName string, master bool, replica string, psql bool, user string, dbName string) {
config := getConfig()
client, err := kubernetes.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
podName := getPodName(clusterName, master, replica)
var execRequest *rest.Request
if psql {
execRequest = client.CoreV1().RESTClient().Post().Resource("pods").
Name(podName).
Namespace(getCurrentNamespace()).
SubResource("exec").
Param("container", "postgres").
Param("command", "psql").
Param("command", dbName).
Param("command", user).
Param("stdin", "true").
Param("stdout", "true").
Param("stderr", "true").
Param("tty", "true")
} else {
execRequest = client.CoreV1().RESTClient().Post().Resource("pods").
Name(podName).
Namespace(getCurrentNamespace()).
SubResource("exec").
Param("container", "postgres").
Param("command", "su").
Param("command", "postgres").
Param("stdin", "true").
Param("stdout", "true").
Param("stderr", "true").
Param("tty", "true")
}
exec, err := remotecommand.NewSPDYExecutor(config, "POST", execRequest.URL())
if err != nil {
log.Fatal(err)
}
err = exec.StreamWithContext(context.TODO(), remotecommand.StreamOptions{
Stdin: os.Stdin,
Stdout: os.Stdout,
Stderr: os.Stderr,
Tty: true,
})
if err != nil {
log.Fatal(err)
}
}
func init() {
connectCmd.Flags().StringP("cluster", "c", "", "provide the cluster name.")
connectCmd.Flags().BoolP("master", "m", false, "connect to master.")
connectCmd.Flags().StringP("replica", "r", "", "connect to replica. Specify replica number.")
connectCmd.Flags().BoolP("psql", "p", false, "connect to psql prompt.")
connectCmd.Flags().StringP("user", "u", "", "provide user.")
connectCmd.Flags().StringP("database", "d", "", "provide database name.")
rootCmd.AddCommand(connectCmd)
}

View File

@ -1,82 +0,0 @@
/*
Copyright © 2019 Vineeth Pothulapati <vineethpothulapati@outlook.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"context"
"fmt"
"log"
"os"
"github.com/spf13/cobra"
v1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// createCmd kubectl pg create.
var createCmd = &cobra.Command{
Use: "create",
Short: "Creates postgres object using manifest file",
Long: `Creates postgres custom resource objects from a manifest file.`,
Run: func(cmd *cobra.Command, args []string) {
fileName, _ := cmd.Flags().GetString("file")
create(fileName)
},
Example: `
kubectl pg create -f cluster-manifest.yaml
`,
}
// Create postgresql resources.
func create(fileName string) {
config := getConfig()
postgresConfig, err := PostgresqlLister.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
ymlFile, err := os.ReadFile(fileName)
if err != nil {
log.Fatal(err)
}
decode := scheme.Codecs.UniversalDeserializer().Decode
obj, _, err := decode([]byte(ymlFile), nil, &v1.Postgresql{})
if err != nil {
log.Fatal(err)
}
postgresSql := obj.(*v1.Postgresql)
_, err = postgresConfig.Postgresqls(postgresSql.Namespace).Create(context.TODO(), postgresSql, metav1.CreateOptions{})
if err != nil {
log.Fatal(err)
}
fmt.Printf("postgresql %s created.\n", postgresSql.Name)
}
func init() {
createCmd.Flags().StringP("file", "f", "", "manifest file with the cluster definition.")
rootCmd.AddCommand(createCmd)
}

View File

@ -1,134 +0,0 @@
/*
Copyright © 2019 Vineeth Pothulapati <vineethpothulapati@outlook.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"context"
"fmt"
"log"
"os"
"github.com/spf13/cobra"
v1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// deleteCmd represents kubectl pg delete.
var deleteCmd = &cobra.Command{
Use: "delete",
Short: "Deletes postgresql object by cluster-name/manifest file",
Long: `Deletes the postgres objects identified by a manifest file or cluster-name.
Deleting the manifest is sufficient to delete the cluster.`,
Run: func(cmd *cobra.Command, args []string) {
namespace, _ := cmd.Flags().GetString("namespace")
file, _ := cmd.Flags().GetString("file")
if file != "" {
deleteByFile(file)
} else if namespace != "" {
if len(args) != 0 {
clusterName := args[0]
deleteByName(clusterName, namespace)
} else {
fmt.Println("cluster name can't be empty")
}
} else {
fmt.Println("use the flag either -n or -f to delete a resource.")
}
},
Example: `
#Deleting the postgres cluster using manifest file
kubectl pg delete -f cluster-manifest.yaml
#Deleting the postgres cluster using cluster name in current namespace.
kubectl pg delete cluster01
#Deleting the postgres cluster using cluster name in provided namespace
kubectl pg delete cluster01 -n namespace01
`,
}
func deleteByFile(file string) {
config := getConfig()
postgresConfig, err := PostgresqlLister.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
ymlFile, err := os.ReadFile(file)
if err != nil {
log.Fatal(err)
}
decode := scheme.Codecs.UniversalDeserializer().Decode
obj, _, err := decode([]byte(ymlFile), nil, &v1.Postgresql{})
if err != nil {
log.Fatal(err)
}
postgresSql := obj.(*v1.Postgresql)
_, err = postgresConfig.Postgresqls(postgresSql.Namespace).Get(context.TODO(), postgresSql.Name, metav1.GetOptions{})
if err != nil {
fmt.Printf("Postgresql %s not found with the provided namespace %s : %s \n", postgresSql.Name, postgresSql.Namespace, err)
return
}
fmt.Printf("Are you sure you want to remove this PostgreSQL cluster? If so, please type (%s/%s) and hit Enter\n", postgresSql.Namespace, postgresSql.Name)
confirmAction(postgresSql.Name, postgresSql.Namespace)
err = postgresConfig.Postgresqls(postgresSql.Namespace).Delete(context.TODO(), postgresSql.Name, metav1.DeleteOptions{})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Postgresql %s deleted from %s.\n", postgresSql.Name, postgresSql.Namespace)
}
func deleteByName(clusterName string, namespace string) {
config := getConfig()
postgresConfig, err := PostgresqlLister.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
_, err = postgresConfig.Postgresqls(namespace).Get(context.TODO(), clusterName, metav1.GetOptions{})
if err != nil {
fmt.Printf("Postgresql %s not found with the provided namespace %s : %s \n", clusterName, namespace, err)
return
}
fmt.Printf("Are you sure you want to remove this PostgreSQL cluster? If so, please type (%s/%s) and hit Enter\n", namespace, clusterName)
confirmAction(clusterName, namespace)
err = postgresConfig.Postgresqls(namespace).Delete(context.TODO(), clusterName, metav1.DeleteOptions{})
if err != nil {
log.Fatal(err)
}
fmt.Printf("Postgresql %s deleted from %s.\n", clusterName, namespace)
}
func init() {
namespace := getCurrentNamespace()
deleteCmd.Flags().StringP("namespace", "n", namespace, "namespace of the cluster to be deleted.")
deleteCmd.Flags().StringP("file", "f", "", "manifest file with the cluster definition.")
rootCmd.AddCommand(deleteCmd)
}

View File

@ -1,119 +0,0 @@
/*
Copyright © 2019 Vineeth Pothulapati <vineethpothulapati@outlook.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"context"
"encoding/json"
"fmt"
"log"
"strconv"
"github.com/spf13/cobra"
PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
)
// extVolumeCmd represents the extVolume command
var extVolumeCmd = &cobra.Command{
Use: "ext-volume",
Short: "Increases the volume size of a given Postgres cluster",
Long: `Extends the volume of the postgres cluster. But volume cannot be shrinked.`,
Run: func(cmd *cobra.Command, args []string) {
clusterName, _ := cmd.Flags().GetString("cluster")
if len(args) > 0 {
volume := args[0]
extVolume(volume, clusterName)
} else {
fmt.Println("please enter the cluster name with -c flag & volume in desired units")
}
},
Example: `
#Extending the volume size of provided cluster
kubectl pg ext-volume 2Gi -c cluster01
`,
}
// extend volume with provided size & cluster name
func extVolume(increasedVolumeSize string, clusterName string) {
config := getConfig()
postgresConfig, err := PostgresqlLister.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
namespace := getCurrentNamespace()
postgresql, err := postgresConfig.Postgresqls(namespace).Get(context.TODO(), clusterName, metav1.GetOptions{})
if err != nil {
log.Fatal(err)
}
oldSize, err := resource.ParseQuantity(postgresql.Spec.Volume.Size)
if err != nil {
log.Fatal(err)
}
newSize, err := resource.ParseQuantity(increasedVolumeSize)
if err != nil {
log.Fatal(err)
}
_, err = strconv.Atoi(newSize.String())
if err == nil {
fmt.Println("provide the valid volume size with respective units i.e Ki, Mi, Gi")
return
}
if newSize.Value() > oldSize.Value() {
patchInstances := volumePatch(newSize)
response, err := postgresConfig.Postgresqls(namespace).Patch(context.TODO(), postgresql.Name, types.MergePatchType, patchInstances, metav1.PatchOptions{})
if err != nil {
log.Fatal(err)
}
if postgresql.ResourceVersion != response.ResourceVersion {
fmt.Printf("%s volume is extended to %s.\n", response.Name, increasedVolumeSize)
} else {
fmt.Printf("%s volume %s is unchanged.\n", response.Name, postgresql.Spec.Volume.Size)
}
} else if newSize.Value() == oldSize.Value() {
fmt.Println("volume already has the desired size.")
} else {
fmt.Printf("volume %s size cannot be shrinked.\n", postgresql.Spec.Volume.Size)
}
}
func volumePatch(volume resource.Quantity) []byte {
patchData := map[string]map[string]map[string]resource.Quantity{"spec": {"volume": {"size": volume}}}
patch, err := json.Marshal(patchData)
if err != nil {
log.Fatal(err, "unable to parse patch to extend volume")
}
return patch
}
func init() {
extVolumeCmd.Flags().StringP("cluster", "c", "", "provide cluster name.")
rootCmd.AddCommand(extVolumeCmd)
}

View File

@ -1,125 +0,0 @@
/*
Copyright © 2019 Vineeth Pothulapati <vineethpothulapati@outlook.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"context"
"fmt"
"log"
"strconv"
"time"
"github.com/spf13/cobra"
v1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
const (
TrimCreateTimestamp = 6000000000
)
// listCmd represents kubectl pg list.
var listCmd = &cobra.Command{
Use: "list",
Short: "Lists all the resources of kind postgresql",
Long: `Lists all the info specific to postgresql objects.`,
Run: func(cmd *cobra.Command, args []string) {
allNamespaces, _ := cmd.Flags().GetBool("all-namespaces")
namespace, _ := cmd.Flags().GetString("namespace")
if allNamespaces {
list(allNamespaces, "")
} else {
list(allNamespaces, namespace)
}
},
Example: `
#Lists postgres cluster in current namespace
kubectl pg list
#Lists postgres clusters in all namespaces
kubectl pg list -A
`,
}
// list command to list postgres.
func list(allNamespaces bool, namespace string) {
config := getConfig()
postgresConfig, err := PostgresqlLister.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
listPostgres, err := postgresConfig.Postgresqls(namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
log.Fatal(err)
}
if len(listPostgres.Items) == 0 {
if namespace != "" {
fmt.Printf("No Postgresql clusters found in namespace: %v\n", namespace)
} else {
fmt.Println("No Postgresql clusters found in all namespaces")
}
return
}
if allNamespaces {
listAll(listPostgres)
} else {
listWithNamespace(listPostgres)
}
}
func listAll(listPostgres *v1.PostgresqlList) {
template := "%-32s%-16s%-12s%-12s%-12s%-12s%-12s\n"
fmt.Printf(template, "NAME", "STATUS", "INSTANCES", "VERSION", "AGE", "VOLUME", "NAMESPACE")
for _, pgObjs := range listPostgres.Items {
fmt.Printf(template, pgObjs.Name,
pgObjs.Status.PostgresClusterStatus,
strconv.Itoa(int(pgObjs.Spec.NumberOfInstances)),
pgObjs.Spec.PostgresqlParam.PgVersion,
time.Since(pgObjs.CreationTimestamp.Time).Truncate(TrimCreateTimestamp),
pgObjs.Spec.Size, pgObjs.Namespace)
}
}
func listWithNamespace(listPostgres *v1.PostgresqlList) {
template := "%-32s%-16s%-12s%-12s%-12s%-12s\n"
fmt.Printf(template, "NAME", "STATUS", "INSTANCES", "VERSION", "AGE", "VOLUME")
for _, pgObjs := range listPostgres.Items {
fmt.Printf(template, pgObjs.Name,
pgObjs.Status.PostgresClusterStatus,
strconv.Itoa(int(pgObjs.Spec.NumberOfInstances)),
pgObjs.Spec.PostgresqlParam.PgVersion,
time.Since(pgObjs.CreationTimestamp.Time).Truncate(TrimCreateTimestamp),
pgObjs.Spec.Size)
}
}
func init() {
listCmd.Flags().BoolP("all-namespaces", "A", false, "list pg resources across all namespaces.")
listCmd.Flags().StringP("namespace", "n", getCurrentNamespace(), "provide the namespace")
rootCmd.AddCommand(listCmd)
}

View File

@ -1,143 +0,0 @@
/*
Copyright © 2019 Vineeth Pothulapati <vineethpothulapati@outlook.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"context"
"io"
"log"
"os"
"github.com/spf13/cobra"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
)
// logsCmd represents the logs command
var logsCmd = &cobra.Command{
Use: "logs",
Short: "This will fetch the logs of the specified postgres cluster & postgres operator",
Long: `Fetches the logs of the postgres cluster (i.e master( with -m flag) & replica with (-r 1 pod number) and without -m or -r connects to random replica`,
Run: func(cmd *cobra.Command, args []string) {
opLogs, _ := cmd.Flags().GetBool("operator")
clusterName, _ := cmd.Flags().GetString("cluster")
master, _ := cmd.Flags().GetBool("master")
replica, _ := cmd.Flags().GetString("replica")
if opLogs {
operatorLogs()
} else {
clusterLogs(clusterName, master, replica)
}
},
Example: `
#Fetch the logs of the postgres operator
kubectl pg logs -o
#Fetch the logs of the master for provided cluster
kubectl pg logs -c cluster01 -m
#Fetch the logs of the random replica for provided cluster
kubectl pg logs -c cluster01
#Fetch the logs of the provided replica number of the cluster
kubectl pg logs -c cluster01 -r 3
`,
}
func operatorLogs() {
config := getConfig()
client, err := kubernetes.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
operator := getPostgresOperator(client)
allPods, err := client.CoreV1().Pods(operator.Namespace).List(context.TODO(), metav1.ListOptions{})
if err != nil {
log.Fatal(err)
}
var operatorPodName string
for _, pod := range allPods.Items {
for key, value := range pod.Labels {
if (key == "name" && value == OperatorName) || (key == "app.kubernetes.io/name" && value == OperatorName) {
operatorPodName = pod.Name
break
}
}
}
execRequest := client.CoreV1().RESTClient().Get().Namespace(operator.Namespace).
Name(operatorPodName).
Resource("pods").
SubResource("log").
Param("follow", "--follow").
Param("container", OperatorName)
readCloser, err := execRequest.Stream(context.TODO())
if err != nil {
log.Fatal(err)
}
defer readCloser.Close()
_, err = io.Copy(os.Stdout, readCloser)
if err != nil {
log.Fatal(err)
}
}
func clusterLogs(clusterName string, master bool, replica string) {
config := getConfig()
client, err := kubernetes.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
podName := getPodName(clusterName, master, replica)
execRequest := client.CoreV1().RESTClient().Get().Namespace(getCurrentNamespace()).
Name(podName).
Resource("pods").
SubResource("log").
Param("follow", "--follow").
Param("container", "postgres")
readCloser, err := execRequest.Stream(context.TODO())
if err != nil {
log.Fatal(err)
}
defer readCloser.Close()
_, err = io.Copy(os.Stdout, readCloser)
if err != nil {
log.Fatal(err)
}
}
func init() {
rootCmd.AddCommand(logsCmd)
logsCmd.Flags().BoolP("operator", "o", false, "logs of operator")
logsCmd.Flags().StringP("cluster", "c", "", "logs for the provided cluster")
logsCmd.Flags().BoolP("master", "m", false, "Patroni logs of master")
logsCmd.Flags().StringP("replica", "r", "", "Patroni logs of replica. Specify replica number.")
}

View File

@ -1,51 +0,0 @@
/*
Copyright © 2019 Vineeth Pothulapati <vineethpothulapati@outlook.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"fmt"
"os"
"github.com/spf13/cobra"
"github.com/spf13/viper"
)
var rootCmd = &cobra.Command{
Use: "kubectl-pg",
Short: "kubectl plugin for the Zalando Postgres operator.",
Long: `kubectl pg plugin for interaction with Zalando postgres operator.`,
}
// Execute adds all child commands to the root command and sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
func Execute() {
if err := rootCmd.Execute(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
func init() {
viper.SetDefault("author", "Vineeth Pothulapati <vineethpothulapati@outlook.com>")
viper.SetDefault("license", "mit")
}

View File

@ -1,194 +0,0 @@
/*
Copyright © 2019 Vineeth Pothulapati <vineethpothulapati@outlook.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"context"
"encoding/json"
"fmt"
"log"
"strconv"
"github.com/spf13/cobra"
PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
// scaleCmd represents the scale command
var scaleCmd = &cobra.Command{
Use: "scale",
Short: "Add/remove pods to a Postgres cluster",
Long: `Scales the postgres objects using cluster-name.
Scaling to 0 leads to down time.`,
Run: func(cmd *cobra.Command, args []string) {
clusterName, err := cmd.Flags().GetString("cluster")
if err != nil {
log.Fatal(err)
}
namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
log.Fatal(err)
}
if len(args) > 0 {
numberOfInstances, err := strconv.Atoi(args[0])
if err != nil {
log.Fatal(err)
}
scale(int32(numberOfInstances), clusterName, namespace)
} else {
fmt.Println("Please enter number of instances to scale.")
}
},
Example: `
#Usage
kubectl pg scale [NUMBER-OF-INSTANCES] -c [CLUSTER-NAME] -n [NAMESPACE]
#Scales the number of instances of the provided cluster
kubectl pg scale 5 -c cluster01
`,
}
func scale(numberOfInstances int32, clusterName string, namespace string) {
config := getConfig()
postgresConfig, err := PostgresqlLister.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
postgresql, err := postgresConfig.Postgresqls(namespace).Get(context.TODO(), clusterName, metav1.GetOptions{})
if err != nil {
log.Fatal(err)
}
minInstances, maxInstances := allowedMinMaxInstances(config)
if minInstances == -1 && maxInstances == -1 {
postgresql.Spec.NumberOfInstances = numberOfInstances
} else if numberOfInstances <= maxInstances && numberOfInstances >= minInstances {
postgresql.Spec.NumberOfInstances = numberOfInstances
} else if minInstances == -1 && numberOfInstances < postgresql.Spec.NumberOfInstances ||
maxInstances == -1 && numberOfInstances > postgresql.Spec.NumberOfInstances {
postgresql.Spec.NumberOfInstances = numberOfInstances
} else {
log.Fatalf("cannot scale to the provided instances as they don't adhere to MIN_INSTANCES: %v and MAX_INSTANCES: %v provided in configmap or operatorconfiguration", maxInstances, minInstances)
}
if numberOfInstances == 0 {
fmt.Printf("Scaling to zero leads to down time. please type %s/%s and hit Enter this serves to confirm the action\n", namespace, clusterName)
confirmAction(clusterName, namespace)
}
patchInstances := scalePatch(numberOfInstances)
UpdatedPostgres, err := postgresConfig.Postgresqls(namespace).Patch(context.TODO(), postgresql.Name, types.MergePatchType, patchInstances, metav1.PatchOptions{})
if err != nil {
log.Fatal(err)
}
if UpdatedPostgres.ResourceVersion != postgresql.ResourceVersion {
fmt.Printf("scaled postgresql %s/%s to %d instances\n", UpdatedPostgres.Namespace, UpdatedPostgres.Name, UpdatedPostgres.Spec.NumberOfInstances)
return
}
fmt.Printf("postgresql %s is unchanged.\n", postgresql.Name)
}
func scalePatch(value int32) []byte {
instances := map[string]map[string]int32{"spec": {"numberOfInstances": value}}
patchInstances, err := json.Marshal(instances)
if err != nil {
log.Fatal(err, "unable to parse patch for scale")
}
return patchInstances
}
func allowedMinMaxInstances(config *rest.Config) (int32, int32) {
k8sClient, err := kubernetes.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
operator := getPostgresOperator(k8sClient)
operatorContainer := operator.Spec.Template.Spec.Containers
var configMapName, operatorConfigName string
// -1 indicates no limitations for min/max instances
minInstances := -1
maxInstances := -1
for _, envData := range operatorContainer[0].Env {
if envData.Name == "CONFIG_MAP_NAME" {
configMapName = envData.Value
}
if envData.Name == "POSTGRES_OPERATOR_CONFIGURATION_OBJECT" {
operatorConfigName = envData.Value
}
}
if operatorConfigName == "" {
configMap, err := k8sClient.CoreV1().ConfigMaps(operator.Namespace).Get(context.TODO(), configMapName, metav1.GetOptions{})
if err != nil {
log.Fatal(err)
}
configMapData := configMap.Data
for key, value := range configMapData {
if key == "min_instances" {
minInstances, err = strconv.Atoi(value)
if err != nil {
log.Fatalf("invalid min instances in configmap %v", err)
}
}
if key == "max_instances" {
maxInstances, err = strconv.Atoi(value)
if err != nil {
log.Fatalf("invalid max instances in configmap %v", err)
}
}
}
} else if configMapName == "" {
pgClient, err := PostgresqlLister.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
operatorConfig, err := pgClient.OperatorConfigurations(operator.Namespace).Get(context.TODO(), operatorConfigName, metav1.GetOptions{})
if err != nil {
log.Fatalf("unable to read operator configuration %v", err)
}
minInstances = int(operatorConfig.Configuration.MinInstances)
maxInstances = int(operatorConfig.Configuration.MaxInstances)
}
return int32(minInstances), int32(maxInstances)
}
func init() {
namespace := getCurrentNamespace()
scaleCmd.Flags().StringP("namespace", "n", namespace, "namespace of the cluster to be scaled")
scaleCmd.Flags().StringP("cluster", "c", "", "provide the cluster name.")
rootCmd.AddCommand(scaleCmd)
}

View File

@ -1,96 +0,0 @@
/*
Copyright © 2019 Vineeth Pothulapati <vineethpothulapati@outlook.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"context"
"fmt"
"log"
"os"
"github.com/spf13/cobra"
v1 "github.com/zalando/postgres-operator/pkg/apis/acid.zalan.do/v1"
PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset/scheme"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
// updateCmd represents kubectl pg update
var updateCmd = &cobra.Command{
Use: "update",
Short: "Updates postgresql object using manifest file",
Long: `Updates the state of cluster using manifest file to reflect the changes on the cluster.`,
Run: func(cmd *cobra.Command, args []string) {
fileName, _ := cmd.Flags().GetString("file")
updatePgResources(fileName)
},
Example: `
#usage
kubectl pg update -f [File-NAME]
#update the postgres cluster with updated manifest file
kubectl pg update -f cluster-manifest.yaml
`,
}
// Update postgresql resources.
func updatePgResources(fileName string) {
config := getConfig()
postgresConfig, err := PostgresqlLister.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
ymlFile, err := os.ReadFile(fileName)
if err != nil {
log.Fatal(err)
}
decode := scheme.Codecs.UniversalDeserializer().Decode
obj, _, err := decode([]byte(ymlFile), nil, &v1.Postgresql{})
if err != nil {
log.Fatal(err)
}
newPostgresObj := obj.(*v1.Postgresql)
oldPostgresObj, err := postgresConfig.Postgresqls(newPostgresObj.Namespace).Get(context.TODO(), newPostgresObj.Name, metav1.GetOptions{})
if err != nil {
log.Fatal(err)
}
newPostgresObj.ResourceVersion = oldPostgresObj.ResourceVersion
response, err := postgresConfig.Postgresqls(newPostgresObj.Namespace).Update(context.TODO(), newPostgresObj, metav1.UpdateOptions{})
if err != nil {
log.Fatal(err)
}
if newPostgresObj.ResourceVersion != response.ResourceVersion {
fmt.Printf("postgresql %s updated.\n", response.Name)
} else {
fmt.Printf("postgresql %s is unchanged.\n", response.Name)
}
}
func init() {
updateCmd.Flags().StringP("file", "f", "", "manifest file with the cluster definition.")
rootCmd.AddCommand(updateCmd)
}

View File

@ -1,172 +0,0 @@
/*
Copyright © 2019 Vineeth Pothulapati <vineethpothulapati@outlook.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"context"
"flag"
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
PostgresqlLister "github.com/zalando/postgres-operator/pkg/generated/clientset/versioned/typed/acid.zalan.do/v1"
v1 "k8s.io/api/apps/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
restclient "k8s.io/client-go/rest"
"k8s.io/client-go/tools/clientcmd"
"k8s.io/client-go/util/homedir"
)
const (
OperatorName = "postgres-operator"
DefaultNamespace = "default"
)
func getConfig() *restclient.Config {
var kubeconfig *string
var config *restclient.Config
envKube := os.Getenv("KUBECONFIG")
if envKube != "" {
kubeconfig = &envKube
} else {
if home := homedir.HomeDir(); home != "" {
kubeconfig = flag.String("kubeconfig", filepath.Join(home, ".kube", "config"), "(optional) absolute path to the kubeconfig file")
} else {
kubeconfig = flag.String("kubeconfig", "", "absolute path to the kubeconfig file")
}
}
flag.Parse()
var err error
config, err = clientcmd.BuildConfigFromFlags("", *kubeconfig)
if err != nil {
log.Fatal(err)
}
return config
}
func getCurrentNamespace() string {
namespace, err := exec.Command("kubectl", "config", "view", "--minify", "--output", "jsonpath={..namespace}").CombinedOutput()
if err != nil {
log.Fatal(err)
}
currentNamespace := string(namespace)
if currentNamespace == "" {
currentNamespace = DefaultNamespace
}
return currentNamespace
}
func confirmAction(clusterName string, namespace string) {
for {
confirmClusterDetails := ""
_, err := fmt.Scan(&confirmClusterDetails)
if err != nil {
log.Fatalf("couldn't get confirmation from the user %v", err)
}
clusterDetails := strings.Split(confirmClusterDetails, "/")
if clusterDetails[0] != namespace || clusterDetails[1] != clusterName {
fmt.Printf("cluster name or namespace does not match. Please re-enter %s/%s\nHint: Press (ctrl+c) to exit\n", namespace, clusterName)
} else {
return
}
}
}
func getPodName(clusterName string, master bool, replicaNumber string) string {
config := getConfig()
client, err := kubernetes.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
postgresConfig, err := PostgresqlLister.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
postgresCluster, err := postgresConfig.Postgresqls(getCurrentNamespace()).Get(context.TODO(), clusterName, metav1.GetOptions{})
if err != nil {
log.Fatal(err)
}
numOfInstances := postgresCluster.Spec.NumberOfInstances
var podName string
var podRole string
replica := clusterName + "-" + replicaNumber
for ins := 0; ins < int(numOfInstances); ins++ {
pod, err := client.CoreV1().Pods(getCurrentNamespace()).Get(context.TODO(), clusterName+"-"+strconv.Itoa(ins), metav1.GetOptions{})
if err != nil {
log.Fatal(err)
}
podRole = pod.Labels["spilo-role"]
if podRole == "master" && master {
podName = pod.Name
fmt.Printf("connected to %s with pod name as %s\n", podRole, podName)
break
} else if podRole == "replica" && !master && (pod.Name == replica || replicaNumber == "") {
podName = pod.Name
fmt.Printf("connected to %s with pod name as %s\n", podRole, podName)
break
}
}
if podName == "" {
log.Fatal("Provided replica doesn't exist")
}
return podName
}
func getPostgresOperator(k8sClient *kubernetes.Clientset) *v1.Deployment {
var operator *v1.Deployment
operator, err := k8sClient.AppsV1().Deployments(getCurrentNamespace()).Get(context.TODO(), OperatorName, metav1.GetOptions{})
if err == nil {
return operator
}
allDeployments := k8sClient.AppsV1().Deployments("")
listDeployments, err := allDeployments.List(context.TODO(), metav1.ListOptions{})
if err != nil {
log.Fatal(err)
}
for _, deployment := range listDeployments.Items {
if deployment.Name == OperatorName {
operator = deployment.DeepCopy()
break
} else {
for key, value := range deployment.Labels {
if key == "app.kubernetes.io/name" && value == OperatorName {
operator = deployment.DeepCopy()
break
}
}
}
}
return operator
}

View File

@ -1,80 +0,0 @@
/*
Copyright © 2019 Vineeth Pothulapati <vineethpothulapati@outlook.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package cmd
import (
"fmt"
"log"
"strings"
"github.com/spf13/cobra"
"k8s.io/client-go/kubernetes"
)
var KubectlPgVersion string = "1.0"
// versionCmd represents the version command
var versionCmd = &cobra.Command{
Use: "version",
Short: "version of kubectl-pg & postgres-operator",
Long: `version of kubectl-pg and current running postgres-operator`,
Run: func(cmd *cobra.Command, args []string) {
namespace, err := cmd.Flags().GetString("namespace")
if err != nil {
log.Fatal(err)
}
version(namespace)
},
Example: `
#Lists the version of kubectl pg plugin and postgres operator in current namespace
kubectl pg version
#Lists the version of kubectl pg plugin and postgres operator in provided namespace
kubectl pg version -n namespace01
`,
}
func version(namespace string) {
fmt.Printf("kubectl-pg: %s\n", KubectlPgVersion)
config := getConfig()
client, err := kubernetes.NewForConfig(config)
if err != nil {
log.Fatal(err)
}
operatorDeployment := getPostgresOperator(client)
if operatorDeployment.Name == "" {
log.Fatalf("make sure zalando's postgres operator is running in namespace %s", namespace)
}
operatorImage := operatorDeployment.Spec.Template.Spec.Containers[0].Image
imageDetails := strings.Split(operatorImage, ":")
imageSplit := len(imageDetails)
imageVersion := imageDetails[imageSplit-1]
fmt.Printf("Postgres-Operator: %s\n", imageVersion)
}
func init() {
rootCmd.AddCommand(versionCmd)
versionCmd.Flags().StringP("namespace", "n", DefaultNamespace, "provide the namespace.")
}

View File

@ -1,72 +0,0 @@
module github.com/zalando/postgres-operator/kubectl-pg
go 1.25.3
require (
github.com/spf13/cobra v1.10.1
github.com/spf13/viper v1.21.0
github.com/zalando/postgres-operator v1.15.0
k8s.io/api v0.32.9
k8s.io/apiextensions-apiserver v0.25.9
k8s.io/apimachinery v0.32.9
k8s.io/client-go v0.32.9
)
require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/fsnotify/fsnotify v1.9.0 // indirect
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.9 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect
github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/moby/spdystream v0.5.0 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/sagikazarmark/locafero v0.11.0 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 // indirect
github.com/spf13/afero v1.15.0 // indirect
github.com/spf13/cast v1.10.0 // indirect
github.com/spf13/pflag v1.0.10 // indirect
github.com/subosito/gotenv v1.6.0 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.45.0 // indirect
golang.org/x/net v0.47.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect
golang.org/x/sys v0.38.0 // indirect
golang.org/x/term v0.37.0 // indirect
golang.org/x/text v0.31.0 // indirect
golang.org/x/time v0.9.0 // indirect
google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 // indirect
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect
sigs.k8s.io/yaml v1.4.0 // indirect
)

View File

@ -1,206 +0,0 @@
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g=
github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.19.6/go.mod h1:osyAmYz/mB/C3I+WsTTSgw1ONzaLJoLCyoi6/zppojs=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.20.2 h1:3sVjiK66+uXK/6oQ8xgcRKcFgQ5KXa2KvnJRumpMGbE=
github.com/go-openapi/jsonreference v0.20.2/go.mod h1:Bl1zwGIM8/wsvqjsOQLJ/SH+En5Ap4rVB5KVcIDZG2k=
github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs=
github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw=
github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo=
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA=
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/moby/spdystream v0.5.0 h1:7r0J1Si3QO/kjRitvSLVVFUjxMEb/YLj6S9FF62JBCU=
github.com/moby/spdystream v0.5.0/go.mod h1:xBAYlnt/ay+11ShkdFKNAG7LsyK/tmNBVvVOwrfMgdI=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d h1:LznySqW8MqVeFh+pW6rOkFdld9QQ7jRydBKKM6jyPVI=
github.com/motomux/pretty v0.0.0-20161209205251-b2aad2c9a95d/go.mod h1:u3hJ0kqCQu/cPpsu3RbCOPZ0d7V3IjPjv1adNRleM9I=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8=
github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/sagikazarmark/locafero v0.11.0 h1:1iurJgmM9G3PA/I+wWYIOw/5SyBtxapeHDcg+AAIFXc=
github.com/sagikazarmark/locafero v0.11.0/go.mod h1:nVIGvgyzw595SUSUE6tvCp3YYTeHs15MvlmU87WwIik=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8 h1:+jumHNA0Wrelhe64i8F6HNlS8pkoyMv5sreGx2Ry5Rw=
github.com/sourcegraph/conc v0.3.1-0.20240121214520-5f936abd7ae8/go.mod h1:3n1Cwaq1E1/1lhQhtRK2ts/ZwZEhjcQeJQ1RuC6Q/8U=
github.com/spf13/afero v1.15.0 h1:b/YBCLWAJdFWJTN9cLhiXXcD7mzKn9Dm86dNnfyQw1I=
github.com/spf13/afero v1.15.0/go.mod h1:NC2ByUVxtQs4b3sIUphxK0NioZnmxgyCrfzeuq8lxMg=
github.com/spf13/cast v1.10.0 h1:h2x0u2shc1QuLHfxi+cTJvs30+ZAHOGRic8uyGTDWxY=
github.com/spf13/cast v1.10.0/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.10.1 h1:lJeBwCfmrnXthfAupyUTzJ/J4Nc1RsHC/mSRU2dll/s=
github.com/spf13/cobra v1.10.1/go.mod h1:7SmJGaTHFVBY0jW4NXGluQoLvhqFQM+6XSKD+P4XaB0=
github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk=
github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
github.com/spf13/viper v1.21.0 h1:x5S+0EU27Lbphp4UKm1C+1oQO+rKx36vfCoaVebLFSU=
github.com/spf13/viper v1.21.0/go.mod h1:P0lhsswPGWD/1lZJ9ny3fYnVqxiegrlNrEmgLjbTCAY=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zalando/postgres-operator v1.15.0 h1:is/7cOrpuV7OwMiN7TG7GgiYHKvaWx8Ptw3hJruFO1I=
github.com/zalando/postgres-operator v1.15.0/go.mod h1:1cSOA5dG2dEqdG0uami1RHTGYX92bgAKYASfAhuMtHE=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.37.0 h1:8EGAD0qCmHYZg6J17DvsMy9/wJ7/D/4pV/wfnld5lTU=
golang.org/x/term v0.37.0/go.mod h1:5pB4lxRNYYVZuTLmy8oR2BH8dflOR+IbTYFD8fi3254=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.32.9 h1:q/59kk8lnecgG0grJqzrmXC1Jcl2hPWp9ltz0FQuoLI=
k8s.io/api v0.32.9/go.mod h1:jIfT3rwW4EU1IXZm9qjzSk/2j91k4CJL5vUULrxqp3Y=
k8s.io/apiextensions-apiserver v0.25.9 h1:Pycd6lm2auABp9wKQHCFSEPG+NPdFSTJXPST6NJFzB8=
k8s.io/apiextensions-apiserver v0.25.9/go.mod h1:ijGxmSG1GLOEaWhTuaEr0M7KUeia3mWCZa6FFQqpt1M=
k8s.io/apimachinery v0.32.9 h1:fXk8ktfsxrdThaEOAQFgkhCK7iyoyvS8nbYJ83o/SSs=
k8s.io/apimachinery v0.32.9/go.mod h1:GpHVgxoKlTxClKcteaeuF1Ul/lDVb74KpZcxcmLDElE=
k8s.io/client-go v0.32.9 h1:ZMyIQ1TEpTDAQni3L2gH1NZzyOA/gHfNcAazzCxMJ0c=
k8s.io/client-go v0.32.9/go.mod h1:2OT8aFSYvUjKGadaeT+AVbhkXQSpMAkiSb88Kz2WggI=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4=
k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro=
k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8=
sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo=
sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc=
sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps=
sigs.k8s.io/yaml v1.4.0 h1:Mk1wCc2gy/F0THH0TAp1QYyJNzRm2KCLy3o5ASXVI5E=
sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY=

View File

@ -1,31 +0,0 @@
/*
Copyright © 2019 Vineeth Pothulapati <vineethpothulapati@outlook.com>
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
*/
package main
import (
"github.com/zalando/postgres-operator/kubectl-pg/cmd"
)
func main() {
cmd.Execute()
}

View File

@ -108,7 +108,7 @@ spec:
description: load balancers' source ranges are the same for master
and replica services
items:
pattern: ^(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])$
pattern: ^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])|(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9]))$
type: string
nullable: true
type: array
@ -274,14 +274,26 @@ spec:
vars that enable load balancers are pointers because it is important to know if any of them is omitted from the Postgres manifest
in that case the var evaluates to nil and the value is taken from the operator config
type: boolean
enableMasterNodePort:
description: |-
vars to enable and configure nodeport services
set ports to 0 or nil to let kubernetes decide which port to use
overrides loadbalancer configuration
type: boolean
enableMasterPoolerLoadBalancer:
type: boolean
enableMasterPoolerNodePort:
type: boolean
enableReplicaConnectionPooler:
type: boolean
enableReplicaLoadBalancer:
type: boolean
enableReplicaNodePort:
type: boolean
enableReplicaPoolerLoadBalancer:
type: boolean
enableReplicaPoolerNodePort:
type: boolean
enableShmVolume:
type: boolean
env:
@ -3409,6 +3421,12 @@ spec:
pattern: '^\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))-((2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\ *$'
type: string
type: array
masterNodePort:
format: int32
type: integer
masterPoolerNodePort:
format: int32
type: integer
masterServiceAnnotations:
additionalProperties:
type: string
@ -3713,6 +3731,12 @@ spec:
replicaLoadBalancer:
description: deprecated
type: boolean
replicaNodePort:
format: int32
type: integer
replicaPoolerNodePort:
format: int32
type: integer
replicaServiceAnnotations:
additionalProperties:
type: string

View File

@ -748,6 +748,18 @@ var OperatorConfigCRDResourceValidation = apiextv1.CustomResourceValidation{
"enable_replica_pooler_load_balancer": {
Type: "boolean",
},
"enable_master_node_port": {
Type: "boolean",
},
"enable_master_pooler_node_port": {
Type: "boolean",
},
"enable_replica_node_port": {
Type: "boolean",
},
"enable_replica_pooler_node_port": {
Type: "boolean",
},
"external_traffic_policy": {
Type: "string",
Enum: []apiextv1.JSON{

View File

@ -137,17 +137,24 @@ type OperatorTimeouts struct {
// LoadBalancerConfiguration defines the LB configuration
type LoadBalancerConfiguration struct {
DbHostedZone string `json:"db_hosted_zone,omitempty"`
EnableMasterLoadBalancer bool `json:"enable_master_load_balancer,omitempty"`
EnableMasterPoolerLoadBalancer bool `json:"enable_master_pooler_load_balancer,omitempty"`
EnableReplicaLoadBalancer bool `json:"enable_replica_load_balancer,omitempty"`
EnableReplicaPoolerLoadBalancer bool `json:"enable_replica_pooler_load_balancer,omitempty"`
CustomServiceAnnotations map[string]string `json:"custom_service_annotations,omitempty"`
MasterDNSNameFormat config.StringTemplate `json:"master_dns_name_format,omitempty"`
MasterLegacyDNSNameFormat config.StringTemplate `json:"master_legacy_dns_name_format,omitempty"`
ReplicaDNSNameFormat config.StringTemplate `json:"replica_dns_name_format,omitempty"`
ReplicaLegacyDNSNameFormat config.StringTemplate `json:"replica_legacy_dns_name_format,omitempty"`
ExternalTrafficPolicy string `json:"external_traffic_policy" default:"Cluster"`
DbHostedZone string `json:"db_hosted_zone,omitempty"`
EnableMasterLoadBalancer bool `json:"enable_master_load_balancer,omitempty"`
EnableMasterPoolerLoadBalancer bool `json:"enable_master_pooler_load_balancer,omitempty"`
EnableReplicaLoadBalancer bool `json:"enable_replica_load_balancer,omitempty"`
EnableReplicaPoolerLoadBalancer bool `json:"enable_replica_pooler_load_balancer,omitempty"`
// kept in LoadBalancerConfiguration because all the other parameters apply here too
EnableMasterNodePort bool `json:"enable_master_node_port,omitempty"`
EnableMasterPoolerNodePort bool `json:"enable_master_pooler_node_port,omitempty"`
EnableReplicaNodePort bool `json:"enable_replica_node_port,omitempty"`
EnableReplicaPoolerNodePort bool `json:"enable_replica_pooler_node_port,omitempty"`
CustomServiceAnnotations map[string]string `json:"custom_service_annotations,omitempty"`
MasterDNSNameFormat config.StringTemplate `json:"master_dns_name_format,omitempty"`
MasterLegacyDNSNameFormat config.StringTemplate `json:"master_legacy_dns_name_format,omitempty"`
ReplicaDNSNameFormat config.StringTemplate `json:"replica_dns_name_format,omitempty"`
ReplicaLegacyDNSNameFormat config.StringTemplate `json:"replica_legacy_dns_name_format,omitempty"`
ExternalTrafficPolicy string `json:"external_traffic_policy" default:"Cluster"`
}
// AWSGCPConfiguration defines the configuration for AWS

View File

@ -108,7 +108,7 @@ spec:
description: load balancers' source ranges are the same for master
and replica services
items:
pattern: ^(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])$
pattern: ^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])|(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9]))$
type: string
nullable: true
type: array
@ -274,14 +274,26 @@ spec:
vars that enable load balancers are pointers because it is important to know if any of them is omitted from the Postgres manifest
in that case the var evaluates to nil and the value is taken from the operator config
type: boolean
enableMasterNodePort:
description: |-
vars to enable and configure nodeport services
set ports to 0 or nil to let kubernetes decide which port to use
overrides loadbalancer configuration
type: boolean
enableMasterPoolerLoadBalancer:
type: boolean
enableMasterPoolerNodePort:
type: boolean
enableReplicaConnectionPooler:
type: boolean
enableReplicaLoadBalancer:
type: boolean
enableReplicaNodePort:
type: boolean
enableReplicaPoolerLoadBalancer:
type: boolean
enableReplicaPoolerNodePort:
type: boolean
enableShmVolume:
type: boolean
env:
@ -3409,6 +3421,12 @@ spec:
pattern: '^\ *((Mon|Tue|Wed|Thu|Fri|Sat|Sun):(2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))-((2[0-3]|[01]?\d):([0-5]?\d)|(2[0-3]|[01]?\d):([0-5]?\d))\ *$'
type: string
type: array
masterNodePort:
format: int32
type: integer
masterPoolerNodePort:
format: int32
type: integer
masterServiceAnnotations:
additionalProperties:
type: string
@ -3713,6 +3731,12 @@ spec:
replicaLoadBalancer:
description: deprecated
type: boolean
replicaNodePort:
format: int32
type: integer
replicaPoolerNodePort:
format: int32
type: integer
replicaServiceAnnotations:
additionalProperties:
type: string

View File

@ -63,6 +63,18 @@ type PostgresSpec struct {
EnableReplicaLoadBalancer *bool `json:"enableReplicaLoadBalancer,omitempty"`
EnableReplicaPoolerLoadBalancer *bool `json:"enableReplicaPoolerLoadBalancer,omitempty"`
// vars to enable and configure nodeport services
// set ports to 0 or nil to let kubernetes decide which port to use
// overrides loadbalancer configuration
EnableMasterNodePort *bool `json:"enableMasterNodePort,omitempty"`
MasterNodePort *int32 `json:"masterNodePort,omitempty"`
EnableMasterPoolerNodePort *bool `json:"enableMasterPoolerNodePort,omitempty"`
MasterPoolerNodePort *int32 `json:"masterPoolerNodePort,omitempty"`
EnableReplicaNodePort *bool `json:"enableReplicaNodePort,omitempty"`
ReplicaNodePort *int32 `json:"replicaNodePort,omitempty"`
EnableReplicaPoolerNodePort *bool `json:"enableReplicaPoolerNodePort,omitempty"`
ReplicaPoolerNodePort *int32 `json:"replicaPoolerNodePort,omitempty"`
// deprecated load balancer settings maintained for backward compatibility
// see "Load balancers" operator docs
UseLoadBalancer *bool `json:"useLoadBalancer,omitempty"`
@ -71,7 +83,7 @@ type PostgresSpec struct {
// load balancers' source ranges are the same for master and replica services
// +nullable
// +kubebuilder:validation:items:Pattern=`^(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])$`
// +kubebuilder:validation:items:Pattern=`^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])|(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9]))$`
// +optional
AllowedSourceRanges []string `json:"allowedSourceRanges"`

View File

@ -5,6 +5,7 @@ import (
"encoding/json"
"errors"
"reflect"
"regexp"
"testing"
"time"
@ -810,3 +811,47 @@ func TestPostgresqlClone(t *testing.T) {
})
}
}
func TestAllowedSourceRangesPattern(t *testing.T) {
// pattern used in CRD validation for allowedSourceRanges
pattern := `^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\/(\d|[1-2]\d|3[0-2])|(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:))\/(12[0-8]|1[01][0-9]|[1-9]?[0-9]))$`
re := regexp.MustCompile(pattern)
valid := []string{
// IPv4
"192.168.1.0/24",
"0.0.0.0/0",
"127.0.0.1/32",
"10.0.0.0/8",
"185.85.220.0/22",
// IPv6
"fd01::/48",
"::1/128",
"::/0",
"2001:db8::/32",
"fe80::1/64",
"2001:0db8:85a3:0000:0000:8a2e:0370:7334/128",
}
invalid := []string{
"999.999.999.999/24",
"192.168.1.0/33",
"192.168.1.0",
"not-an-ip",
"fd01::/129",
"::gggg/64",
"",
}
for _, cidr := range valid {
if !re.MatchString(cidr) {
t.Errorf("expected %q to match allowedSourceRanges pattern", cidr)
}
}
for _, cidr := range invalid {
if re.MatchString(cidr) {
t.Errorf("expected %q NOT to match allowedSourceRanges pattern", cidr)
}
}
}

View File

@ -745,6 +745,46 @@ func (in *PostgresSpec) DeepCopyInto(out *PostgresSpec) {
*out = new(bool)
**out = **in
}
if in.EnableMasterNodePort != nil {
in, out := &in.EnableMasterNodePort, &out.EnableMasterNodePort
*out = new(bool)
**out = **in
}
if in.MasterNodePort != nil {
in, out := &in.MasterNodePort, &out.MasterNodePort
*out = new(int32)
**out = **in
}
if in.EnableMasterPoolerNodePort != nil {
in, out := &in.EnableMasterPoolerNodePort, &out.EnableMasterPoolerNodePort
*out = new(bool)
**out = **in
}
if in.MasterPoolerNodePort != nil {
in, out := &in.MasterPoolerNodePort, &out.MasterPoolerNodePort
*out = new(int32)
**out = **in
}
if in.EnableReplicaNodePort != nil {
in, out := &in.EnableReplicaNodePort, &out.EnableReplicaNodePort
*out = new(bool)
**out = **in
}
if in.ReplicaNodePort != nil {
in, out := &in.ReplicaNodePort, &out.ReplicaNodePort
*out = new(int32)
**out = **in
}
if in.EnableReplicaPoolerNodePort != nil {
in, out := &in.EnableReplicaPoolerNodePort, &out.EnableReplicaPoolerNodePort
*out = new(bool)
**out = **in
}
if in.ReplicaPoolerNodePort != nil {
in, out := &in.ReplicaPoolerNodePort, &out.ReplicaPoolerNodePort
*out = new(int32)
**out = **in
}
if in.UseLoadBalancer != nil {
in, out := &in.UseLoadBalancer, &out.UseLoadBalancer
*out = new(bool)

View File

@ -859,6 +859,14 @@ func (c *Cluster) compareServices(old, new *v1.Service) (bool, string) {
return false, "new service's ExternalTrafficPolicy does not match the current one"
}
if len(old.Spec.Ports) > 0 && len(new.Spec.Ports) > 0 {
// we need to check whether the new port is not zero (=user-defined)
// and only overwrite if it is
if new.Spec.Ports[0].NodePort != 0 && old.Spec.Ports[0].NodePort != new.Spec.Ports[0].NodePort {
return false, "new service's NodePort does not match the current one"
}
}
return true, ""
}
@ -893,6 +901,16 @@ func (c *Cluster) compareLogicalBackupJob(cur, new *batchv1.CronJob) *compareLog
reasons = append(reasons, fmt.Sprintf("new job's env PG_VERSION %q does not match the current one %q", newPgVersion, curPgVersion))
}
if !reflect.DeepEqual(cur.Labels, new.Labels) {
match = false
reasons = append(reasons, "new job's labels do not match the current ones")
}
if !reflect.DeepEqual(cur.Spec.JobTemplate.Labels, new.Spec.JobTemplate.Labels) {
match = false
reasons = append(reasons, "new job's template labels do not match the current ones")
}
needsReplace := false
contReasons := make([]string, 0)
needsReplace, contReasons = c.compareContainers("cronjob container", cur.Spec.JobTemplate.Spec.Template.Spec.Containers, new.Spec.JobTemplate.Spec.Template.Spec.Containers, needsReplace, contReasons)

View File

@ -1334,7 +1334,8 @@ func newService(
svcType v1.ServiceType,
sourceRanges []string,
selector map[string]string,
policy v1.ServiceExternalTrafficPolicyType) *v1.Service {
policy v1.ServiceExternalTrafficPolicyType,
nodePort *int32) *v1.Service {
svc := &v1.Service{
Spec: v1.ServiceSpec{
Selector: selector,
@ -1344,6 +1345,16 @@ func newService(
},
}
svc.Annotations = annotations
if nodePort != nil {
svc.Spec.Ports = []v1.ServicePort{
{
Name: "port",
NodePort: *nodePort,
},
}
}
return svc
}
@ -1370,6 +1381,7 @@ func TestCompareServices(t *testing.T) {
[]string{"128.141.0.0/16", "137.138.0.0/16"},
nil,
defaultPolicy,
nil,
)
ownerRef := metav1.OwnerReference{
@ -1381,6 +1393,9 @@ func TestCompareServices(t *testing.T) {
serviceWithOwnerReference.ObjectMeta.OwnerReferences = append(serviceWithOwnerReference.ObjectMeta.OwnerReferences, ownerRef)
portZero := int32(0)
portNotZero := int32(1337)
tests := []struct {
about string
current *v1.Service
@ -1396,14 +1411,14 @@ func TestCompareServices(t *testing.T) {
},
v1.ServiceTypeClusterIP,
[]string{"128.141.0.0/16", "137.138.0.0/16"},
nil, defaultPolicy),
nil, defaultPolicy, nil),
new: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
},
v1.ServiceTypeClusterIP,
[]string{"128.141.0.0/16", "137.138.0.0/16"},
nil, defaultPolicy),
nil, defaultPolicy, nil),
match: true,
},
{
@ -1414,14 +1429,14 @@ func TestCompareServices(t *testing.T) {
},
v1.ServiceTypeClusterIP,
[]string{"128.141.0.0/16", "137.138.0.0/16"},
nil, defaultPolicy),
nil, defaultPolicy, nil),
new: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"},
nil, defaultPolicy),
nil, defaultPolicy, nil),
match: false,
reason: `new service's type "LoadBalancer" does not match the current one "ClusterIP"`,
},
@ -1433,14 +1448,14 @@ func TestCompareServices(t *testing.T) {
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"},
nil, defaultPolicy),
nil, defaultPolicy, nil),
new: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
},
v1.ServiceTypeLoadBalancer,
[]string{"185.249.56.0/22"},
nil, defaultPolicy),
nil, defaultPolicy, nil),
match: false,
reason: `new service's LoadBalancerSourceRange does not match the current one`,
},
@ -1452,14 +1467,14 @@ func TestCompareServices(t *testing.T) {
},
v1.ServiceTypeLoadBalancer,
[]string{"128.141.0.0/16", "137.138.0.0/16"},
nil, defaultPolicy),
nil, defaultPolicy, nil),
new: newService(
map[string]string{
constants.ZalandoDNSNameAnnotation: "clstr.acid.zalan.do",
},
v1.ServiceTypeLoadBalancer,
[]string{},
nil, defaultPolicy),
nil, defaultPolicy, nil),
match: false,
reason: `new service's LoadBalancerSourceRange does not match the current one`,
},
@ -1471,7 +1486,7 @@ func TestCompareServices(t *testing.T) {
},
v1.ServiceTypeClusterIP,
[]string{"128.141.0.0/16", "137.138.0.0/16"},
nil, defaultPolicy),
nil, defaultPolicy, nil),
new: serviceWithOwnerReference,
match: false,
},
@ -1481,12 +1496,12 @@ func TestCompareServices(t *testing.T) {
map[string]string{},
v1.ServiceTypeClusterIP,
[]string{},
nil, defaultPolicy),
nil, defaultPolicy, nil),
new: newService(
map[string]string{},
v1.ServiceTypeClusterIP,
[]string{},
map[string]string{"cluster-name": "clstr", "spilo-role": "master"}, defaultPolicy),
map[string]string{"cluster-name": "clstr", "spilo-role": "master"}, defaultPolicy, nil),
match: false,
},
{
@ -1495,14 +1510,42 @@ func TestCompareServices(t *testing.T) {
map[string]string{},
v1.ServiceTypeClusterIP,
[]string{},
nil, defaultPolicy),
nil, defaultPolicy, nil),
new: newService(
map[string]string{},
v1.ServiceTypeClusterIP,
[]string{},
nil, v1.ServiceExternalTrafficPolicyTypeLocal),
nil, v1.ServiceExternalTrafficPolicyTypeLocal, nil),
match: false,
},
{
about: "services differ on node port",
current: newService(
map[string]string{},
v1.ServiceTypeNodePort,
[]string{},
nil, defaultPolicy, &portZero),
new: newService(
map[string]string{},
v1.ServiceTypeNodePort,
[]string{},
nil, defaultPolicy, &portNotZero),
match: false,
},
{
about: "services do not differ on node port when requesting 0",
current: newService(
map[string]string{},
v1.ServiceTypeNodePort,
[]string{},
nil, defaultPolicy, &portNotZero),
new: newService(
map[string]string{},
v1.ServiceTypeNodePort,
[]string{},
nil, defaultPolicy, &portZero),
match: true,
},
}
for _, tt := range tests {

View File

@ -597,7 +597,9 @@ func (c *Cluster) generateConnectionPoolerService(connectionPooler *ConnectionPo
},
}
if c.shouldCreateLoadBalancerForPoolerService(poolerRole, spec) {
if ok, port := c.shouldCreateNodePortForPoolerService(poolerRole, spec); ok {
c.configureNodePortService(&serviceSpec, port)
} else if c.shouldCreateLoadBalancerForPoolerService(poolerRole, spec) {
c.configureLoadBalanceService(&serviceSpec, spec.AllowedSourceRanges)
}
@ -625,7 +627,9 @@ func (c *Cluster) generatePoolerServiceAnnotations(role PostgresRole, spec *acid
var dnsString string
annotations := c.getCustomServiceAnnotations(role, spec)
if c.shouldCreateLoadBalancerForPoolerService(role, spec) {
nodePort, _ := c.shouldCreateNodePortForPoolerService(role, spec)
if !nodePort && c.shouldCreateLoadBalancerForPoolerService(role, spec) {
// -repl suffix will be added by replicaDNSName
clusterNameWithPoolerSuffix := c.connectionPoolerName(Master)
if role == Master {
@ -666,6 +670,37 @@ func (c *Cluster) shouldCreateLoadBalancerForPoolerService(role PostgresRole, sp
}
}
func (c *Cluster) shouldCreateNodePortForPoolerService(role PostgresRole, spec *acidv1.PostgresSpec) (bool, int32) {
switch role {
case Replica:
// if the value is explicitly set in a Postgresql manifest, follow this setting
if spec.EnableReplicaPoolerNodePort != nil {
port := int32(0)
if spec.ReplicaPoolerNodePort != nil {
port = *spec.ReplicaPoolerNodePort
}
return *spec.EnableReplicaPoolerNodePort, port
}
// otherwise, follow the operator configuration
return c.OpConfig.EnableReplicaPoolerNodePort, 0
case Master:
if spec.EnableMasterPoolerNodePort != nil {
port := int32(0)
if spec.MasterPoolerNodePort != nil {
port = *spec.MasterPoolerNodePort
}
return *spec.EnableMasterPoolerNodePort, port
}
return c.OpConfig.EnableMasterPoolerNodePort, 0
default:
panic(fmt.Sprintf("Unknown role %v", role))
}
}
func (c *Cluster) listPoolerPods(listOptions metav1.ListOptions) ([]v1.Pod, error) {
pods, err := c.KubeClient.Pods(c.Namespace).List(context.TODO(), listOptions)
if err != nil {

View File

@ -1285,3 +1285,181 @@ func TestConnectionPoolerServiceSpec(t *testing.T) {
}
}
}
func TestConnectionPoolerServiceType(t *testing.T) {
testName := "Test connection pooler service type selection"
cluster := New(
Config{
OpConfig: config.Config{
ProtectedRoles: []string{"admin"},
Auth: config.Auth{
SuperUsername: superUserName,
ReplicationUsername: replicationUserName,
},
ConnectionPooler: config.ConnectionPooler{
ConnectionPoolerDefaultCPURequest: "100m",
ConnectionPoolerDefaultCPULimit: "100m",
ConnectionPoolerDefaultMemoryRequest: "100Mi",
ConnectionPoolerDefaultMemoryLimit: "100Mi",
},
Resources: config.Resources{
EnableOwnerReferences: util.True(),
},
},
},
k8sutil.KubernetesClient{},
acidv1.Postgresql{},
logger,
eventRecorder,
)
cluster.Statefulset = &appsv1.StatefulSet{
ObjectMeta: metav1.ObjectMeta{
Name: "test-sts",
},
}
cluster.ConnectionPooler = map[PostgresRole]*ConnectionPoolerObjects{
Master: {
Deployment: nil,
Service: nil,
LookupFunction: false,
Role: Master,
},
Replica: {
Deployment: nil,
Service: nil,
LookupFunction: false,
Role: Replica,
},
}
tests := []struct {
subTest string
spec *acidv1.PostgresSpec
cluster *Cluster
expectedType map[PostgresRole]v1.ServiceType
}{
{
subTest: "default configuration -> ClusterIP for both",
spec: &acidv1.PostgresSpec{
ConnectionPooler: &acidv1.ConnectionPooler{},
},
cluster: cluster,
expectedType: map[PostgresRole]v1.ServiceType{
Master: v1.ServiceTypeClusterIP,
Replica: v1.ServiceTypeClusterIP,
},
},
{
subTest: "LoadBalancer for both roles",
spec: &acidv1.PostgresSpec{
ConnectionPooler: &acidv1.ConnectionPooler{},
EnableMasterPoolerLoadBalancer: boolToPointer(true),
EnableReplicaPoolerLoadBalancer: boolToPointer(true),
},
cluster: cluster,
expectedType: map[PostgresRole]v1.ServiceType{
Master: v1.ServiceTypeLoadBalancer,
Replica: v1.ServiceTypeLoadBalancer,
},
},
{
subTest: "LoadBalancer for master",
spec: &acidv1.PostgresSpec{
ConnectionPooler: &acidv1.ConnectionPooler{},
EnableMasterPoolerLoadBalancer: boolToPointer(true),
},
cluster: cluster,
expectedType: map[PostgresRole]v1.ServiceType{
Master: v1.ServiceTypeLoadBalancer,
Replica: v1.ServiceTypeClusterIP,
},
},
{
subTest: "LoadBalancer for replica",
spec: &acidv1.PostgresSpec{
ConnectionPooler: &acidv1.ConnectionPooler{},
EnableReplicaPoolerLoadBalancer: boolToPointer(true),
},
cluster: cluster,
expectedType: map[PostgresRole]v1.ServiceType{
Master: v1.ServiceTypeClusterIP,
Replica: v1.ServiceTypeLoadBalancer,
},
},
{
subTest: "NodePort for both roles",
spec: &acidv1.PostgresSpec{
ConnectionPooler: &acidv1.ConnectionPooler{},
EnableMasterPoolerNodePort: boolToPointer(true),
EnableReplicaPoolerNodePort: boolToPointer(true),
},
cluster: cluster,
expectedType: map[PostgresRole]v1.ServiceType{
Master: v1.ServiceTypeNodePort,
Replica: v1.ServiceTypeNodePort,
},
},
{
subTest: "NodePort for master",
spec: &acidv1.PostgresSpec{
ConnectionPooler: &acidv1.ConnectionPooler{},
EnableMasterPoolerNodePort: boolToPointer(true),
},
cluster: cluster,
expectedType: map[PostgresRole]v1.ServiceType{
Master: v1.ServiceTypeNodePort,
Replica: v1.ServiceTypeClusterIP,
},
},
{
subTest: "NodePort for replica",
spec: &acidv1.PostgresSpec{
ConnectionPooler: &acidv1.ConnectionPooler{},
EnableReplicaPoolerNodePort: boolToPointer(true),
},
cluster: cluster,
expectedType: map[PostgresRole]v1.ServiceType{
Master: v1.ServiceTypeClusterIP,
Replica: v1.ServiceTypeNodePort,
},
},
{
subTest: "NodePort overrides LoadBalancer for both roles",
spec: &acidv1.PostgresSpec{
ConnectionPooler: &acidv1.ConnectionPooler{},
EnableMasterPoolerLoadBalancer: boolToPointer(true),
EnableReplicaPoolerLoadBalancer: boolToPointer(true),
EnableMasterPoolerNodePort: boolToPointer(true),
EnableReplicaPoolerNodePort: boolToPointer(true),
},
cluster: cluster,
expectedType: map[PostgresRole]v1.ServiceType{
Master: v1.ServiceTypeNodePort,
Replica: v1.ServiceTypeNodePort,
},
},
}
roles := []PostgresRole{Master, Replica}
for _, tt := range tests {
tt.cluster.Spec = *tt.spec
for _, role := range roles {
svc := tt.cluster.generateConnectionPoolerService(tt.cluster.ConnectionPooler[role])
expected, ok := tt.expectedType[role]
if !ok {
t.Fatalf("%s [%s]: missing expectedType for role %v", testName, tt.subTest, role)
}
if svc.Spec.Type != expected {
t.Errorf("%s [%s] role=%s: service Type is incorrect, got %s, expected %s",
testName, tt.subTest, role, svc.Spec.Type, expected)
}
}
}
}

View File

@ -2004,6 +2004,37 @@ func (c *Cluster) shouldCreateLoadBalancerForService(role PostgresRole, spec *ac
}
func (c *Cluster) shouldCreateNodePortForService(role PostgresRole, spec *acidv1.PostgresSpec) (bool, int32) {
switch role {
case Replica:
// if the value is explicitly set in a Postgresql manifest, follow this setting
if spec.EnableReplicaNodePort != nil {
port := int32(0)
if spec.ReplicaNodePort != nil {
port = *spec.ReplicaNodePort
}
return *spec.EnableReplicaNodePort, port
}
// otherwise, follow the operator configuration
return c.OpConfig.EnableReplicaNodePort, 0
case Master:
if spec.EnableMasterNodePort != nil {
port := int32(0)
if spec.MasterNodePort != nil {
port = *spec.MasterNodePort
}
return *spec.EnableMasterNodePort, port
}
return c.OpConfig.EnableMasterNodePort, 0
default:
panic(fmt.Sprintf("Unknown role %v", role))
}
}
func (c *Cluster) generateService(role PostgresRole, spec *acidv1.PostgresSpec) *v1.Service {
serviceSpec := v1.ServiceSpec{
Ports: []v1.ServicePort{{Name: "postgresql", Port: pgPort, TargetPort: intstr.IntOrString{IntVal: pgPort}}},
@ -2016,7 +2047,9 @@ func (c *Cluster) generateService(role PostgresRole, spec *acidv1.PostgresSpec)
serviceSpec.Selector = c.roleLabelsSet(false, role)
}
if c.shouldCreateLoadBalancerForService(role, spec) {
if ok, port := c.shouldCreateNodePortForService(role, spec); ok {
c.configureNodePortService(&serviceSpec, port)
} else if c.shouldCreateLoadBalancerForService(role, spec) {
c.configureLoadBalanceService(&serviceSpec, spec.AllowedSourceRanges)
}
@ -2049,10 +2082,21 @@ func (c *Cluster) configureLoadBalanceService(serviceSpec *v1.ServiceSpec, sourc
serviceSpec.Type = v1.ServiceTypeLoadBalancer
}
func (c *Cluster) configureNodePortService(serviceSpec *v1.ServiceSpec, port int32) {
serviceSpec.ExternalTrafficPolicy = v1.ServiceExternalTrafficPolicyType(c.OpConfig.ExternalTrafficPolicy)
serviceSpec.Type = v1.ServiceTypeNodePort
if port != 0 && len(serviceSpec.Ports) > 0 {
serviceSpec.Ports[0].NodePort = port
}
}
func (c *Cluster) generateServiceAnnotations(role PostgresRole, spec *acidv1.PostgresSpec) map[string]string {
annotations := c.getCustomServiceAnnotations(role, spec)
if c.shouldCreateLoadBalancerForService(role, spec) {
nodePort, _ := c.shouldCreateNodePortForService(role, spec)
if !nodePort && c.shouldCreateLoadBalancerForService(role, spec) {
dnsName := c.dnsName(role)
// External DNS name annotation is not customizable
@ -2414,6 +2458,10 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) {
// configure a cron job
jobTemplateSpec := batchv1.JobTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: labels,
Annotations: c.annotationsSet(annotations),
},
Spec: jobSpec,
}
@ -2426,8 +2474,8 @@ func (c *Cluster) generateLogicalBackupJob() (*batchv1.CronJob, error) {
ObjectMeta: metav1.ObjectMeta{
Name: c.getLogicalBackupJobName(),
Namespace: c.Namespace,
Labels: c.labelsSet(true),
Annotations: c.annotationsSet(nil),
Labels: labels,
Annotations: c.annotationsSet(annotations),
OwnerReferences: c.ownerReferences(),
},
Spec: batchv1.CronJobSpec{

View File

@ -2971,32 +2971,32 @@ func newLBFakeClient() (k8sutil.KubernetesClient, *fake.Clientset) {
}, clientSet
}
func getServices(serviceType v1.ServiceType, sourceRanges []string, extTrafficPolicy, clusterName string) []v1.ServiceSpec {
func getServices(serviceType v1.ServiceType, sourceRanges []string, extTrafficPolicy, clusterName string, nodePort int32) []v1.ServiceSpec {
return []v1.ServiceSpec{
{
ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyType(extTrafficPolicy),
LoadBalancerSourceRanges: sourceRanges,
Ports: []v1.ServicePort{{Name: "postgresql", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}}},
Ports: []v1.ServicePort{{Name: "postgresql", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}, NodePort: nodePort}},
Type: serviceType,
},
{
ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyType(extTrafficPolicy),
LoadBalancerSourceRanges: sourceRanges,
Ports: []v1.ServicePort{{Name: clusterName + "-pooler", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}}},
Ports: []v1.ServicePort{{Name: clusterName + "-pooler", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}, NodePort: nodePort}},
Selector: map[string]string{"connection-pooler": clusterName + "-pooler"},
Type: serviceType,
},
{
ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyType(extTrafficPolicy),
LoadBalancerSourceRanges: sourceRanges,
Ports: []v1.ServicePort{{Name: "postgresql", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}}},
Ports: []v1.ServicePort{{Name: "postgresql", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}, NodePort: nodePort}},
Selector: map[string]string{"spilo-role": "replica", "application": "spilo", "cluster-name": clusterName},
Type: serviceType,
},
{
ExternalTrafficPolicy: v1.ServiceExternalTrafficPolicyType(extTrafficPolicy),
LoadBalancerSourceRanges: sourceRanges,
Ports: []v1.ServicePort{{Name: clusterName + "-pooler-repl", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}}},
Ports: []v1.ServicePort{{Name: clusterName + "-pooler-repl", Port: 5432, TargetPort: intstr.IntOrString{IntVal: 5432}, NodePort: nodePort}},
Selector: map[string]string{"connection-pooler": clusterName + "-pooler-repl"},
Type: serviceType,
},
@ -3064,7 +3064,7 @@ func TestEnableLoadBalancers(t *testing.T) {
},
},
},
expectedServices: getServices(v1.ServiceTypeClusterIP, nil, "", clusterName),
expectedServices: getServices(v1.ServiceTypeClusterIP, nil, "", clusterName, 0),
},
{
subTest: "LBs enabled in manifest, disabled in config",
@ -3111,7 +3111,7 @@ func TestEnableLoadBalancers(t *testing.T) {
},
},
},
expectedServices: getServices(v1.ServiceTypeLoadBalancer, sourceRanges, extTrafficPolicy, clusterName),
expectedServices: getServices(v1.ServiceTypeLoadBalancer, sourceRanges, extTrafficPolicy, clusterName, 0),
},
}
@ -3143,6 +3143,195 @@ func TestEnableLoadBalancers(t *testing.T) {
}
}
func TestEnableNodePorts(t *testing.T) {
clusterName := "acid-test-cluster"
namespace := "default"
clusterNameLabel := "cluster-name"
roleLabel := "spilo-role"
roles := []PostgresRole{Master, Replica}
extTrafficPolicy := "Cluster"
port := int32(1337)
tests := []struct {
subTest string
config config.Config
pgSpec acidv1.Postgresql
expectedServices []v1.ServiceSpec
}{
{
subTest: "NodePorts enabled in config, disabled in manifest",
config: config.Config{
ConnectionPooler: config.ConnectionPooler{
ConnectionPoolerDefaultCPURequest: "100m",
ConnectionPoolerDefaultCPULimit: "100m",
ConnectionPoolerDefaultMemoryRequest: "100Mi",
ConnectionPoolerDefaultMemoryLimit: "100Mi",
NumberOfInstances: k8sutil.Int32ToPointer(1),
},
EnableMasterNodePort: true,
EnableMasterPoolerNodePort: true,
EnableReplicaNodePort: true,
EnableReplicaPoolerNodePort: true,
ExternalTrafficPolicy: extTrafficPolicy,
Resources: config.Resources{
ClusterLabels: map[string]string{"application": "spilo"},
ClusterNameLabel: clusterNameLabel,
PodRoleLabel: roleLabel,
},
},
pgSpec: acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{
Name: clusterName,
Namespace: namespace,
},
Spec: acidv1.PostgresSpec{
EnableConnectionPooler: util.True(),
EnableReplicaConnectionPooler: util.True(),
EnableMasterNodePort: util.False(),
EnableMasterPoolerNodePort: util.False(),
EnableReplicaNodePort: util.False(),
EnableReplicaPoolerNodePort: util.False(),
NumberOfInstances: 1,
Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")},
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")},
},
TeamID: "acid",
Volume: acidv1.Volume{
Size: "1G",
},
},
},
expectedServices: getServices(v1.ServiceTypeClusterIP, nil, "", clusterName, 0),
},
{
subTest: "NodePorts configured in manifest, disabled in config",
config: config.Config{
ConnectionPooler: config.ConnectionPooler{
ConnectionPoolerDefaultCPURequest: "100m",
ConnectionPoolerDefaultCPULimit: "100m",
ConnectionPoolerDefaultMemoryRequest: "100Mi",
ConnectionPoolerDefaultMemoryLimit: "100Mi",
NumberOfInstances: k8sutil.Int32ToPointer(1),
},
EnableMasterNodePort: false,
EnableMasterPoolerNodePort: false,
EnableReplicaNodePort: false,
EnableReplicaPoolerNodePort: false,
ExternalTrafficPolicy: extTrafficPolicy,
Resources: config.Resources{
ClusterLabels: map[string]string{"application": "spilo"},
ClusterNameLabel: clusterNameLabel,
PodRoleLabel: roleLabel,
},
},
pgSpec: acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{
Name: clusterName,
Namespace: namespace,
},
Spec: acidv1.PostgresSpec{
EnableConnectionPooler: util.True(),
EnableReplicaConnectionPooler: util.True(),
EnableMasterNodePort: util.True(),
EnableMasterPoolerNodePort: util.True(),
EnableReplicaNodePort: util.True(),
EnableReplicaPoolerNodePort: util.True(),
NumberOfInstances: 1,
Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")},
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")},
},
TeamID: "acid",
Volume: acidv1.Volume{
Size: "1G",
},
},
},
expectedServices: getServices(v1.ServiceTypeNodePort, nil, extTrafficPolicy, clusterName, 0),
},
{
subTest: "NodePorts configured in manifest, disabled in config, custom port specified",
config: config.Config{
ConnectionPooler: config.ConnectionPooler{
ConnectionPoolerDefaultCPURequest: "100m",
ConnectionPoolerDefaultCPULimit: "100m",
ConnectionPoolerDefaultMemoryRequest: "100Mi",
ConnectionPoolerDefaultMemoryLimit: "100Mi",
NumberOfInstances: k8sutil.Int32ToPointer(1),
},
EnableMasterNodePort: false,
EnableMasterPoolerNodePort: false,
EnableReplicaNodePort: false,
EnableReplicaPoolerNodePort: false,
ExternalTrafficPolicy: extTrafficPolicy,
Resources: config.Resources{
ClusterLabels: map[string]string{"application": "spilo"},
ClusterNameLabel: clusterNameLabel,
PodRoleLabel: roleLabel,
},
},
pgSpec: acidv1.Postgresql{
ObjectMeta: metav1.ObjectMeta{
Name: clusterName,
Namespace: namespace,
},
Spec: acidv1.PostgresSpec{
EnableConnectionPooler: util.True(),
EnableReplicaConnectionPooler: util.True(),
EnableMasterNodePort: util.True(),
MasterNodePort: &port,
EnableMasterPoolerNodePort: util.True(),
MasterPoolerNodePort: &port,
EnableReplicaNodePort: util.True(),
ReplicaNodePort: &port,
EnableReplicaPoolerNodePort: util.True(),
ReplicaPoolerNodePort: &port,
NumberOfInstances: 1,
Resources: &acidv1.Resources{
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")},
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("10")},
},
TeamID: "acid",
Volume: acidv1.Volume{
Size: "1G",
},
},
},
expectedServices: getServices(v1.ServiceTypeNodePort, nil, extTrafficPolicy, clusterName, port),
},
}
for _, tt := range tests {
client, _ := newLBFakeClient()
var cluster = New(
Config{
OpConfig: tt.config,
}, client, tt.pgSpec, logger, eventRecorder)
cluster.Name = clusterName
cluster.Namespace = namespace
cluster.ConnectionPooler = map[PostgresRole]*ConnectionPoolerObjects{}
generatedServices := make([]v1.ServiceSpec, 0)
for _, role := range roles {
cluster.syncService(role)
cluster.ConnectionPooler[role] = &ConnectionPoolerObjects{
Name: cluster.connectionPoolerName(role),
ClusterName: cluster.Name,
Namespace: cluster.Namespace,
Role: role,
}
cluster.syncConnectionPoolerWorker(&tt.pgSpec, &tt.pgSpec, role)
generatedServices = append(generatedServices, cluster.Services[role].Spec)
generatedServices = append(generatedServices, cluster.ConnectionPooler[role].Service.Spec)
}
if !reflect.DeepEqual(tt.expectedServices, generatedServices) {
t.Errorf("%s %s: expected %#v but got %#v", t.Name(), tt.subTest, tt.expectedServices, generatedServices)
}
}
}
func TestGenerateResourceRequirements(t *testing.T) {
client, _ := newFakeK8sTestClient()
clusterName := "acid-test-cluster"
@ -3875,7 +4064,7 @@ func TestGenerateLogicalBackupJob(t *testing.T) {
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("100m"), Memory: k8sutil.StringToPointer("100Mi")},
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("500Mi")},
},
expectedLabel: map[string]string{configResources.ClusterNameLabel: clusterName, "team": teamId},
expectedLabel: map[string]string{"application": "spilo-logical-backup", configResources.ClusterNameLabel: clusterName, "team": teamId},
expectedAnnotation: nil,
},
{
@ -3900,7 +4089,7 @@ func TestGenerateLogicalBackupJob(t *testing.T) {
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("10m"), Memory: k8sutil.StringToPointer("50Mi")},
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("300m"), Memory: k8sutil.StringToPointer("300Mi")},
},
expectedLabel: map[string]string{configResources.ClusterNameLabel: clusterName, "team": teamId},
expectedLabel: map[string]string{"application": "spilo-logical-backup", configResources.ClusterNameLabel: clusterName, "team": teamId},
expectedAnnotation: nil,
},
{
@ -3923,7 +4112,7 @@ func TestGenerateLogicalBackupJob(t *testing.T) {
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("50m"), Memory: k8sutil.StringToPointer("100Mi")},
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("250m"), Memory: k8sutil.StringToPointer("500Mi")},
},
expectedLabel: map[string]string{configResources.ClusterNameLabel: clusterName, "team": teamId},
expectedLabel: map[string]string{"application": "spilo-logical-backup", configResources.ClusterNameLabel: clusterName, "team": teamId},
expectedAnnotation: nil,
},
{
@ -3946,7 +4135,7 @@ func TestGenerateLogicalBackupJob(t *testing.T) {
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("100m"), Memory: k8sutil.StringToPointer("200Mi")},
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("200Mi")},
},
expectedLabel: map[string]string{configResources.ClusterNameLabel: clusterName, "team": teamId},
expectedLabel: map[string]string{"application": "spilo-logical-backup", configResources.ClusterNameLabel: clusterName, "team": teamId},
expectedAnnotation: nil,
},
{
@ -3968,7 +4157,7 @@ func TestGenerateLogicalBackupJob(t *testing.T) {
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("100m"), Memory: k8sutil.StringToPointer("100Mi")},
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("500Mi")},
},
expectedLabel: map[string]string{"labelKey": "labelValue", "cluster-name": clusterName, "team": teamId},
expectedLabel: map[string]string{"application": "spilo-logical-backup", "labelKey": "labelValue", "cluster-name": clusterName, "team": teamId},
expectedAnnotation: nil,
},
{
@ -3990,7 +4179,7 @@ func TestGenerateLogicalBackupJob(t *testing.T) {
ResourceRequests: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("100m"), Memory: k8sutil.StringToPointer("100Mi")},
ResourceLimits: acidv1.ResourceDescription{CPU: k8sutil.StringToPointer("1"), Memory: k8sutil.StringToPointer("500Mi")},
},
expectedLabel: map[string]string{configResources.ClusterNameLabel: clusterName, "team": teamId},
expectedLabel: map[string]string{"application": "spilo-logical-backup", configResources.ClusterNameLabel: clusterName, "team": teamId},
expectedAnnotation: map[string]string{"annotationKey": "annotationValue"},
},
}

View File

@ -324,9 +324,14 @@ func (c *Cluster) updateService(role PostgresRole, oldService *v1.Service, newSe
// patch does not work because of LoadBalancerSourceRanges field (even if set to nil)
oldServiceType := oldService.Spec.Type
newServiceType := newService.Spec.Type
if newServiceType == "ClusterIP" && newServiceType != oldServiceType {
if newServiceType != oldServiceType && oldServiceType == v1.ServiceTypeLoadBalancer {
// Kubernetes rejects updates that change type away from LoadBalancer while
// loadBalancerSourceRanges is still set; clear it before updating
newService.Spec.LoadBalancerSourceRanges = nil
newService.ResourceVersion = oldService.ResourceVersion
newService.Spec.ClusterIP = oldService.Spec.ClusterIP
if newServiceType == v1.ServiceTypeClusterIP {
newService.Spec.ClusterIP = oldService.Spec.ClusterIP
}
}
svc, err = c.KubeClient.Services(serviceName.Namespace).Update(context.TODO(), newService, metav1.UpdateOptions{})
if err != nil {

View File

@ -1769,6 +1769,16 @@ func (c *Cluster) syncLogicalBackupJob() error {
}
c.logger.Info("the logical backup job is synced")
}
if !reflect.DeepEqual(job.Labels, desiredJob.Labels) {
patchData, err := metaLabelsPatch(desiredJob.Labels)
if err != nil {
return fmt.Errorf("could not form patch for the logical backup job %q labels: %v", jobName, err)
}
_, err = c.KubeClient.CronJobs(c.Namespace).Patch(context.TODO(), jobName, types.MergePatchType, []byte(patchData), metav1.PatchOptions{})
if err != nil {
return fmt.Errorf("could not patch labels of the logical backup job %q: %v", jobName, err)
}
}
if changed, _ := c.compareAnnotations(job.Annotations, desiredJob.Annotations, nil); changed {
patchData, err := metaAnnotationsPatch(desiredJob.Annotations)
if err != nil {

View File

@ -167,6 +167,14 @@ func metaAnnotationsPatch(annotations map[string]string) ([]byte, error) {
}{&meta})
}
func metaLabelsPatch(labels map[string]string) ([]byte, error) {
var meta metav1.ObjectMeta
meta.Labels = labels
return json.Marshal(struct {
ObjMeta interface{} `json:"metadata"`
}{&meta})
}
func (c *Cluster) logPDBChanges(old, new *policyv1.PodDisruptionBudget, isUpdate bool, reason string) {
if isUpdate {
c.logger.Infof("pod disruption budget %q has been changed", util.NameFromMeta(old.ObjectMeta))

View File

@ -171,6 +171,10 @@ func (c *Controller) importConfigurationFromCRD(fromCRD *acidv1.OperatorConfigur
result.EnableMasterPoolerLoadBalancer = fromCRD.LoadBalancer.EnableMasterPoolerLoadBalancer
result.EnableReplicaLoadBalancer = fromCRD.LoadBalancer.EnableReplicaLoadBalancer
result.EnableReplicaPoolerLoadBalancer = fromCRD.LoadBalancer.EnableReplicaPoolerLoadBalancer
result.EnableMasterNodePort = fromCRD.LoadBalancer.EnableMasterNodePort
result.EnableMasterPoolerNodePort = fromCRD.LoadBalancer.EnableMasterPoolerNodePort
result.EnableReplicaNodePort = fromCRD.LoadBalancer.EnableReplicaNodePort
result.EnableReplicaPoolerNodePort = fromCRD.LoadBalancer.EnableReplicaPoolerNodePort
result.CustomServiceAnnotations = fromCRD.LoadBalancer.CustomServiceAnnotations
result.MasterDNSNameFormat = fromCRD.LoadBalancer.MasterDNSNameFormat
result.MasterLegacyDNSNameFormat = fromCRD.LoadBalancer.MasterLegacyDNSNameFormat

View File

@ -220,6 +220,10 @@ type Config struct {
EnableMasterPoolerLoadBalancer bool `name:"enable_master_pooler_load_balancer" default:"false"`
EnableReplicaLoadBalancer bool `name:"enable_replica_load_balancer" default:"false"`
EnableReplicaPoolerLoadBalancer bool `name:"enable_replica_pooler_load_balancer" default:"false"`
EnableMasterNodePort bool `name:"enable_master_node_port" default:"false"`
EnableMasterPoolerNodePort bool `name:"enable_master_pooler_node_port" default:"false"`
EnableReplicaNodePort bool `name:"enable_replica_node_port" default:"false"`
EnableReplicaPoolerNodePort bool `name:"enable_replica_pooler_node_port" default:"false"`
CustomServiceAnnotations map[string]string `name:"custom_service_annotations"`
CustomPodAnnotations map[string]string `name:"custom_pod_annotations"`
EnablePodAntiAffinity bool `name:"enable_pod_antiaffinity" default:"false"`