@highstate/restic 0.9.18 → 0.9.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/highstate.manifest.json +2 -2
- package/dist/index.js +160 -17969
- package/dist/index.js.map +1 -1
- package/dist/repository/index.js +68 -0
- package/dist/repository/index.js.map +1 -0
- package/package.json +9 -8
- package/src/job-pair.ts +179 -156
- package/src/repository/index.ts +77 -0
- package/dist/chunk-G3PMV62Z.js +0 -33
- package/dist/chunk-G3PMV62Z.js.map +0 -1
- package/dist/repo/index.js +0 -47
- package/dist/repo/index.js.map +0 -1
- package/src/repo/index.ts +0 -51
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { restic } from '@highstate/library';
|
|
2
|
+
import { forUnit, toPromise } from '@highstate/pulumi';
|
|
3
|
+
import { parseL34Endpoint, l34EndpointToString } from '@highstate/common';
|
|
4
|
+
import { uniqueBy } from 'remeda';
|
|
5
|
+
|
|
6
|
+
// src/repository/index.ts
|
|
7
|
+
var { args, inputs, secrets, outputs } = forUnit(restic.repository);
|
|
8
|
+
var remoteInfo = await toPromise(
|
|
9
|
+
secrets.rcloneConfig.apply((config) => {
|
|
10
|
+
const remoteNames = Array.from(config.matchAll(/(?<=\[).+?(?=\])/g));
|
|
11
|
+
if (remoteNames.length === 0) {
|
|
12
|
+
throw new Error("No remotes found in rclone config");
|
|
13
|
+
}
|
|
14
|
+
if (remoteNames.length > 1) {
|
|
15
|
+
throw new Error("Multiple remotes found in rclone config");
|
|
16
|
+
}
|
|
17
|
+
const remoteName = remoteNames[0][0];
|
|
18
|
+
const remoteSection = config.split(`[${remoteName}]`)[1]?.split(/\n\s*\[/)[0] || "";
|
|
19
|
+
const typeMatch = remoteSection.match(/^\s*type\s*=\s*(.+)$/m);
|
|
20
|
+
if (!typeMatch) {
|
|
21
|
+
throw new Error(`No type found for remote '${remoteName}'`);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
name: remoteName,
|
|
25
|
+
type: typeMatch[1].trim()
|
|
26
|
+
};
|
|
27
|
+
})
|
|
28
|
+
);
|
|
29
|
+
var { remoteL3Endpoints, remoteL4Endpoints } = await toPromise(inputs);
|
|
30
|
+
var autoDiscoveredEndpoints = {
|
|
31
|
+
yandex: [
|
|
32
|
+
"cloud-api.yandex.com",
|
|
33
|
+
"downloader.disk.yandex.ru",
|
|
34
|
+
"*.storage.yandex.net",
|
|
35
|
+
"*.disk.yandex.net"
|
|
36
|
+
]
|
|
37
|
+
};
|
|
38
|
+
var remoteEndpoints = uniqueBy(
|
|
39
|
+
[
|
|
40
|
+
//
|
|
41
|
+
...(autoDiscoveredEndpoints[remoteInfo.type] ?? []).map(parseL34Endpoint),
|
|
42
|
+
...args.remoteEndpoints.map(parseL34Endpoint),
|
|
43
|
+
...remoteL3Endpoints,
|
|
44
|
+
...remoteL4Endpoints
|
|
45
|
+
],
|
|
46
|
+
l34EndpointToString
|
|
47
|
+
);
|
|
48
|
+
var repository_default = outputs({
|
|
49
|
+
repo: {
|
|
50
|
+
type: "rclone",
|
|
51
|
+
pathPattern: args.pathPattern,
|
|
52
|
+
rcloneConfig: secrets.rcloneConfig,
|
|
53
|
+
remoteName: remoteInfo.name,
|
|
54
|
+
remoteEndpoints
|
|
55
|
+
},
|
|
56
|
+
$statusFields: {
|
|
57
|
+
remoteName: remoteInfo.name,
|
|
58
|
+
remoteType: remoteInfo.type,
|
|
59
|
+
remoteEndpoints: {
|
|
60
|
+
value: remoteEndpoints.map(l34EndpointToString),
|
|
61
|
+
complementaryTo: "remoteEndpoints"
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
export { repository_default as default };
|
|
67
|
+
//# sourceMappingURL=index.js.map
|
|
68
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/repository/index.ts"],"names":[],"mappings":";;;;;;AAKA,IAAM,EAAE,MAAM,MAAA,EAAQ,OAAA,EAAS,SAAQ,GAAI,OAAA,CAAQ,OAAO,UAAU,CAAA;AAEpE,IAAM,aAAa,MAAM,SAAA;AAAA,EACvB,OAAA,CAAQ,YAAA,CAAa,KAAA,CAAM,CAAA,MAAA,KAAU;AACnC,IAAA,MAAM,cAAc,KAAA,CAAM,IAAA,CAAK,MAAA,CAAO,QAAA,CAAS,mBAAmB,CAAC,CAAA;AAEnE,IAAA,IAAI,WAAA,CAAY,WAAW,CAAA,EAAG;AAC5B,MAAA,MAAM,IAAI,MAAM,mCAAmC,CAAA;AAAA,IACrD;AAEA,IAAA,IAAI,WAAA,CAAY,SAAS,CAAA,EAAG;AAC1B,MAAA,MAAM,IAAI,MAAM,yCAAyC,CAAA;AAAA,IAC3D;AAEA,IAAA,MAAM,UAAA,GAAa,WAAA,CAAY,CAAC,CAAA,CAAE,CAAC,CAAA;AAGnC,IAAA,MAAM,aAAA,GAAgB,MAAA,CAAO,KAAA,CAAM,CAAA,CAAA,EAAI,UAAU,CAAA,CAAA,CAAG,CAAA,CAAE,CAAC,CAAA,EAAG,KAAA,CAAM,SAAS,CAAA,CAAE,CAAC,CAAA,IAAK,EAAA;AACjF,IAAA,MAAM,SAAA,GAAY,aAAA,CAAc,KAAA,CAAM,uBAAuB,CAAA;AAE7D,IAAA,IAAI,CAAC,SAAA,EAAW;AACd,MAAA,MAAM,IAAI,KAAA,CAAM,CAAA,0BAAA,EAA6B,UAAU,CAAA,CAAA,CAAG,CAAA;AAAA,IAC5D;AAEA,IAAA,OAAO;AAAA,MACL,IAAA,EAAM,UAAA;AAAA,MACN,IAAA,EAAM,SAAA,CAAU,CAAC,CAAA,CAAE,IAAA;AAAK,KAC1B;AAAA,EACF,CAAC;AACH,CAAA;AAEA,IAAM,EAAE,iBAAA,EAAmB,iBAAA,EAAkB,GAAI,MAAM,UAAU,MAAM,CAAA;AAEvE,IAAM,uBAAA,GAAoD;AAAA,EACxD,MAAA,EAAQ;AAAA,IACN,sBAAA;AAAA,IACA,2BAAA;AAAA,IACA,sBAAA;AAAA,IACA;AAAA;AAEJ,CAAA;AAEA,IAAM,eAAA,GAAkB,QAAA;AAAA,EACtB;AAAA;AAAA,IAEE,GAAA,CAAI,wBAAwB,UAAA,CAAW,IAAI,KAAK,EAAC,EAAG,IAAI,gBAAgB,CAAA;AAAA,IACxE,GAAG,IAAA,CAAK,eAAA,CAAgB,GAAA,CAAI,gBAAgB,CAAA;AAAA,IAC5C,GAAG,iBAAA;AAAA,IACH,GAAG;AAAA,GACL;AAAA,EACA;AACF,CAAA;AAEA,IAAO,qBAAQ,OAAA,CAAQ;AAAA,EACrB,IAAA,EAAM;AAAA,IACJ,IAAA,EAAM,QAAA;AAAA,IACN,aAAa,IAAA,CAAK,WAAA;AAAA,IAClB,cAAc,OAAA,CAAQ,YAAA;AAAA,IACtB,YAAY,UAAA,CAAW,IAAA;AAAA,IACvB;AAAA,GACF;AAAA,EAEA,aAAA,EAAe;AAAA,IACb,YAAY,UAAA,CAAW,IAAA;AAAA,IACvB,YAAY,UAAA,CAAW,IAAA;AAAA,IAEvB,eAAA,EAAiB;AAAA,MACf,KAAA,EAAO,eAAA,CAAgB,GAAA,CAAI,mBAAmB,CAAA;AAAA,MAC9C,eAAA,EAAiB;AAAA;AACnB;AAEJ,CAAC","file":"index.js","sourcesContent":["import { restic } from \"@highstate/library\"\nimport { forUnit, toPromise } from \"@highstate/pulumi\"\nimport { l34EndpointToString, parseL34Endpoint } from \"@highstate/common\"\nimport { uniqueBy } from \"remeda\"\n\nconst { args, inputs, secrets, outputs } = forUnit(restic.repository)\n\nconst remoteInfo = await toPromise(\n secrets.rcloneConfig.apply(config => {\n const remoteNames = Array.from(config.matchAll(/(?<=\\[).+?(?=\\])/g))\n\n if (remoteNames.length === 0) {\n throw new Error(\"No remotes found in rclone config\")\n }\n\n if (remoteNames.length > 1) {\n throw new Error(\"Multiple remotes found in rclone config\")\n }\n\n const remoteName = remoteNames[0][0]\n\n // extract the type from the remote section\n const remoteSection = config.split(`[${remoteName}]`)[1]?.split(/\\n\\s*\\[/)[0] || \"\"\n const typeMatch = remoteSection.match(/^\\s*type\\s*=\\s*(.+)$/m)\n\n if (!typeMatch) {\n throw new Error(`No type found for remote '${remoteName}'`)\n }\n\n return {\n name: remoteName,\n type: typeMatch[1].trim(),\n }\n }),\n)\n\nconst { remoteL3Endpoints, remoteL4Endpoints } = await toPromise(inputs)\n\nconst autoDiscoveredEndpoints: Record<string, string[]> = {\n yandex: [\n \"cloud-api.yandex.com\",\n \"downloader.disk.yandex.ru\",\n \"*.storage.yandex.net\",\n \"*.disk.yandex.net\",\n ],\n}\n\nconst remoteEndpoints = uniqueBy(\n [\n //\n ...(autoDiscoveredEndpoints[remoteInfo.type] ?? []).map(parseL34Endpoint),\n ...args.remoteEndpoints.map(parseL34Endpoint),\n ...remoteL3Endpoints,\n ...remoteL4Endpoints,\n ],\n l34EndpointToString,\n)\n\nexport default outputs({\n repo: {\n type: \"rclone\",\n pathPattern: args.pathPattern,\n rcloneConfig: secrets.rcloneConfig,\n remoteName: remoteInfo.name,\n remoteEndpoints,\n },\n\n $statusFields: {\n remoteName: remoteInfo.name,\n remoteType: remoteInfo.type,\n\n remoteEndpoints: {\n value: remoteEndpoints.map(l34EndpointToString),\n complementaryTo: \"remoteEndpoints\",\n },\n },\n})\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@highstate/restic",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.19",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"dist",
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
"types": "./src/index.ts",
|
|
12
12
|
"default": "./dist/index.js"
|
|
13
13
|
},
|
|
14
|
-
"./
|
|
14
|
+
"./repository": "./dist/repository/index.js"
|
|
15
15
|
},
|
|
16
16
|
"publishConfig": {
|
|
17
17
|
"access": "public"
|
|
@@ -21,15 +21,16 @@
|
|
|
21
21
|
"update-images": "../../scripts/update-images.sh ./assets/images.json"
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@highstate/common": "^0.9.
|
|
25
|
-
"@highstate/
|
|
26
|
-
"@highstate/
|
|
27
|
-
"@highstate/
|
|
24
|
+
"@highstate/common": "^0.9.19",
|
|
25
|
+
"@highstate/contract": "^0.9.19",
|
|
26
|
+
"@highstate/k8s": "^0.9.19",
|
|
27
|
+
"@highstate/library": "^0.9.19",
|
|
28
|
+
"@highstate/pulumi": "^0.9.19",
|
|
28
29
|
"@pulumi/kubernetes": "^4.18.0",
|
|
29
30
|
"remeda": "^2.21.0"
|
|
30
31
|
},
|
|
31
32
|
"devDependencies": {
|
|
32
|
-
"@highstate/cli": "^0.9.
|
|
33
|
+
"@highstate/cli": "^0.9.19"
|
|
33
34
|
},
|
|
34
|
-
"gitHead": "
|
|
35
|
+
"gitHead": "e77d292335556c6e5b6275acda1a3d1609d786a1"
|
|
35
36
|
}
|
package/src/job-pair.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type {
|
|
1
|
+
import type { restic } from "@highstate/library"
|
|
2
2
|
import type { InputL34Endpoint } from "@highstate/common"
|
|
3
3
|
import { batch } from "@pulumi/kubernetes"
|
|
4
4
|
import {
|
|
@@ -8,10 +8,10 @@ import {
|
|
|
8
8
|
Job,
|
|
9
9
|
ScriptBundle,
|
|
10
10
|
Secret,
|
|
11
|
-
type CommonArgs,
|
|
12
11
|
type Container,
|
|
13
12
|
type ScriptDistribution,
|
|
14
13
|
type ScriptEnvironment,
|
|
14
|
+
type ScopedResourceArgs,
|
|
15
15
|
type WorkloadVolume,
|
|
16
16
|
} from "@highstate/k8s"
|
|
17
17
|
import {
|
|
@@ -19,24 +19,22 @@ import {
|
|
|
19
19
|
getUnitInstanceName,
|
|
20
20
|
normalize,
|
|
21
21
|
output,
|
|
22
|
-
Output,
|
|
22
|
+
type Output,
|
|
23
|
+
toPromise,
|
|
23
24
|
type ComponentResourceOptions,
|
|
24
25
|
type Input,
|
|
25
26
|
type InputArray,
|
|
26
|
-
type InstanceTerminal,
|
|
27
|
-
type InstanceTrigger,
|
|
28
|
-
type InstanceTriggerInvocation,
|
|
29
27
|
} from "@highstate/pulumi"
|
|
30
|
-
import {
|
|
28
|
+
import {
|
|
29
|
+
text,
|
|
30
|
+
type UnitTerminal,
|
|
31
|
+
type UnitTrigger,
|
|
32
|
+
type TriggerInvocation,
|
|
33
|
+
} from "@highstate/contract"
|
|
31
34
|
import * as images from "../assets/images.json"
|
|
32
35
|
import { backupEnvironment } from "./scripts"
|
|
33
36
|
|
|
34
|
-
export type BackupJobPairArgs =
|
|
35
|
-
/**
|
|
36
|
-
* The k8s cluster to calculate the repository path.
|
|
37
|
-
*/
|
|
38
|
-
cluster: Input<k8s.Cluster>
|
|
39
|
-
|
|
37
|
+
export type BackupJobPairArgs = ScopedResourceArgs & {
|
|
40
38
|
/**
|
|
41
39
|
* The repository to backup/restore to/from.
|
|
42
40
|
*/
|
|
@@ -108,22 +106,22 @@ export class BackupJobPair extends ComponentResource {
|
|
|
108
106
|
/**
|
|
109
107
|
* The credentials used to access the repository and encrypt the backups.
|
|
110
108
|
*/
|
|
111
|
-
readonly credentials:
|
|
109
|
+
readonly credentials: Secret
|
|
112
110
|
|
|
113
111
|
/**
|
|
114
112
|
* The script bundle used by the backup and restore jobs.
|
|
115
113
|
*/
|
|
116
|
-
readonly scriptBundle:
|
|
114
|
+
readonly scriptBundle: ScriptBundle
|
|
117
115
|
|
|
118
116
|
/**
|
|
119
117
|
* The job resource which restores the volume from the backup before creating an application.
|
|
120
118
|
*/
|
|
121
|
-
readonly restoreJob:
|
|
119
|
+
readonly restoreJob: Job
|
|
122
120
|
|
|
123
121
|
/**
|
|
124
122
|
* The cron job resource which backups the volume regularly.
|
|
125
123
|
*/
|
|
126
|
-
readonly backupJob:
|
|
124
|
+
readonly backupJob: CronJob
|
|
127
125
|
|
|
128
126
|
/**
|
|
129
127
|
* The full name of the Restic repository.
|
|
@@ -137,155 +135,150 @@ export class BackupJobPair extends ComponentResource {
|
|
|
137
135
|
) {
|
|
138
136
|
super("highstate:restic:BackupJobPair", name, args, opts)
|
|
139
137
|
|
|
140
|
-
|
|
141
|
-
return Secret.create(
|
|
142
|
-
`${name}-backup-credentials`,
|
|
143
|
-
{
|
|
144
|
-
namespace: args.namespace,
|
|
145
|
-
cluster: args.cluster,
|
|
138
|
+
const cluster = output(args.namespace).cluster
|
|
146
139
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
140
|
+
this.credentials = Secret.create(
|
|
141
|
+
`${name}-backup-credentials`,
|
|
142
|
+
{
|
|
143
|
+
namespace: args.namespace,
|
|
144
|
+
|
|
145
|
+
stringData: {
|
|
146
|
+
password: args.backupPassword,
|
|
147
|
+
"rclone.conf": output(args.resticRepo).rcloneConfig,
|
|
151
148
|
},
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
149
|
+
},
|
|
150
|
+
{ ...opts, parent: this },
|
|
151
|
+
)
|
|
155
152
|
|
|
156
|
-
this.resticRepoPath = output(
|
|
157
|
-
|
|
158
|
-
|
|
153
|
+
this.resticRepoPath = output({
|
|
154
|
+
cluster,
|
|
155
|
+
resticRepo: args.resticRepo,
|
|
156
|
+
}).apply(({ cluster, resticRepo }) => {
|
|
157
|
+
const relativePath = resticRepo.pathPattern
|
|
158
|
+
.replace(/\$clusterName/g, cluster.name)
|
|
159
159
|
.replace(/\$appName/g, name)
|
|
160
160
|
.replace(/\$unitName/g, getUnitInstanceName())
|
|
161
161
|
|
|
162
|
-
return `rclone:${
|
|
162
|
+
return `rclone:${resticRepo.remoteName}:${relativePath}`
|
|
163
163
|
})
|
|
164
164
|
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
curl https://rclone.org/install.sh | bash
|
|
181
|
-
`,
|
|
165
|
+
this.scriptBundle = new ScriptBundle(
|
|
166
|
+
`${name}-backup-scripts`,
|
|
167
|
+
{
|
|
168
|
+
namespace: args.namespace,
|
|
169
|
+
distribution: args.distribution ?? "alpine",
|
|
170
|
+
|
|
171
|
+
environments: output({
|
|
172
|
+
environment: args.environment,
|
|
173
|
+
environments: args.environments,
|
|
174
|
+
}).apply(({ environment, environments }) => [
|
|
175
|
+
backupEnvironment,
|
|
176
|
+
{
|
|
177
|
+
alpine: {
|
|
178
|
+
packages: ["rclone"],
|
|
182
179
|
},
|
|
183
180
|
|
|
184
|
-
|
|
185
|
-
|
|
181
|
+
ubuntu: {
|
|
182
|
+
preInstallPackages: ["curl", "unzip"],
|
|
186
183
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
environment: {
|
|
193
|
-
RESTIC_REPOSITORY: repoPath,
|
|
194
|
-
RESTIC_PASSWORD_FILE: "/credentials/password",
|
|
195
|
-
RESTIC_HOSTNAME: "default",
|
|
196
|
-
RCLONE_CONFIG: "/credentials/rclone.conf",
|
|
197
|
-
EXTRA_BACKUP_OPTIONS: args.backupOptions?.join(" "),
|
|
198
|
-
},
|
|
184
|
+
preInstallScripts: {
|
|
185
|
+
"rclone.sh": text`
|
|
186
|
+
#!/bin/sh
|
|
187
|
+
set -e
|
|
199
188
|
|
|
200
|
-
|
|
189
|
+
curl https://rclone.org/install.sh | bash
|
|
190
|
+
`,
|
|
191
|
+
},
|
|
201
192
|
|
|
202
|
-
|
|
203
|
-
{
|
|
204
|
-
volume: this.credentials,
|
|
205
|
-
mountPath: "/credentials",
|
|
206
|
-
readOnly: true,
|
|
193
|
+
allowedEndpoints: ["rclone.org:443", "downloads.rclone.org:443"],
|
|
207
194
|
},
|
|
208
|
-
...(args.volume
|
|
209
|
-
? [
|
|
210
|
-
{
|
|
211
|
-
volume: args.volume,
|
|
212
|
-
mountPath: "/data",
|
|
213
|
-
subPath: args.subPath,
|
|
214
|
-
},
|
|
215
|
-
]
|
|
216
|
-
: []),
|
|
217
|
-
],
|
|
218
|
-
} satisfies ScriptEnvironment
|
|
219
|
-
},
|
|
220
|
-
)
|
|
221
195
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
)
|
|
238
|
-
})
|
|
196
|
+
allowedEndpoints: output({
|
|
197
|
+
allowedEndpoints: args.allowedEndpoints,
|
|
198
|
+
resticRepo: args.resticRepo,
|
|
199
|
+
}).apply(({ allowedEndpoints, resticRepo }) => [
|
|
200
|
+
...(allowedEndpoints ?? []),
|
|
201
|
+
...(resticRepo.remoteEndpoints ?? []),
|
|
202
|
+
]),
|
|
203
|
+
|
|
204
|
+
environment: {
|
|
205
|
+
RESTIC_REPOSITORY: this.resticRepoPath,
|
|
206
|
+
RESTIC_PASSWORD_FILE: "/credentials/password",
|
|
207
|
+
RESTIC_HOSTNAME: "default",
|
|
208
|
+
RCLONE_CONFIG: "/credentials/rclone.conf",
|
|
209
|
+
EXTRA_BACKUP_OPTIONS: output(args.backupOptions).apply(options => options?.join(" ")),
|
|
210
|
+
},
|
|
239
211
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
212
|
+
volumes: [this.credentials, ...(args.volume ? [args.volume] : [])],
|
|
213
|
+
|
|
214
|
+
volumeMounts: [
|
|
215
|
+
{
|
|
216
|
+
volume: this.credentials,
|
|
217
|
+
mountPath: "/credentials",
|
|
218
|
+
readOnly: true,
|
|
219
|
+
},
|
|
220
|
+
...(args.volume
|
|
221
|
+
? [
|
|
222
|
+
{
|
|
223
|
+
volume: args.volume,
|
|
224
|
+
mountPath: "/data",
|
|
225
|
+
subPath: args.subPath,
|
|
226
|
+
},
|
|
227
|
+
]
|
|
228
|
+
: []),
|
|
229
|
+
],
|
|
230
|
+
} satisfies ScriptEnvironment,
|
|
231
|
+
...normalize(environment, environments),
|
|
232
|
+
]),
|
|
233
|
+
},
|
|
234
|
+
{ ...opts, parent: this },
|
|
235
|
+
)
|
|
246
236
|
|
|
247
|
-
|
|
248
|
-
|
|
237
|
+
this.restoreJob = Job.create(
|
|
238
|
+
`${name}-restore`,
|
|
239
|
+
{
|
|
240
|
+
namespace: args.namespace,
|
|
249
241
|
|
|
242
|
+
container: output(args.restoreContainer).apply(restoreContainer =>
|
|
243
|
+
createScriptContainer({
|
|
244
|
+
...restoreContainer,
|
|
250
245
|
main: "restore.sh",
|
|
251
246
|
bundle: this.scriptBundle,
|
|
252
247
|
}),
|
|
248
|
+
),
|
|
253
249
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
})
|
|
259
|
-
|
|
260
|
-
this.backupJob = output(args).apply(args => {
|
|
261
|
-
return new CronJob(
|
|
262
|
-
`${name}-backup`,
|
|
263
|
-
{
|
|
264
|
-
namespace: args.namespace,
|
|
265
|
-
cluster: args.cluster,
|
|
250
|
+
backoffLimit: 2,
|
|
251
|
+
},
|
|
252
|
+
{ ...opts, parent: this },
|
|
253
|
+
)
|
|
266
254
|
|
|
267
|
-
|
|
268
|
-
|
|
255
|
+
this.backupJob = CronJob.create(
|
|
256
|
+
`${name}-backup`,
|
|
257
|
+
{
|
|
258
|
+
namespace: args.namespace,
|
|
269
259
|
|
|
260
|
+
container: output(args.backupContainer).apply(backupContainer =>
|
|
261
|
+
createScriptContainer({
|
|
262
|
+
...backupContainer,
|
|
270
263
|
main: "backup.sh",
|
|
271
264
|
bundle: this.scriptBundle,
|
|
272
265
|
}),
|
|
266
|
+
),
|
|
273
267
|
|
|
274
|
-
|
|
275
|
-
|
|
268
|
+
schedule: args.schedule ?? "0 0 * * *",
|
|
269
|
+
concurrencyPolicy: "Forbid",
|
|
276
270
|
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
},
|
|
271
|
+
jobTemplate: {
|
|
272
|
+
spec: {
|
|
273
|
+
backoffLimit: 2,
|
|
281
274
|
},
|
|
282
275
|
},
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
276
|
+
},
|
|
277
|
+
{ ...opts, parent: this },
|
|
278
|
+
)
|
|
286
279
|
}
|
|
287
280
|
|
|
288
|
-
handleTrigger(triggers:
|
|
281
|
+
handleTrigger(triggers: TriggerInvocation[]): UnitTrigger | undefined {
|
|
289
282
|
const triggerName = `restic.backup-on-destroy.${this.name}`
|
|
290
283
|
const invokedTrigger = triggers.find(trigger => trigger.name === triggerName)
|
|
291
284
|
|
|
@@ -296,16 +289,23 @@ export class BackupJobPair extends ComponentResource {
|
|
|
296
289
|
|
|
297
290
|
return {
|
|
298
291
|
name: triggerName,
|
|
299
|
-
|
|
300
|
-
|
|
292
|
+
meta: {
|
|
293
|
+
title: "Backup on Destroy",
|
|
294
|
+
description: `Backup the "${this.name}" before destroying.`,
|
|
295
|
+
icon: "material-symbols:backup",
|
|
296
|
+
},
|
|
301
297
|
spec: {
|
|
302
298
|
type: "before-destroy",
|
|
303
299
|
},
|
|
304
300
|
}
|
|
305
301
|
}
|
|
306
302
|
|
|
307
|
-
createTerminal():
|
|
308
|
-
return {
|
|
303
|
+
createTerminal(): Output<UnitTerminal> {
|
|
304
|
+
return output({
|
|
305
|
+
backupPassword: this.args.backupPassword,
|
|
306
|
+
resticRepo: this.args.resticRepo,
|
|
307
|
+
resticRepoPath: this.resticRepoPath,
|
|
308
|
+
}).apply(({ backupPassword, resticRepo, resticRepoPath }) => ({
|
|
309
309
|
name: "restic",
|
|
310
310
|
|
|
311
311
|
meta: {
|
|
@@ -314,39 +314,62 @@ export class BackupJobPair extends ComponentResource {
|
|
|
314
314
|
icon: "material-symbols:backup",
|
|
315
315
|
},
|
|
316
316
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
317
|
+
spec: {
|
|
318
|
+
image: images["terminal-restic"].image,
|
|
319
|
+
command: ["bash", "/welcome.sh"],
|
|
320
|
+
|
|
321
|
+
files: {
|
|
322
|
+
"/welcome.sh": {
|
|
323
|
+
meta: { name: "/welcome.sh" },
|
|
324
|
+
content: {
|
|
325
|
+
type: "embedded" as const,
|
|
326
|
+
value: text`
|
|
327
|
+
echo "Use 'restic' to manage the repository."
|
|
328
|
+
echo
|
|
329
|
+
|
|
330
|
+
exec bash
|
|
331
|
+
`,
|
|
332
|
+
},
|
|
333
|
+
},
|
|
324
334
|
|
|
325
|
-
|
|
326
|
-
|
|
335
|
+
"/credentials/password": {
|
|
336
|
+
meta: { name: "/credentials/password" },
|
|
337
|
+
content: {
|
|
338
|
+
type: "embedded" as const,
|
|
339
|
+
value: backupPassword,
|
|
340
|
+
},
|
|
341
|
+
},
|
|
327
342
|
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
343
|
+
"/root/.config/rclone/rclone.conf": {
|
|
344
|
+
meta: { name: "/root/.config/rclone/rclone.conf" },
|
|
345
|
+
content: {
|
|
346
|
+
type: "embedded" as const,
|
|
347
|
+
value: resticRepo.rcloneConfig,
|
|
348
|
+
},
|
|
349
|
+
},
|
|
350
|
+
},
|
|
331
351
|
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
352
|
+
env: {
|
|
353
|
+
RESTIC_REPOSITORY: resticRepoPath,
|
|
354
|
+
RESTIC_PASSWORD_FILE: "/credentials/password",
|
|
355
|
+
},
|
|
335
356
|
},
|
|
336
|
-
}
|
|
357
|
+
}))
|
|
337
358
|
}
|
|
338
359
|
|
|
339
360
|
private async createBackupOnDestroyJob(): Promise<void> {
|
|
361
|
+
const cluster = await toPromise(output(this.args.namespace).cluster)
|
|
362
|
+
|
|
340
363
|
new batch.v1.Job(
|
|
341
364
|
`${this.name}-backup-on-destroy`,
|
|
342
365
|
{
|
|
343
366
|
metadata: {
|
|
344
367
|
name: `${this.name}-backup-on-destroy`,
|
|
345
|
-
namespace: this.backupJob.
|
|
368
|
+
namespace: this.backupJob.metadata.namespace,
|
|
346
369
|
},
|
|
347
|
-
spec: this.backupJob.
|
|
370
|
+
spec: this.backupJob.spec.jobTemplate.spec,
|
|
348
371
|
},
|
|
349
|
-
{ ...this.opts, parent: this, provider:
|
|
372
|
+
{ ...this.opts, parent: this, provider: getProvider(cluster) },
|
|
350
373
|
)
|
|
351
374
|
}
|
|
352
375
|
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { restic } from "@highstate/library"
|
|
2
|
+
import { forUnit, toPromise } from "@highstate/pulumi"
|
|
3
|
+
import { l34EndpointToString, parseL34Endpoint } from "@highstate/common"
|
|
4
|
+
import { uniqueBy } from "remeda"
|
|
5
|
+
|
|
6
|
+
const { args, inputs, secrets, outputs } = forUnit(restic.repository)
|
|
7
|
+
|
|
8
|
+
const remoteInfo = await toPromise(
|
|
9
|
+
secrets.rcloneConfig.apply(config => {
|
|
10
|
+
const remoteNames = Array.from(config.matchAll(/(?<=\[).+?(?=\])/g))
|
|
11
|
+
|
|
12
|
+
if (remoteNames.length === 0) {
|
|
13
|
+
throw new Error("No remotes found in rclone config")
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (remoteNames.length > 1) {
|
|
17
|
+
throw new Error("Multiple remotes found in rclone config")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const remoteName = remoteNames[0][0]
|
|
21
|
+
|
|
22
|
+
// extract the type from the remote section
|
|
23
|
+
const remoteSection = config.split(`[${remoteName}]`)[1]?.split(/\n\s*\[/)[0] || ""
|
|
24
|
+
const typeMatch = remoteSection.match(/^\s*type\s*=\s*(.+)$/m)
|
|
25
|
+
|
|
26
|
+
if (!typeMatch) {
|
|
27
|
+
throw new Error(`No type found for remote '${remoteName}'`)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
name: remoteName,
|
|
32
|
+
type: typeMatch[1].trim(),
|
|
33
|
+
}
|
|
34
|
+
}),
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
const { remoteL3Endpoints, remoteL4Endpoints } = await toPromise(inputs)
|
|
38
|
+
|
|
39
|
+
const autoDiscoveredEndpoints: Record<string, string[]> = {
|
|
40
|
+
yandex: [
|
|
41
|
+
"cloud-api.yandex.com",
|
|
42
|
+
"downloader.disk.yandex.ru",
|
|
43
|
+
"*.storage.yandex.net",
|
|
44
|
+
"*.disk.yandex.net",
|
|
45
|
+
],
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const remoteEndpoints = uniqueBy(
|
|
49
|
+
[
|
|
50
|
+
//
|
|
51
|
+
...(autoDiscoveredEndpoints[remoteInfo.type] ?? []).map(parseL34Endpoint),
|
|
52
|
+
...args.remoteEndpoints.map(parseL34Endpoint),
|
|
53
|
+
...remoteL3Endpoints,
|
|
54
|
+
...remoteL4Endpoints,
|
|
55
|
+
],
|
|
56
|
+
l34EndpointToString,
|
|
57
|
+
)
|
|
58
|
+
|
|
59
|
+
export default outputs({
|
|
60
|
+
repo: {
|
|
61
|
+
type: "rclone",
|
|
62
|
+
pathPattern: args.pathPattern,
|
|
63
|
+
rcloneConfig: secrets.rcloneConfig,
|
|
64
|
+
remoteName: remoteInfo.name,
|
|
65
|
+
remoteEndpoints,
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
$statusFields: {
|
|
69
|
+
remoteName: remoteInfo.name,
|
|
70
|
+
remoteType: remoteInfo.type,
|
|
71
|
+
|
|
72
|
+
remoteEndpoints: {
|
|
73
|
+
value: remoteEndpoints.map(l34EndpointToString),
|
|
74
|
+
complementaryTo: "remoteEndpoints",
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
})
|
package/dist/chunk-G3PMV62Z.js
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
var __create = Object.create;
|
|
2
|
-
var __defProp = Object.defineProperty;
|
|
3
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
6
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
7
|
-
var __commonJS = (cb, mod) => function __require() {
|
|
8
|
-
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
9
|
-
};
|
|
10
|
-
var __export = (target, all) => {
|
|
11
|
-
for (var name in all)
|
|
12
|
-
__defProp(target, name, { get: all[name], enumerable: true });
|
|
13
|
-
};
|
|
14
|
-
var __copyProps = (to, from, except, desc) => {
|
|
15
|
-
if (from && typeof from === "object" || typeof from === "function") {
|
|
16
|
-
for (let key of __getOwnPropNames(from))
|
|
17
|
-
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
18
|
-
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
19
|
-
}
|
|
20
|
-
return to;
|
|
21
|
-
};
|
|
22
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
23
|
-
// If the importer is in node compatibility mode or this is not an ESM
|
|
24
|
-
// file that has been converted to a CommonJS file using a Babel-
|
|
25
|
-
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
26
|
-
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
27
|
-
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
28
|
-
mod
|
|
29
|
-
));
|
|
30
|
-
|
|
31
|
-
export { __commonJS, __export, __toESM };
|
|
32
|
-
//# sourceMappingURL=chunk-G3PMV62Z.js.map
|
|
33
|
-
//# sourceMappingURL=chunk-G3PMV62Z.js.map
|