Skip to content

Commit 750063b

Browse files
authored
Merge pull request #1533 from bcgov/dev
Dev
2 parents 7424dcf + 1190915 commit 750063b

11 files changed

Lines changed: 472 additions & 54 deletions

File tree

applications/Unity.AutoUI/package-lock.json

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

applications/Unity.GrantManager/src/Unity.GrantManager.Application/HealthChecks/BackgroundWorkers/DataHealthCheckWorker.cs

Lines changed: 52 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -33,66 +33,70 @@ public DataHealthCheckWorker(ICurrentTenant currentTenant,
3333
_emailNotificationService = emailNotificationService;
3434
_paymentRequestsRepository = paymentRequestsRepository;
3535

36-
string? envInfo = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
3736

38-
if (string.Equals(envInfo, "Production", StringComparison.OrdinalIgnoreCase))
39-
{
40-
string cronExpression = SettingDefinitions.GetSettingsValue(settingManager, SettingsConstants.BackgroundJobs.DataHealthCheckMonitor_Expression);
41-
42-
JobDetail = JobBuilder
43-
.Create<DataHealthCheckWorker>()
44-
.WithIdentity(nameof(DataHealthCheckWorker))
45-
.Build();
46-
47-
Trigger = TriggerBuilder
48-
.Create()
49-
.WithIdentity(nameof(DataHealthCheckWorker))
50-
.WithSchedule(CronScheduleBuilder.CronSchedule(cronExpression)
51-
.WithMisfireHandlingInstructionIgnoreMisfires())
52-
.Build();
53-
}
37+
string cronExpression = SettingDefinitions.GetSettingsValue(settingManager, SettingsConstants.BackgroundJobs.DataHealthCheckMonitor_Expression);
38+
39+
JobDetail = JobBuilder
40+
.Create<DataHealthCheckWorker>()
41+
.WithIdentity(nameof(DataHealthCheckWorker))
42+
.Build();
43+
44+
Trigger = TriggerBuilder
45+
.Create()
46+
.WithIdentity(nameof(DataHealthCheckWorker))
47+
.WithSchedule(CronScheduleBuilder.CronSchedule(cronExpression)
48+
.WithMisfireHandlingInstructionIgnoreMisfires())
49+
.Build();
5450
}
5551

5652
public override async Task Execute(IJobExecutionContext context)
5753
{
5854
Logger.LogInformation("Executing DataHealthCheckWorker...");
59-
var tenants = await _tenantRepository.GetListAsync();
60-
bool sendEmail = false;
61-
var emailBodyBuilder = new System.Text.StringBuilder();
62-
63-
foreach (var tenant in tenants)
55+
56+
57+
string? envInfo = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
58+
59+
if (string.Equals(envInfo, "Production", StringComparison.OrdinalIgnoreCase))
6460
{
65-
using (_currentTenant.Change(tenant.Id, tenant.Name))
61+
62+
var tenants = await _tenantRepository.GetListAsync();
63+
bool sendEmail = false;
64+
var emailBodyBuilder = new System.Text.StringBuilder();
65+
66+
foreach (var tenant in tenants)
6667
{
67-
// Lookup the missing emails
68-
var missingEmailsCount = await _emailNotificationService.GetEmailsChesWithNoResponseCountAsync();
69-
if (missingEmailsCount > 0)
68+
using (_currentTenant.Change(tenant.Id, tenant.Name))
7069
{
71-
Logger.LogWarning("Tenant {TenantName} has {MissingEmailsCount} missing email(s) with a status of Initialized or Sent but no CHES Response.", tenant.Name, missingEmailsCount);
72-
string missingEmailBody = $"Unity tenant {tenant.Name} has {missingEmailsCount} email(s) that were sent but have no CHES Response.";
73-
sendEmail = true;
74-
emailBodyBuilder.AppendLine($"{missingEmailBody}<br />");
75-
}
76-
// Lookup the missing payments
77-
var missingPayments = await GetPaymentsSentWithoutResponseCountAsync();
78-
if (missingPayments > 0)
79-
{
80-
Logger.LogWarning("Tenant {TenantName} has {MissingPaymentsCount} payments sent without a response.", tenant.Name, missingPayments);
81-
string missingPaymentBody = $"Unity tenant {tenant.Name} has {missingPayments} payment(s) that are in Submitted status but have no CAS Response.";
82-
sendEmail = true;
83-
emailBodyBuilder.AppendLine($"{missingPaymentBody}<br />");
70+
// Lookup the missing emails
71+
var missingEmailsCount = await _emailNotificationService.GetEmailsChesWithNoResponseCountAsync();
72+
if (missingEmailsCount > 0)
73+
{
74+
Logger.LogWarning("Tenant {TenantName} has {MissingEmailsCount} missing email(s) with a status of Initialized or Sent but no CHES Response.", tenant.Name, missingEmailsCount);
75+
string missingEmailBody = $"Unity tenant {tenant.Name} has {missingEmailsCount} email(s) that were sent but have no CHES Response.";
76+
sendEmail = true;
77+
emailBodyBuilder.AppendLine($"{missingEmailBody}<br />");
78+
}
79+
// Lookup the missing payments
80+
var missingPayments = await GetPaymentsSentWithoutResponseCountAsync();
81+
if (missingPayments > 0)
82+
{
83+
Logger.LogWarning("Tenant {TenantName} has {MissingPaymentsCount} payments sent without a response.", tenant.Name, missingPayments);
84+
string missingPaymentBody = $"Unity tenant {tenant.Name} has {missingPayments} payment(s) that are in Submitted status but have no CAS Response.";
85+
sendEmail = true;
86+
emailBodyBuilder.AppendLine($"{missingPaymentBody}<br />");
87+
}
8488
}
8589
}
86-
}
8790

88-
if (sendEmail)
89-
{
90-
string emailBody = emailBodyBuilder.ToString();
91-
await SendEmailAlert(emailBody, "Data Health Check Alert - Emails/Payments Missing Responses");
92-
}
91+
if (sendEmail)
92+
{
93+
string emailBody = emailBodyBuilder.ToString();
94+
await SendEmailAlert(emailBody, "Data Health Check Alert - Emails/Payments Missing Responses");
95+
}
9396

94-
Logger.LogInformation("DataHealthCheckWorker Executed...");
95-
await Task.CompletedTask;
97+
Logger.LogInformation("DataHealthCheckWorker Executed...");
98+
await Task.CompletedTask;
99+
}
96100
}
97101

98102
private async Task<int> GetPaymentsSentWithoutResponseCountAsync()
@@ -118,7 +122,7 @@ await _emailNotificationService.SendEmailNotification(
118122
htmlBody,
119123
subject,
120124
"NoReply@gov.bc.ca", "html",
121-
"");
125+
"");
122126

123127
Logger.LogInformation("Missing Alerts Sent...");
124128

database/crunchy-postgres/Chart.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ type: application
1717
# This is the chart version. This version number should be incremented each time you make changes
1818
# to the chart and its templates, including the app version.
1919
# Versions are expected to follow Semantic Versioning (https://semver.org/)
20-
version: 0.1.2
20+
version: 0.1.4
2121

2222
# This is the version number of the application being deployed. This version number should be
2323
# incremented each time you make changes to the application. Versions are not expected to

database/crunchy-postgres/README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,71 @@ A [Prometheus](https://prometheus.io/) exporter for PostgreSQL
132132
| `pgmonitor.exporterr.limits.memory` | Memory limits | `128Mi` |
133133

134134
---
135+
136+
## Data Restore CronJob
137+
138+
This feature allows you to set up a daily CronJob that restores data from a source S3 repository (e.g., from another database instance) into the current PostgreSQL cluster. This is useful for change data capture scenarios where you need to regularly sync data from a source database. The configuration reuses the same structure as `dataSource` and `pgBackRest.s3` for consistency.
139+
140+
### Configuration
141+
142+
| Parameter | Description | Default |
143+
| ---------------------------------------------- | ----------------------------------------------------- | ---------------------- |
144+
| `dataRestore.enabled` | Enable the data restore CronJob | `false` |
145+
| `dataRestore.schedule` | Cron schedule for the restore job | `"0 2 * * *"` |
146+
| `dataRestore.image` | pgBackRest image to use for restore | `crunchy-pgbackrest` |
147+
| `dataRestore.secretName` | K8s secret containing S3 credentials (reuse existing)| `s3-pgbackrest` |
148+
| `dataRestore.repo.name` | Repository name (repo1, repo2, etc.) | `repo2` |
149+
| `dataRestore.repo.path` | S3 path prefix | `/habackup` |
150+
| `dataRestore.repo.s3.bucket` | Source S3 bucket name | `bucketName` |
151+
| `dataRestore.repo.s3.endpoint` | S3 endpoint URL | Object store endpoint |
152+
| `dataRestore.repo.s3.region` | S3 region | `not-used` |
153+
| `dataRestore.repo.s3.uriStyle` | S3 URI style (path or host) | `path` |
154+
| `dataRestore.stanza` | pgBackRest stanza name | `db` |
155+
| `dataRestore.target.clusterName` | Target cluster name (defaults to current cluster) | `""` |
156+
| `dataRestore.target.database` | Target database name | `postgres` |
157+
| `dataRestore.resources.requests.cpu` | CPU requests for restore job | `100m` |
158+
| `dataRestore.resources.requests.memory` | Memory requests for restore job | `256Mi` |
159+
| `dataRestore.resources.limits.cpu` | CPU limits for restore job | `500m` |
160+
| `dataRestore.resources.limits.memory` | Memory limits for restore job | `512Mi` |
161+
| `dataRestore.successfulJobsHistoryLimit` | Number of successful jobs to keep in history | `3` |
162+
| `dataRestore.failedJobsHistoryLimit` | Number of failed jobs to keep in history | `1` |
163+
| `dataRestore.restartPolicy` | Pod restart policy for failed jobs | `OnFailure` |
164+
| `dataRestore.additionalArgs` | Additional pgbackrest arguments | `[]` |
165+
166+
### Usage Example
167+
168+
The configuration reuses existing S3 secrets and follows the same patterns as `dataSource`:
169+
170+
```yaml
171+
dataRestore:
172+
enabled: true
173+
schedule: "0 2 * * *" # Daily at 2 AM
174+
# Reuse existing S3 secret from dataSource or pgBackRest.s3
175+
secretName: "dev-s3-pgbackrest"
176+
repo:
177+
name: repo2
178+
path: "/habackup-source-database"
179+
s3:
180+
bucket: "source-database-backups"
181+
endpoint: "https://sector.objectstore.gov.bc.ca"
182+
region: "not-used"
183+
uriStyle: "path"
184+
stanza: db
185+
target:
186+
database: "myapp"
187+
additionalArgs:
188+
- "--log-level-console=debug"
189+
- "--process-max=2"
190+
```
191+
192+
### Important Notes
193+
194+
- The restore uses `--delta` mode, which only restores changed files for efficiency
195+
- Reuses existing S3 secrets from `dataSource` or `pgBackRest.s3` configuration
196+
- The job runs with the specified S3 repository as the source
197+
- Ensure the source S3 repository contains valid pgBackRest backups
198+
- The target cluster must be accessible and have proper credentials
199+
- Monitor CronJob logs for restore status and any errors
200+
- Configuration follows the same patterns as `dataSource` for consistency
201+
202+
---

database/crunchy-postgres/custom-values-example.yaml

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,37 @@ pgBackRest:
4040
keySecret: "s3SecretValue"
4141
# set the default schedule to avoid conflicts
4242
fullSchedule: 30 11 * * *
43-
incrementalSchedule: 30 3,15,19,23 * * *
43+
incrementalSchedule: 30 3,15,19,23 * * *
44+
45+
# Data restore cronjob configuration example
46+
# Uncomment and configure to enable daily restore from source database
47+
# Reuses the same structure as dataSource for consistency
48+
# dataRestore:
49+
# enabled: true
50+
# schedule: "0 2 * * *" # Daily at 2 AM
51+
# image: "artifacts.developer.gov.bc.ca/bcgov-docker-local/crunchy-pgbackrest:ubi8-2.47-1"
52+
# # Reuse existing S3 secret (same as dataSource or pgBackRest.s3)
53+
# secretName: "new-s3-pgbackrest"
54+
# repo:
55+
# name: repo2
56+
# path: "/habackup-source"
57+
# s3:
58+
# bucket: "source-database-backups"
59+
# endpoint: "https://sector.objectstore.gov.bc.ca"
60+
# region: "not-used"
61+
# uriStyle: "path"
62+
# stanza: db
63+
# target:
64+
# # Leave empty to use current cluster name
65+
# clusterName: ""
66+
# database: "myapp"
67+
# resources:
68+
# requests:
69+
# cpu: 200m
70+
# memory: 512Mi
71+
# limits:
72+
# cpu: 1000m
73+
# memory: 1Gi
74+
# additionalArgs:
75+
# - "--log-level-console=debug"
76+
# - "--process-max=2"

database/crunchy-postgres/templates/PostgresCluster.yaml

Lines changed: 25 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,33 @@ apiVersion: postgres-operator.crunchydata.com/v1beta1
22
kind: PostgresCluster
33
metadata:
44
name: {{ template "crunchy-postgres.fullname" . }}
5-
labels: {{ include "crunchy-postgres.labels" . | nindent 4 }}
5+
labels:
6+
helm.sh/chart: {{ include "crunchy-postgres.chart" . }}
7+
app.kubernetes.io/name: {{ include "crunchy-postgres.name" . }}
8+
app.kubernetes.io/instance: {{ include "crunchy-postgres.fullname" . }}
9+
{{- if .Chart.AppVersion }}
10+
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
11+
{{- end }}
12+
app.kubernetes.io/managed-by: {{ .Release.Service }}
13+
{{- range $key, $value := .Values.labels }}
14+
{{ $key }}: {{ $value | quote }}
15+
{{- end }}
16+
app.kubernetes.io/component: "database"
617
spec:
718
openshift: {{ .Values.openshift | default false }}
819
metadata:
9-
labels: {{ include "crunchy-postgres.labels" . | nindent 6 }}
20+
labels:
21+
helm.sh/chart: {{ include "crunchy-postgres.chart" . }}
22+
app.kubernetes.io/name: {{ include "crunchy-postgres.name" . }}
23+
app.kubernetes.io/instance: {{ include "crunchy-postgres.fullname" . }}
24+
{{- if .Chart.AppVersion }}
25+
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
26+
{{- end }}
27+
app.kubernetes.io/managed-by: {{ .Release.Service }}
28+
{{- range $key, $value := .Values.labels }}
29+
{{ $key }}: {{ $value | quote }}
30+
{{- end }}
31+
app.kubernetes.io/component: "database"
1032
{{ if .Values.crunchyImage }}
1133
image: {{ .Values.crunchyImage }}
1234
{{ end }}
@@ -87,6 +109,7 @@ spec:
87109
- secret:
88110
name: {{ .Values.dataSource.secretName }}
89111
global:
112+
repo2-s3-uri-style: {{ .Values.dataSource.repo.s3.uriStyle | quote }}
90113
repo2-path: {{ .Values.dataSource.repo.path }}
91114
repo:
92115
name: {{ .Values.dataSource.repo.name }}

database/crunchy-postgres/templates/_helpers.tpl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,9 @@ helm.sh/chart: {{ include "crunchy-postgres.chart" . }}
4040
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
4141
{{- end }}
4242
app.kubernetes.io/managed-by: {{ .Release.Service }}
43-
app.kubernetes.io/part-of: {{ index .Values.labels "app.kubernetes.io/part-of" | default "crunchydb" }}
43+
{{- range $key, $value := .Values.labels }}
44+
{{ $key }}: {{ $value | quote }}
45+
{{- end }}
4446
app.kubernetes.io/component: "database"
4547
{{- end }}
4648

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{{- if .Values.dataRestore.enabled }}
2+
apiVersion: v1
3+
kind: ConfigMap
4+
metadata:
5+
name: {{ include "crunchy-postgres.fullname" . }}-data-restore-config
6+
labels:
7+
helm.sh/chart: {{ include "crunchy-postgres.chart" . }}
8+
app.kubernetes.io/name: {{ include "crunchy-postgres.name" . }}
9+
app.kubernetes.io/instance: {{ include "crunchy-postgres.fullname" . }}
10+
{{- if .Chart.AppVersion }}
11+
app.kubernetes.io/version: {{ .Chart.AppVersion | quote }}
12+
{{- end }}
13+
app.kubernetes.io/managed-by: {{ .Release.Service }}
14+
{{- range $key, $value := .Values.labels }}
15+
{{ $key }}: {{ $value | quote }}
16+
{{- end }}
17+
app.kubernetes.io/component: "data-restore-config"
18+
data:
19+
pgbackrest.conf: |
20+
[global]
21+
repo{{ .Values.dataRestore.repo.name | replace "repo" "" }}-type=s3
22+
repo{{ .Values.dataRestore.repo.name | replace "repo" "" }}-s3-bucket={{ .Values.dataRestore.repo.bucket }}
23+
repo{{ .Values.dataRestore.repo.name | replace "repo" "" }}-s3-endpoint={{ .Values.dataRestore.repo.endpoint }}
24+
repo{{ .Values.dataRestore.repo.name | replace "repo" "" }}-s3-region={{ .Values.dataRestore.repo.s3.region | default "not-used" }}
25+
repo{{ .Values.dataRestore.repo.name | replace "repo" "" }}-path={{ .Values.dataRestore.repo.path }}
26+
repo{{ .Values.dataRestore.repo.name | replace "repo" "" }}-s3-uri-style={{ .Values.dataRestore.repo.s3.uriStyle | default "path" }}
27+
log-level-console=info
28+
log-level-file=debug
29+
30+
[{{ .Values.dataRestore.stanza }}]
31+
pg1-host={{ if .Values.dataRestore.target.clusterName }}{{ .Values.dataRestore.target.clusterName }}{{ else }}{{ include "crunchy-postgres.fullname" . }}{{ end }}-primary.{{ .Release.Namespace }}.svc.cluster.local
32+
pg1-port=5432
33+
pg1-user=postgres
34+
pg1-database={{ .Values.dataRestore.target.database }}
35+
{{- end }}

0 commit comments

Comments
 (0)