forked from adampetrovic/home-ops
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathupgrade-pg.py
179 lines (151 loc) · 6.05 KB
/
upgrade-pg.py
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
#!/usr/bin/env python3
#
# Helper script to do major version upgrades of a cloudnative-pg based
# PostgreSQL cluster.
#
# Usage: python3 upgrade-pg.py <cluster-name> <new-pg-version>
#
# Example: python3 upgrade-pg.py my-backend-db 16.0
#
# Make sure to include major and minor version in the pg version.
# Error handling is quite basic, when you're asked to check if the
# new database is ok, do so by checking if the relevant data is there.
# When in doubt, don't continue, as you could suffer data loss (especially
# if you have no backups configured).
#
# This script does the following:
# 1. Create a temporary cluster with the new PostgreSQL version
# with data from the source cluster.
# 2. After confirmation, delete the original cluster (so we can replace it).
# 3. Create a new source cluster from the temporary cluster
# 4. After confirmation, delete the temporary cluster.
#
# Backups are disabled for the new cluster, as the new PostgreSQL version
# probably needs a new storage location, because the file formats may not
# be compatible.
#
# You need to make sure any subsequent infrastructure changes use the upgraded
# PostgreSQL version, and configure backup. Then take a new base backup.
#
# Note that the final cluster manifest needs some cleaning up yet.
#
# _NOTE:_ tested only a little, take care!
#
#
# Changelog:
# 20230918 initial version
#
import json
from subprocess import run
from random import randint
from time import sleep
def kubectl_get(kind, name):
'''Return Kubernetes manifest for cluster'''
r = run(['kubectl', 'get', kind, name, '-o', 'json', '-n', 'database'], check=True, capture_output=True)
return json.loads(r.stdout)
def kubectl_create(manifest):
'''Create Kubernetes resource from manifest'''
run(['kubectl', 'create', '-f', '-', '-n', 'database'], check=True, input=manifest, text=True, capture_output=True)
def kubectl_delete(kind, name):
'''Delete Kubernets resource'''
run(['kubectl', 'delete', kind, name, '-n', 'database'], check=True)
def kubectl_wait_cluster_ready(name, reverse=False):
'''Wait until the cloudnative-pg cluster is ready'''
ready_instances = 0
while (not reverse and ready_instances == 0) or (reverse and ready_instances > 0):
sleep(5)
r = run(['kubectl', 'get', 'cluster', name, '-o', 'go-template={{ .status.readyInstances }}', '-n', 'database'], check=True, capture_output=True)
s = r.stdout.decode().strip()
if s and s != '<no value>': ready_instances = int(s)
class ClusterTemplate:
def __init__(self, manifest):
self.manifest = manifest
@property
def name(self):
return self.manifest.get('metadata', {}).get('name')
@name.setter
def name(self, value):
self.manifest.get('metadata', {})['name'] = value
@property
def spec(self):
return self.manifest.get('spec', {})
@property
def database(self):
return self.spec['bootstrap']['initdb']['database']
@property
def owner(self):
return self.spec['bootstrap']['initdb']['owner']
@property
def secret(self):
return self.spec['bootstrap']['initdb']['secret']['name']
@property
def imageName(self):
return self.spec.get('imageName')
@imageName.setter
def imageName(self, value):
self.spec['imageName'] = value
@property
def imageVersion(self):
return self.imageName.split(':', 1)[-1]
@imageVersion.setter
def imageVersion(self, value):
name, version = self.imageName.split(':', 1)
self.imageName = name + ':' + value
def delete_backup(self):
if 'backup' in self.spec:
self.spec.pop('backup')
def import_from(self, name, database, user, secret):
cluster_name = database + '-' + str(randint(0, 999999))
# add external cluster for specified database
if not self.spec.get('externalClusters'): self.spec['externalClusters'] = []
self.spec['externalClusters'].append({
'name': cluster_name,
'connectionParameters': {
'host': name + '-r',
'user': user,
'dbname': database
},
'password': {
'name': secret,
'key': 'password'
}
})
# indicate to import from specified database
self.spec['bootstrap']['initdb']['import'] = {
'type': 'microservice',
'databases': [database],
'source': { 'externalCluster': cluster_name }
}
if __name__ == '__main__':
import re
import sys
name, new_version = sys.argv[1:3]
new_name = name + '-' + re.sub(r'\.\d+$', '', new_version)
print('Retrieving existing cluster template')
template = ClusterTemplate(kubectl_get('cluster', name))
template.name = new_name
template.imageVersion = new_version
template.delete_backup()
template.import_from(name, template.database, template.owner, template.secret)
print('Creating temporary upgraded database')
kubectl_create(json.dumps(template.manifest))
kubectl_wait_cluster_ready(new_name)
s = input('Temporary upgraded database ready. Is it in order, continue and delete the source database? [y/n]')
if s.lower() != 'y': sys.exit()
print('Deleting source database (as to replace it)')
kubectl_delete('cluster', name)
kubectl_wait_cluster_ready(name, reverse=True)
print('Re-creating source database')
template.name = name
template.import_from(new_name, template.database, template.owner, template.secret)
kubectl_create(json.dumps(template.manifest))
kubectl_wait_cluster_ready(name)
s = input('Final upgraded database ready. Is it in order, continue and delete the temporary database? [y/n]')
if s.lower() != 'y': sys.exit()
print('Deleting temporary upgraded database')
kubectl_delete('cluster', new_name)
print('Done!')
print('Remember to update your deployment configuration:')
print('- set PostgreSQL version to ' + new_version)
print('- use a new backup destination (als WALs will not be compatible)')
print('- create a new base backup')