Skip to content

Commit 7b539e0

Browse files
authored
Pgadmin integration with operator
implements pgadmin integration with the operator New commands added: pgo create pgadmin... pgo delete pgadmin... pgo show pgadmin... PgAdmin is managed as a separate deployment and servcie as part of the identified cluster(s) The operator ensures users are added and removed from pgadmin when added and removed to the PgCluster and synchronizes password updates such that the preconfigured database connections stay up-to-date. If the pgadmin deployment is added after the reation of users not managed in kubernetes can be synced to pgadmin by updating their password. Issue: [ch4030]
1 parent 2a6cda9 commit 7b539e0

File tree

44 files changed

+3126
-25
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+3126
-25
lines changed

Gopkg.lock

Lines changed: 3 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apis/crunchydata.com/v1/task.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ const PgtaskUpgrade = "clusterupgrade"
3232
const PgtaskUpgradeCreated = "cluster upgrade - task created"
3333
const PgtaskUpgradeInProgress = "cluster upgrade - in progress"
3434

35+
const PgtaskPgAdminAdd = "add-pgadmin"
36+
const PgtaskPgAdminDelete = "delete-pgadmin"
37+
3538
const PgtaskWorkflow = "workflow"
3639
const PgtaskWorkflowCloneType = "cloneworkflow"
3740
const PgtaskWorkflowCreateClusterType = "createcluster"

apiserver/perms.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ const (
4343
CREATE_FAILOVER_PERM = "CreateFailover"
4444
CREATE_INGEST_PERM = "CreateIngest"
4545
CREATE_NAMESPACE_PERM = "CreateNamespace"
46+
CREATE_PGADMIN_PERM = "CreatePgAdmin"
4647
CREATE_PGBOUNCER_PERM = "CreatePgbouncer"
4748
CREATE_PGOUSER_PERM = "CreatePgouser"
4849
CREATE_PGOROLE_PERM = "CreatePgorole"
@@ -59,6 +60,7 @@ const (
5960
DELETE_CLUSTER_PERM = "DeleteCluster"
6061
DELETE_INGEST_PERM = "DeleteIngest"
6162
DELETE_NAMESPACE_PERM = "DeleteNamespace"
63+
DELETE_PGADMIN_PERM = "DeletePgAdmin"
6264
DELETE_PGBOUNCER_PERM = "DeletePgbouncer"
6365
DELETE_PGOROLE_PERM = "DeletePgorole"
6466
DELETE_PGOUSER_PERM = "DeletePgouser"
@@ -72,6 +74,7 @@ const (
7274
SHOW_CONFIG_PERM = "ShowConfig"
7375
SHOW_INGEST_PERM = "ShowIngest"
7476
SHOW_NAMESPACE_PERM = "ShowNamespace"
77+
SHOW_PGADMIN_PERM = "ShowPgAdmin"
7578
SHOW_PGBOUNCER_PERM = "ShowPgBouncer"
7679
SHOW_PGOROLE_PERM = "ShowPgorole"
7780
SHOW_PGOUSER_PERM = "ShowPgouser"
@@ -127,6 +130,7 @@ func InitializePerms() {
127130
CREATE_FAILOVER_PERM: "yes",
128131
CREATE_INGEST_PERM: "yes",
129132
CREATE_NAMESPACE_PERM: "yes",
133+
CREATE_PGADMIN_PERM: "yes",
130134
CREATE_PGBOUNCER_PERM: "yes",
131135
CREATE_PGOROLE_PERM: "yes",
132136
CREATE_PGOUSER_PERM: "yes",
@@ -143,6 +147,7 @@ func InitializePerms() {
143147
DELETE_CLUSTER_PERM: "yes",
144148
DELETE_INGEST_PERM: "yes",
145149
DELETE_NAMESPACE_PERM: "yes",
150+
DELETE_PGADMIN_PERM: "yes",
146151
DELETE_PGBOUNCER_PERM: "yes",
147152
DELETE_PGOROLE_PERM: "yes",
148153
DELETE_PGOUSER_PERM: "yes",
@@ -156,6 +161,7 @@ func InitializePerms() {
156161
SHOW_CONFIG_PERM: "yes",
157162
SHOW_INGEST_PERM: "yes",
158163
SHOW_NAMESPACE_PERM: "yes",
164+
SHOW_PGADMIN_PERM: "yes",
159165
SHOW_PGBOUNCER_PERM: "yes",
160166
SHOW_PGOROLE_PERM: "yes",
161167
SHOW_PGOUSER_PERM: "yes",
Lines changed: 299 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,299 @@
1+
package pgadminservice
2+
3+
/*
4+
Copyright 2020 Crunchy Data Solutions, Inc.
5+
Licensed under the Apache License, Version 2.0 (the "License");
6+
you may not use this file except in compliance with the License.
7+
You may obtain a copy of the License at
8+
9+
http://www.apache.org/licenses/LICENSE-2.0
10+
11+
Unless required by applicable law or agreed to in writing, software
12+
distributed under the License is distributed on an "AS IS" BASIS,
13+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
See the License for the specific language governing permissions and
15+
limitations under the License.
16+
*/
17+
18+
import (
19+
"fmt"
20+
21+
crv1 "github.com/crunchydata/postgres-operator/apis/crunchydata.com/v1"
22+
"github.com/crunchydata/postgres-operator/apiserver"
23+
msgs "github.com/crunchydata/postgres-operator/apiservermsgs"
24+
"github.com/crunchydata/postgres-operator/config"
25+
"github.com/crunchydata/postgres-operator/internal/pgadmin"
26+
"github.com/crunchydata/postgres-operator/kubeapi"
27+
28+
log "github.com/sirupsen/logrus"
29+
meta_v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+
)
31+
32+
const pgAdminServiceSuffix = "-pgadmin"
33+
34+
// CreatePgAdmin ...
35+
// pgo create pgadmin mycluster
36+
// pgo create pgadmin --selector=name=mycluster
37+
func CreatePgAdmin(request *msgs.CreatePgAdminRequest, ns, pgouser string) msgs.CreatePgAdminResponse {
38+
var err error
39+
resp := msgs.CreatePgAdminResponse{
40+
Status: msgs.Status{Code: msgs.Ok},
41+
Results: []string{},
42+
}
43+
44+
log.Debugf("createPgAdmin selector is [%s]", request.Selector)
45+
46+
// try to get the list of clusters. if there is an error, put it into the
47+
// status and return
48+
clusterList, err := getClusterList(request.Namespace, request.Args, request.Selector)
49+
if err != nil {
50+
resp.SetError(err.Error())
51+
return resp
52+
}
53+
54+
for _, cluster := range clusterList.Items {
55+
// check if the current cluster is not upgraded to the deployed
56+
// Operator version. If not, do not allow the command to complete
57+
if cluster.Annotations[config.ANNOTATION_IS_UPGRADED] == config.ANNOTATIONS_FALSE {
58+
resp.Status.Code = msgs.Error
59+
resp.Status.Msg = cluster.Name + msgs.UpgradeError
60+
return resp
61+
}
62+
63+
log.Debugf("adding pgAdmin to cluster [%s]", cluster.Name)
64+
65+
// generate the pgtask, starting with spec
66+
spec := crv1.PgtaskSpec{
67+
Namespace: cluster.Namespace,
68+
Name: fmt.Sprintf("%s-%s", config.LABEL_PGADMIN_TASK_ADD, cluster.Name),
69+
TaskType: crv1.PgtaskPgAdminAdd,
70+
StorageSpec: cluster.Spec.PrimaryStorage,
71+
Parameters: map[string]string{
72+
config.LABEL_PGADMIN_TASK_CLUSTER: cluster.Name,
73+
},
74+
}
75+
76+
task := &crv1.Pgtask{
77+
ObjectMeta: meta_v1.ObjectMeta{
78+
Name: spec.Name,
79+
Labels: map[string]string{
80+
config.LABEL_PG_CLUSTER: cluster.Name,
81+
config.LABEL_PGADMIN_TASK_ADD: "true",
82+
config.LABEL_PGOUSER: pgouser,
83+
},
84+
},
85+
Spec: spec,
86+
}
87+
88+
if err := kubeapi.Createpgtask(apiserver.RESTClient, task, cluster.Namespace); err != nil {
89+
log.Error(err)
90+
resp.SetError("error creating tasks for one or more clusters")
91+
resp.Results = append(resp.Results, fmt.Sprintf("%s: error - %s", cluster.Name, err.Error()))
92+
continue
93+
} else {
94+
resp.Results = append(resp.Results, fmt.Sprintf("%s pgAdmin addition scheduled", cluster.Name))
95+
}
96+
}
97+
98+
return resp
99+
}
100+
101+
// DeletePgAdmin ...
102+
// pgo delete pgadmin mycluster
103+
// pgo delete pgadmin --selector=name=mycluster
104+
func DeletePgAdmin(request *msgs.DeletePgAdminRequest, ns string) msgs.DeletePgAdminResponse {
105+
var err error
106+
resp := msgs.DeletePgAdminResponse{
107+
Status: msgs.Status{Code: msgs.Ok},
108+
Results: []string{},
109+
}
110+
111+
log.Debugf("deletePgAdmin selector is [%s]", request.Selector)
112+
113+
// try to get the list of clusters. if there is an error, put it into the
114+
// status and return
115+
clusterList, err := getClusterList(request.Namespace, request.Args, request.Selector)
116+
if err != nil {
117+
resp.SetError(err.Error())
118+
return resp
119+
}
120+
121+
for _, cluster := range clusterList.Items {
122+
// check if the current cluster is not upgraded to the deployed
123+
// Operator version. If not, do not allow the command to complete
124+
if cluster.Annotations[config.ANNOTATION_IS_UPGRADED] == config.ANNOTATIONS_FALSE {
125+
resp.Status.Code = msgs.Error
126+
resp.Status.Msg = cluster.Name + msgs.UpgradeError
127+
return resp
128+
}
129+
130+
log.Debugf("deleting pgAdmin from cluster [%s]", cluster.Name)
131+
132+
// generate the pgtask, starting with spec
133+
spec := crv1.PgtaskSpec{
134+
Namespace: cluster.Namespace,
135+
Name: config.LABEL_PGADMIN_TASK_DELETE + "-" + cluster.Name,
136+
TaskType: crv1.PgtaskPgAdminDelete,
137+
Parameters: map[string]string{
138+
config.LABEL_PGADMIN_TASK_CLUSTER: cluster.Name,
139+
},
140+
}
141+
142+
task := &crv1.Pgtask{
143+
ObjectMeta: meta_v1.ObjectMeta{
144+
Name: spec.Name,
145+
Labels: map[string]string{
146+
config.LABEL_PG_CLUSTER: cluster.Name,
147+
config.LABEL_PGADMIN_TASK_DELETE: "true",
148+
},
149+
},
150+
Spec: spec,
151+
}
152+
153+
if err := kubeapi.Createpgtask(apiserver.RESTClient, task, cluster.Namespace); err != nil {
154+
log.Error(err)
155+
resp.SetError("error creating tasks for one or more clusters")
156+
resp.Results = append(resp.Results, fmt.Sprintf("%s: error - %s", cluster.Name, err.Error()))
157+
return resp
158+
} else {
159+
resp.Results = append(resp.Results, cluster.Name+" pgAdmin delete scheduled")
160+
}
161+
162+
}
163+
164+
return resp
165+
}
166+
167+
// ShowPgAdmin gets information about a PostgreSQL cluster's pgAdmin
168+
// deployment
169+
//
170+
// pgo show pgadmin
171+
// pgo show pgadmin --selector
172+
func ShowPgAdmin(request *msgs.ShowPgAdminRequest, namespace string) msgs.ShowPgAdminResponse {
173+
log.Debugf("show pgAdmin called, cluster [%v], selector [%s]", request.ClusterNames, request.Selector)
174+
175+
response := msgs.ShowPgAdminResponse{
176+
Results: []msgs.ShowPgAdminDetail{},
177+
Status: msgs.Status{Code: msgs.Ok},
178+
}
179+
180+
// try to get the list of clusters. if there is an error, put it into the
181+
// status and return
182+
clusterList, err := getClusterList(request.Namespace, request.ClusterNames, request.Selector)
183+
184+
if err != nil {
185+
response.SetError(err.Error())
186+
return response
187+
}
188+
189+
// iterate through the list of clusters to get the relevant pgAdmin
190+
// information about them
191+
for _, cluster := range clusterList.Items {
192+
result := msgs.ShowPgAdminDetail{
193+
ClusterName: cluster.Spec.Name,
194+
HasPgAdmin: true,
195+
}
196+
197+
// first, check if the cluster has the pgAdmin label. If it does not, we
198+
// add it to the list and keep iterating
199+
clusterLabels := cluster.GetLabels()
200+
201+
if clusterLabels[config.LABEL_PGADMIN] != "true" {
202+
result.HasPgAdmin = false
203+
response.Results = append(response.Results, result)
204+
continue
205+
}
206+
207+
// This takes advantage of pgadmin deployment and pgadmin service
208+
// sharing a name that is clustername + pgAdminServiceSuffix
209+
service, _, err := kubeapi.GetService(
210+
apiserver.Clientset,
211+
cluster.Name+pgAdminServiceSuffix,
212+
cluster.Namespace)
213+
if err != nil {
214+
response.SetError(err.Error())
215+
return response
216+
}
217+
218+
result.ServiceClusterIP = service.Spec.ClusterIP
219+
result.ServiceName = service.Name
220+
if len(service.Spec.ExternalIPs) > 0 {
221+
result.ServiceExternalIP = service.Spec.ExternalIPs[0]
222+
}
223+
if len(service.Status.LoadBalancer.Ingress) > 0 {
224+
result.ServiceExternalIP = service.Status.LoadBalancer.Ingress[0].IP
225+
}
226+
227+
// In the future, construct results to contain individual error stati
228+
// for now log and return empty content if encountered
229+
qr, err := pgadmin.GetPgAdminQueryRunner(apiserver.Clientset, apiserver.RESTConfig, &cluster)
230+
if err != nil {
231+
log.Error(err)
232+
continue
233+
} else if qr != nil {
234+
names, err := pgadmin.GetUsernames(qr)
235+
if err != nil {
236+
log.Error(err)
237+
continue
238+
}
239+
result.Users = names
240+
}
241+
242+
// append the result to the list
243+
response.Results = append(response.Results, result)
244+
}
245+
246+
return response
247+
}
248+
249+
// getClusterList tries to return a list of clusters based on either having an
250+
// argument list of cluster names, or a Kubernetes selector
251+
func getClusterList(namespace string, clusterNames []string, selector string) (crv1.PgclusterList, error) {
252+
clusterList := crv1.PgclusterList{}
253+
254+
// see if there are any values in the cluster name list or in the selector
255+
// if nothing exists, return an error
256+
if len(clusterNames) == 0 && selector == "" {
257+
err := fmt.Errorf("either a list of cluster names or a selector needs to be supplied for this comment")
258+
return clusterList, err
259+
}
260+
261+
// try to build the cluster list based on either the selector or the list
262+
// of arguments...or both. First, start with the selector
263+
if selector != "" {
264+
err := kubeapi.GetpgclustersBySelector(apiserver.RESTClient, &clusterList,
265+
selector, namespace)
266+
267+
// if there is an error, return here with an empty cluster list
268+
if err != nil {
269+
return crv1.PgclusterList{}, err
270+
}
271+
}
272+
273+
// now try to get clusters based specific cluster names
274+
for _, clusterName := range clusterNames {
275+
cluster := crv1.Pgcluster{}
276+
277+
_, err := kubeapi.Getpgcluster(apiserver.RESTClient, &cluster,
278+
clusterName, namespace)
279+
280+
// if there is an error, capture it here and return here with an empty list
281+
if err != nil {
282+
return crv1.PgclusterList{}, err
283+
}
284+
285+
// if successful, append to the cluster list
286+
clusterList.Items = append(clusterList.Items, cluster)
287+
}
288+
289+
log.Debugf("clusters founds: [%d]", len(clusterList.Items))
290+
291+
// if after all this, there are no clusters found, return an error
292+
if len(clusterList.Items) == 0 {
293+
err := fmt.Errorf("no clusters found")
294+
return clusterList, err
295+
}
296+
297+
// all set! return the cluster list with error
298+
return clusterList, nil
299+
}

0 commit comments

Comments
 (0)