@highstate/restic 0.9.18 → 0.9.20

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.
@@ -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.18",
3
+ "version": "0.9.20",
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
- "./repo": "./dist/repo/index.js"
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.18",
25
- "@highstate/k8s": "^0.9.18",
26
- "@highstate/library": "^0.9.18",
27
- "@highstate/pulumi": "^0.9.18",
24
+ "@highstate/common": "^0.9.20",
25
+ "@highstate/contract": "^0.9.20",
26
+ "@highstate/k8s": "^0.9.20",
27
+ "@highstate/library": "^0.9.20",
28
+ "@highstate/pulumi": "^0.9.20",
28
29
  "@pulumi/kubernetes": "^4.18.0",
29
30
  "remeda": "^2.21.0"
30
31
  },
31
32
  "devDependencies": {
32
- "@highstate/cli": "^0.9.18"
33
+ "@highstate/cli": "^0.9.20"
33
34
  },
34
- "gitHead": "9ebcd7da56b00b8ca08bf52cc8438f527338cd64"
35
+ "gitHead": "4bf9183450c2c6f51d6a99d77efc379ff5c7b7ef"
35
36
  }
package/src/job-pair.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { k8s, restic } from "@highstate/library"
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 { text } from "@highstate/contract"
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 = CommonArgs & {
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: Output<Secret>
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: Output<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: Output<Job>
119
+ readonly restoreJob: Job
122
120
 
123
121
  /**
124
122
  * The cron job resource which backups the volume regularly.
125
123
  */
126
- readonly backupJob: Output<CronJob>
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
- this.credentials = output(args).apply(args => {
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
- stringData: {
148
- password: args.backupPassword,
149
- "rclone.conf": args.resticRepo.rcloneConfig,
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
- { ...opts, parent: this },
153
- )
154
- })
149
+ },
150
+ { ...opts, parent: this },
151
+ )
155
152
 
156
- this.resticRepoPath = output(args).apply(args => {
157
- const relativePath = args.resticRepo.pathPattern
158
- .replace(/\$clusterName/g, args.cluster.name)
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:${args.resticRepo.remoteName}:${relativePath}`
162
+ return `rclone:${resticRepo.remoteName}:${relativePath}`
163
163
  })
164
164
 
165
- const environment = output({ args, repoPath: this.resticRepoPath }).apply(
166
- ({ args, repoPath }) => {
167
- return {
168
- alpine: {
169
- packages: ["rclone"],
170
- },
171
-
172
- ubuntu: {
173
- preInstallPackages: ["curl", "unzip"],
174
-
175
- preInstallScripts: {
176
- "rclone.sh": text`
177
- #!/bin/sh
178
- set -e
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
- allowedEndpoints: ["rclone.org:443", "downloads.rclone.org:443"],
185
- },
181
+ ubuntu: {
182
+ preInstallPackages: ["curl", "unzip"],
186
183
 
187
- allowedEndpoints: [
188
- ...(args.allowedEndpoints ?? []),
189
- ...(args.resticRepo.remoteEndpoints ?? []),
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
- volumes: [this.credentials, ...(args.volume ? [args.volume] : [])],
189
+ curl https://rclone.org/install.sh | bash
190
+ `,
191
+ },
201
192
 
202
- volumeMounts: [
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
- this.scriptBundle = output(args).apply(args => {
223
- return new ScriptBundle(
224
- `${name}-backup-scripts`,
225
- {
226
- cluster: args.cluster,
227
- namespace: args.namespace,
228
- distribution: args.distribution ?? "alpine",
229
-
230
- environments: [
231
- backupEnvironment,
232
- environment,
233
- ...normalize(args.environment, args.environments),
234
- ],
235
- },
236
- { ...opts, parent: this },
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
- this.restoreJob = output(args).apply(args => {
241
- return new Job(
242
- `${name}-restore`,
243
- {
244
- cluster: args.cluster,
245
- namespace: args.namespace,
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
- container: createScriptContainer({
248
- ...args.restoreContainer,
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
- backoffLimit: 2,
255
- },
256
- { ...opts, parent: this },
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
- container: createScriptContainer({
268
- ...args.backupContainer,
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
- schedule: args.schedule ?? "0 0 * * *",
275
- concurrencyPolicy: "Forbid",
268
+ schedule: args.schedule ?? "0 0 * * *",
269
+ concurrencyPolicy: "Forbid",
276
270
 
277
- jobTemplate: {
278
- spec: {
279
- backoffLimit: 2,
280
- },
271
+ jobTemplate: {
272
+ spec: {
273
+ backoffLimit: 2,
281
274
  },
282
275
  },
283
- { ...opts, parent: this },
284
- )
285
- })
276
+ },
277
+ { ...opts, parent: this },
278
+ )
286
279
  }
287
280
 
288
- handleTrigger(triggers: InstanceTriggerInvocation[]): InstanceTrigger | undefined {
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
- title: "Backup on Destroy",
300
- description: `Backup the "${this.name}" before destroying.`,
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(): InstanceTerminal {
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
- image: images["terminal-restic"].image,
318
- command: ["bash", "/welcome.sh"],
319
-
320
- files: {
321
- "/welcome.sh": text`
322
- echo "Use 'restic' to manage the repository."
323
- echo
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
- exec bash
326
- `,
335
+ "/credentials/password": {
336
+ meta: { name: "/credentials/password" },
337
+ content: {
338
+ type: "embedded" as const,
339
+ value: backupPassword,
340
+ },
341
+ },
327
342
 
328
- "/credentials/password": output(this.args).backupPassword,
329
- "/root/.config/rclone/rclone.conf": output(this.args).resticRepo.rcloneConfig,
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
- env: {
333
- RESTIC_REPOSITORY: this.resticRepoPath,
334
- RESTIC_PASSWORD_FILE: "/credentials/password",
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.cronJob.metadata.namespace,
368
+ namespace: this.backupJob.metadata.namespace,
346
369
  },
347
- spec: this.backupJob.cronJob.spec.jobTemplate.spec,
370
+ spec: this.backupJob.spec.jobTemplate.spec,
348
371
  },
349
- { ...this.opts, parent: this, provider: await getProvider(this.args.cluster) },
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
+ })
@@ -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