-
Notifications
You must be signed in to change notification settings - Fork 592
/
util.go
298 lines (250 loc) · 9.72 KB
/
util.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
package util
/*
Copyright 2017 - 2020 Crunchy Data Solutions, Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import (
"encoding/json"
"errors"
"fmt"
"math/rand"
"regexp"
"strings"
"time"
"github.com/crunchydata/postgres-operator/internal/config"
crv1 "github.com/crunchydata/postgres-operator/pkg/apis/crunchydata.com/v1"
pgo "github.com/crunchydata/postgres-operator/pkg/generated/clientset/versioned"
jsonpatch "github.com/evanphx/json-patch"
log "github.com/sirupsen/logrus"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
)
const letterBytes = "abcdefghijklmnopqrstuvwxyz"
// JSONPatchOperation represents the structure for a JSON patch operation
type JSONPatchOperation struct {
Op string `json:"op"`
Path string `json:"path"`
Value interface{} `json:"value"`
}
// gisImageTagRegex is a regular expression designed to match the standard image tag for
// the crunchy-postgres-gis-ha container
var gisImageTagRegex = regexp.MustCompile(`(.+-[\d|\.]+)-[\d|\.]+?(-[\d|\.]+.*)`)
func init() {
rand.Seed(time.Now().UnixNano())
}
// ThingSpec is a json patch structure
type ThingSpec struct {
Op string `json:"op"`
Path string `json:"path"`
Value string `json:"value"`
}
// Patch will patch a particular resource
func Patch(restclient rest.Interface, path string, value string, resource string, name string, namespace string) error {
things := make([]ThingSpec, 1)
things[0].Op = "replace"
things[0].Path = path
things[0].Value = value
patchBytes, err4 := json.Marshal(things)
if err4 != nil {
log.Error("error in converting patch " + err4.Error())
}
log.Debug(string(patchBytes))
_, err6 := restclient.Patch(types.JSONPatchType).
Namespace(namespace).
Resource(resource).
Name(name).
Body(patchBytes).
Do().
Get()
return err6
}
// GetLabels ...
func GetLabels(name, clustername string, replica bool) string {
var output string
if replica {
output += fmt.Sprintf("\"primary\": \"%s\",\n", "false")
}
output += fmt.Sprintf("\"name\": \"%s\",\n", name)
output += fmt.Sprintf("\"pg-cluster\": \"%s\"\n", clustername)
return output
}
//CurrentPrimaryUpdate prepares the needed data structures with the correct current primary value
//before passing them along to be patched into the current pgcluster CRD's annotations
func CurrentPrimaryUpdate(clientset pgo.Interface, cluster *crv1.Pgcluster, currentPrimary, namespace string) error {
//create a new map
metaLabels := make(map[string]string)
//copy the relevant values into the new map
for k, v := range cluster.ObjectMeta.Labels {
metaLabels[k] = v
}
//update this map with the new deployment label
metaLabels[config.LABEL_DEPLOYMENT_NAME] = currentPrimary
//Update CRD with the current primary name and the new deployment to point to after the failover
if err := PatchClusterCRD(clientset, metaLabels, cluster, currentPrimary, namespace); err != nil {
log.Errorf("failoverlogic: could not patch pgcluster %s with the current primary", currentPrimary)
}
return nil
}
// PatchClusterCRD patches the pgcluster CRD with any updated labels, or an updated current
// primary annotation value. As this uses a JSON merge patch, it will only updates those
// values that are different between the old and new CRD values.
func PatchClusterCRD(clientset pgo.Interface, labelMap map[string]string, oldCrd *crv1.Pgcluster, currentPrimary, namespace string) error {
oldData, err := json.Marshal(oldCrd)
if err != nil {
return err
}
// if there are no meta object lables on the current CRD, create a new map to hold them
if oldCrd.ObjectMeta.Labels == nil {
oldCrd.ObjectMeta.Labels = make(map[string]string)
}
// if there are not any annotation on the current CRD, create a new map to hold them
if oldCrd.Annotations == nil {
oldCrd.Annotations = make(map[string]string)
}
// update our pgcluster annotation with the correct current primary value
oldCrd.Annotations[config.ANNOTATION_CURRENT_PRIMARY] = currentPrimary
oldCrd.Annotations[config.ANNOTATION_PRIMARY_DEPLOYMENT] = currentPrimary
// update the stored primary storage value to match the current primary and deployment name
oldCrd.Spec.PrimaryStorage.Name = currentPrimary
for k, v := range labelMap {
if len(validation.IsQualifiedName(k)) == 0 && len(validation.IsValidLabelValue(v)) == 0 {
oldCrd.ObjectMeta.Labels[k] = v
} else {
log.Debugf("user label %s:%s does not meet Kubernetes label requirements and will not be used to label "+
"pgcluster %s", k, v, oldCrd.Spec.Name)
}
}
var newData, patchBytes []byte
newData, err = json.Marshal(oldCrd)
if err != nil {
return err
}
patchBytes, err = jsonpatch.CreateMergePatch(oldData, newData)
if err != nil {
return err
}
log.Debug(string(patchBytes))
_, err6 := clientset.CrunchydataV1().Pgclusters(namespace).Patch(oldCrd.Spec.Name, types.MergePatchType, patchBytes)
return err6
}
// GetValueOrDefault checks whether the first value given is set. If it is,
// that value is returned. If not, the second, default value is returned instead
func GetValueOrDefault(value, defaultValue string) string {
if value != "" {
return value
}
return defaultValue
}
// GetSecretPassword ...
func GetSecretPassword(clientset kubernetes.Interface, db, suffix, Namespace string) (string, error) {
var err error
selector := "pg-cluster=" + db
secrets, err := clientset.
CoreV1().Secrets(Namespace).
List(metav1.ListOptions{LabelSelector: selector})
if err != nil {
return "", err
}
log.Debugf("secrets for %s", db)
secretName := db + suffix
for _, s := range secrets.Items {
log.Debugf("secret : %s", s.ObjectMeta.Name)
if s.ObjectMeta.Name == secretName {
log.Debug("pgprimary password found")
return string(s.Data["password"][:]), err
}
}
log.Error("primary secret not found for " + db)
return "", errors.New("primary secret not found for " + db)
}
// GetStandardImageTag takes the current image name and the image tag value
// stored in the pgcluster CRD and, if the image being used is the
// crunchy-postgres-gis-ha container with the corresponding tag, it returns
// the tag without the addition of the GIS version. This tag value can then
// be used when provisioning containers using the standard containers tag.
func GetStandardImageTag(imageName, imageTag string) string {
if imageName == "crunchy-postgres-gis-ha" && strings.Count(imageTag, "-") > 2 {
return gisImageTagRegex.ReplaceAllString(imageTag, "$1$2")
}
return imageTag
}
// RandStringBytesRmndr ...
func RandStringBytesRmndr(n int) string {
b := make([]byte, n)
for i := range b {
b[i] = letterBytes[rand.Int63()%int64(len(letterBytes))]
}
return string(b)
}
// IsStringOneOf tests to see string testVal is included in the list
// of strings provided using acceptedVals
func IsStringOneOf(testVal string, acceptedVals ...string) bool {
isOneOf := false
for _, val := range acceptedVals {
if testVal == val {
isOneOf = true
break
}
}
return isOneOf
}
// SQLQuoteIdentifier quotes an "identifier" (e.g. a table or a column name) to
// be used as part of an SQL statement.
//
// Any double quotes in name will be escaped. The quoted identifier will be
// case sensitive when used in a query. If the input string contains a zero
// byte, the result will be truncated immediately before it.
//
// Implementation borrowed from lib/pq: https://github.com/lib/pq which is
// licensed under the MIT License
func SQLQuoteIdentifier(identifier string) string {
end := strings.IndexRune(identifier, 0)
if end > -1 {
identifier = identifier[:end]
}
return `"` + strings.Replace(identifier, `"`, `""`, -1) + `"`
}
// SQLQuoteLiteral quotes a 'literal' (e.g. a parameter, often used to pass literal
// to DDL and other statements that do not accept parameters) to be used as part
// of an SQL statement.
//
// Any single quotes in name will be escaped. Any backslashes (i.e. "\") will be
// replaced by two backslashes (i.e. "\\") and the C-style escape identifier
// that PostgreSQL provides ('E') will be prepended to the string.
//
// Implementation borrowed from lib/pq: https://github.com/lib/pq which is
// licensed under the MIT License. Curiously, @jkatz and @cbandy were the ones
// who worked on the patch to add this, prior to being at Crunchy Data
func SQLQuoteLiteral(literal string) string {
// This follows the PostgreSQL internal algorithm for handling quoted literals
// from libpq, which can be found in the "PQEscapeStringInternal" function,
// which is found in the libpq/fe-exec.c source file:
// https://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/interfaces/libpq/fe-exec.c
//
// substitute any single-quotes (') with two single-quotes ('')
literal = strings.Replace(literal, `'`, `''`, -1)
// determine if the string has any backslashes (\) in it.
// if it does, replace any backslashes (\) with two backslashes (\\)
// then, we need to wrap the entire string with a PostgreSQL
// C-style escape. Per how "PQEscapeStringInternal" handles this case, we
// also add a space before the "E"
if strings.Contains(literal, `\`) {
literal = strings.Replace(literal, `\`, `\\`, -1)
literal = ` E'` + literal + `'`
} else {
// otherwise, we can just wrap the literal with a pair of single quotes
literal = `'` + literal + `'`
}
return literal
}