diff --git a/.github/workflows/publish_ghcr_image.yaml b/.github/workflows/publish_ghcr_image.yaml index 2425e39b3..5a0c3b045 100644 --- a/.github/workflows/publish_ghcr_image.yaml +++ b/.github/workflows/publish_ghcr_image.yaml @@ -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 diff --git a/.gitignore b/.gitignore index 5938db216..65aad49c5 100644 --- a/.gitignore +++ b/.gitignore @@ -26,7 +26,6 @@ _testmain.go *.test *.prof /vendor/ -/kubectl-pg/vendor/ /build/ /docker/build/ /github.com/ diff --git a/charts/postgres-operator/crds/operatorconfigurations.yaml b/charts/postgres-operator/crds/operatorconfigurations.yaml index 28b0584aa..ed06ce2cc 100644 --- a/charts/postgres-operator/crds/operatorconfigurations.yaml +++ b/charts/postgres-operator/crds/operatorconfigurations.yaml @@ -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: diff --git a/charts/postgres-operator/crds/postgresqls.yaml b/charts/postgres-operator/crds/postgresqls.yaml index 37bea3985..2bbf5ee49 100644 --- a/charts/postgres-operator/crds/postgresqls.yaml +++ b/charts/postgres-operator/crds/postgresqls.yaml @@ -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 diff --git a/docs/administrator.md b/docs/administrator.md index e854775ce..ec0bc28dc 100644 --- a/docs/administrator.md +++ b/docs/administrator.md @@ -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 diff --git a/docs/reference/cluster_manifest.md b/docs/reference/cluster_manifest.md index a45350f38..0717e411f 100644 --- a/docs/reference/cluster_manifest.md +++ b/docs/reference/cluster_manifest.md @@ -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 diff --git a/e2e/tests/k8s_api.py b/e2e/tests/k8s_api.py index 1f42ad4bc..0ef3d6315 100644 --- a/e2e/tests/k8s_api.py +++ b/e2e/tests/k8s_api.py @@ -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): diff --git a/go.mod b/go.mod index e0e0b1956..9efa24150 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index a1fa39389..0d0ebb7d2 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/kubectl-pg/README.md b/kubectl-pg/README.md deleted file mode 100644 index 8213d4ff5..000000000 --- a/kubectl-pg/README.md +++ /dev/null @@ -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 diff --git a/kubectl-pg/build.sh b/kubectl-pg/build.sh deleted file mode 100755 index a81bf54fc..000000000 --- a/kubectl-pg/build.sh +++ /dev/null @@ -1,4 +0,0 @@ - -VERSION=1.0 -sed -i "s/KubectlPgVersion string = \"[^\"]*\"/KubectlPgVersion string = \"${VERSION}\"/" cmd/version.go -go install \ No newline at end of file diff --git a/kubectl-pg/cmd/addDb.go b/kubectl-pg/cmd/addDb.go deleted file mode 100644 index 1c33579d9..000000000 --- a/kubectl-pg/cmd/addDb.go +++ /dev/null @@ -1,114 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -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) -} diff --git a/kubectl-pg/cmd/addUser.go b/kubectl-pg/cmd/addUser.go deleted file mode 100644 index 602adb51d..000000000 --- a/kubectl-pg/cmd/addUser.go +++ /dev/null @@ -1,144 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -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) -} diff --git a/kubectl-pg/cmd/check.go b/kubectl-pg/cmd/check.go deleted file mode 100644 index 6068c35bb..000000000 --- a/kubectl-pg/cmd/check.go +++ /dev/null @@ -1,74 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -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) -} diff --git a/kubectl-pg/cmd/connect.go b/kubectl-pg/cmd/connect.go deleted file mode 100644 index a7643ca05..000000000 --- a/kubectl-pg/cmd/connect.go +++ /dev/null @@ -1,144 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -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) -} diff --git a/kubectl-pg/cmd/create.go b/kubectl-pg/cmd/create.go deleted file mode 100644 index 3d34a7d25..000000000 --- a/kubectl-pg/cmd/create.go +++ /dev/null @@ -1,82 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -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) -} diff --git a/kubectl-pg/cmd/delete.go b/kubectl-pg/cmd/delete.go deleted file mode 100644 index 73a6e7b0b..000000000 --- a/kubectl-pg/cmd/delete.go +++ /dev/null @@ -1,134 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -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) -} diff --git a/kubectl-pg/cmd/extVolume.go b/kubectl-pg/cmd/extVolume.go deleted file mode 100644 index 02ccc372d..000000000 --- a/kubectl-pg/cmd/extVolume.go +++ /dev/null @@ -1,119 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -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) -} diff --git a/kubectl-pg/cmd/list.go b/kubectl-pg/cmd/list.go deleted file mode 100644 index 4fd6de3ba..000000000 --- a/kubectl-pg/cmd/list.go +++ /dev/null @@ -1,125 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -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) -} diff --git a/kubectl-pg/cmd/logs.go b/kubectl-pg/cmd/logs.go deleted file mode 100644 index 21a4fd6ec..000000000 --- a/kubectl-pg/cmd/logs.go +++ /dev/null @@ -1,143 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -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.") -} diff --git a/kubectl-pg/cmd/root.go b/kubectl-pg/cmd/root.go deleted file mode 100644 index 163d6f6ea..000000000 --- a/kubectl-pg/cmd/root.go +++ /dev/null @@ -1,51 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -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 ") - viper.SetDefault("license", "mit") -} diff --git a/kubectl-pg/cmd/scale.go b/kubectl-pg/cmd/scale.go deleted file mode 100644 index 0a7bdc60f..000000000 --- a/kubectl-pg/cmd/scale.go +++ /dev/null @@ -1,194 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -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) -} diff --git a/kubectl-pg/cmd/update.go b/kubectl-pg/cmd/update.go deleted file mode 100644 index eb9259586..000000000 --- a/kubectl-pg/cmd/update.go +++ /dev/null @@ -1,96 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -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) -} diff --git a/kubectl-pg/cmd/util.go b/kubectl-pg/cmd/util.go deleted file mode 100644 index fa0eb6d42..000000000 --- a/kubectl-pg/cmd/util.go +++ /dev/null @@ -1,172 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -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 -} diff --git a/kubectl-pg/cmd/version.go b/kubectl-pg/cmd/version.go deleted file mode 100644 index 23cc55422..000000000 --- a/kubectl-pg/cmd/version.go +++ /dev/null @@ -1,80 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -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.") -} diff --git a/kubectl-pg/go.mod b/kubectl-pg/go.mod deleted file mode 100644 index 7f80cbfd7..000000000 --- a/kubectl-pg/go.mod +++ /dev/null @@ -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 -) diff --git a/kubectl-pg/go.sum b/kubectl-pg/go.sum deleted file mode 100644 index 488d24edc..000000000 --- a/kubectl-pg/go.sum +++ /dev/null @@ -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= diff --git a/kubectl-pg/main.go b/kubectl-pg/main.go deleted file mode 100644 index bfcd5eb29..000000000 --- a/kubectl-pg/main.go +++ /dev/null @@ -1,31 +0,0 @@ -/* -Copyright © 2019 Vineeth Pothulapati - -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() -} diff --git a/manifests/postgresql.crd.yaml b/manifests/postgresql.crd.yaml index b9652ef22..72d7153ba 100644 --- a/manifests/postgresql.crd.yaml +++ b/manifests/postgresql.crd.yaml @@ -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 diff --git a/pkg/apis/acid.zalan.do/v1/crds.go b/pkg/apis/acid.zalan.do/v1/crds.go index addd1b4f6..14bfccb58 100644 --- a/pkg/apis/acid.zalan.do/v1/crds.go +++ b/pkg/apis/acid.zalan.do/v1/crds.go @@ -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{ diff --git a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go index e37f091d0..60cab70ea 100644 --- a/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go +++ b/pkg/apis/acid.zalan.do/v1/operator_configuration_type.go @@ -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 diff --git a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml index b9652ef22..72d7153ba 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml +++ b/pkg/apis/acid.zalan.do/v1/postgresql.crd.yaml @@ -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 diff --git a/pkg/apis/acid.zalan.do/v1/postgresql_type.go b/pkg/apis/acid.zalan.do/v1/postgresql_type.go index d67f23741..81efa42cc 100644 --- a/pkg/apis/acid.zalan.do/v1/postgresql_type.go +++ b/pkg/apis/acid.zalan.do/v1/postgresql_type.go @@ -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"` diff --git a/pkg/apis/acid.zalan.do/v1/util_test.go b/pkg/apis/acid.zalan.do/v1/util_test.go index fcc5ae5fd..857622581 100644 --- a/pkg/apis/acid.zalan.do/v1/util_test.go +++ b/pkg/apis/acid.zalan.do/v1/util_test.go @@ -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) + } + } +} diff --git a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go index 2f68755db..9ca9a54f1 100644 --- a/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go +++ b/pkg/apis/acid.zalan.do/v1/zz_generated.deepcopy.go @@ -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) diff --git a/pkg/cluster/cluster.go b/pkg/cluster/cluster.go index b7a7b8e56..6d9e6150a 100644 --- a/pkg/cluster/cluster.go +++ b/pkg/cluster/cluster.go @@ -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) diff --git a/pkg/cluster/cluster_test.go b/pkg/cluster/cluster_test.go index 84d75cb0a..5fdf1a220 100644 --- a/pkg/cluster/cluster_test.go +++ b/pkg/cluster/cluster_test.go @@ -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 { diff --git a/pkg/cluster/connection_pooler.go b/pkg/cluster/connection_pooler.go index 292c126c3..f92acfdd8 100644 --- a/pkg/cluster/connection_pooler.go +++ b/pkg/cluster/connection_pooler.go @@ -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 { diff --git a/pkg/cluster/connection_pooler_test.go b/pkg/cluster/connection_pooler_test.go index 1e4f96ebf..9caf5aa60 100644 --- a/pkg/cluster/connection_pooler_test.go +++ b/pkg/cluster/connection_pooler_test.go @@ -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) + } + } + } +} diff --git a/pkg/cluster/k8sres.go b/pkg/cluster/k8sres.go index 866eeb752..302797dc4 100644 --- a/pkg/cluster/k8sres.go +++ b/pkg/cluster/k8sres.go @@ -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{ diff --git a/pkg/cluster/k8sres_test.go b/pkg/cluster/k8sres_test.go index 2010de067..bf21b8645 100644 --- a/pkg/cluster/k8sres_test.go +++ b/pkg/cluster/k8sres_test.go @@ -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"}, }, } diff --git a/pkg/cluster/resources.go b/pkg/cluster/resources.go index ed3eb3d75..1fdad3e5e 100644 --- a/pkg/cluster/resources.go +++ b/pkg/cluster/resources.go @@ -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 { diff --git a/pkg/cluster/sync.go b/pkg/cluster/sync.go index 7c478477a..e15b5fedc 100644 --- a/pkg/cluster/sync.go +++ b/pkg/cluster/sync.go @@ -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 { diff --git a/pkg/cluster/util.go b/pkg/cluster/util.go index 9c830129d..cbcccd16e 100644 --- a/pkg/cluster/util.go +++ b/pkg/cluster/util.go @@ -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)) diff --git a/pkg/controller/operator_config.go b/pkg/controller/operator_config.go index 65e3161e7..a85b418e7 100644 --- a/pkg/controller/operator_config.go +++ b/pkg/controller/operator_config.go @@ -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 diff --git a/pkg/util/config/config.go b/pkg/util/config/config.go index cad9ebb2c..35537cf44 100644 --- a/pkg/util/config/config.go +++ b/pkg/util/config/config.go @@ -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"`