@fabrika/provider-zerops 0.0.26 → 0.3.1

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.
Files changed (83) hide show
  1. package/package.json +18 -5
  2. package/src/api/client.ts +522 -0
  3. package/src/api/errors.ts +50 -0
  4. package/src/api/polling.ts +59 -0
  5. package/src/api/readers.ts +185 -0
  6. package/src/api/service-env-response.ts +34 -0
  7. package/src/api/types.ts +602 -0
  8. package/src/api.ts +29 -1378
  9. package/src/authoring.ts +7 -10
  10. package/src/cli-args.ts +0 -5
  11. package/src/cli.ts +3 -28
  12. package/src/collaborators.ts +2 -4
  13. package/src/compile.ts +75 -20
  14. package/src/control.ts +217 -447
  15. package/src/descriptor.ts +135 -0
  16. package/src/index.ts +24 -128
  17. package/src/installation/admin-options.ts +128 -0
  18. package/src/installation/admin.ts +123 -0
  19. package/src/installation/deploy-options.ts +155 -0
  20. package/src/installation/deploy.ts +668 -0
  21. package/src/installation/generated/platform-console-schema.ts +41 -0
  22. package/src/installation/generated/platform-proxy-manifest.ts +72 -0
  23. package/src/installation/hosts.ts +117 -0
  24. package/src/installation/index.ts +366 -0
  25. package/src/installation/init.ts +521 -0
  26. package/src/installation/install-collaborators.ts +28 -0
  27. package/src/installation/install-environment.ts +193 -0
  28. package/src/installation/install-options.ts +193 -0
  29. package/src/installation/install-project.ts +201 -0
  30. package/src/installation/install-provisioning.ts +185 -0
  31. package/src/installation/install.ts +286 -0
  32. package/src/installation/installation-source.ts +42 -0
  33. package/src/installation/log.ts +77 -0
  34. package/src/installation/machine-key.ts +65 -0
  35. package/src/installation/manifest.ts +95 -0
  36. package/src/installation/placement-options.ts +33 -0
  37. package/src/installation/platform-source.ts +106 -0
  38. package/src/installation/platform-upload.ts +34 -0
  39. package/src/installation/proxy-manifest.ts +113 -0
  40. package/src/installation/secrets.ts +79 -0
  41. package/src/installation/sidecar.ts +122 -0
  42. package/src/installation/source-options.ts +12 -0
  43. package/src/installation/templates/README.md +116 -0
  44. package/src/installation/templates/fabrika.ref +1 -0
  45. package/src/installation/templates/gitignore +3 -0
  46. package/src/installation/templates/platform.yml +144 -0
  47. package/src/installation/topology/artifacts.ts +141 -0
  48. package/src/installation/topology/index.ts +13 -0
  49. package/src/installation/topology/installation-schemas.ts +225 -0
  50. package/src/installation/topology/invariants.ts +86 -0
  51. package/src/installation/topology/setups.ts +442 -0
  52. package/src/installation/topology/topology.ts +501 -0
  53. package/src/installation/topology/validate.ts +26 -0
  54. package/src/installation/upgrade-options.ts +108 -0
  55. package/src/installation/upgrade-output.ts +31 -0
  56. package/src/installation/upgrade.ts +373 -0
  57. package/src/managed-env.ts +105 -0
  58. package/src/manifest.ts +208 -315
  59. package/src/namespace/claims.ts +77 -0
  60. package/src/namespace/codec.ts +179 -0
  61. package/src/namespace/constants.ts +30 -0
  62. package/src/namespace/errors.ts +60 -0
  63. package/src/namespace/manifest.ts +28 -0
  64. package/src/namespace/operator.ts +151 -0
  65. package/src/namespace/provisioning.ts +482 -0
  66. package/src/namespace/proxy-attempt.ts +220 -0
  67. package/src/namespace/proxy-source.ts +22 -0
  68. package/src/namespace/topology.ts +115 -0
  69. package/src/namespace/types.ts +114 -0
  70. package/src/namespace.ts +34 -1260
  71. package/src/plan.ts +21 -23
  72. package/src/provider.ts +399 -254
  73. package/src/service-references.ts +28 -0
  74. package/src/source/constants.ts +11 -0
  75. package/src/source/port.ts +49 -0
  76. package/src/source/types.ts +4 -0
  77. package/src/source/validation.ts +72 -0
  78. package/src/source.ts +2 -1220
  79. package/src/types.ts +8 -7
  80. package/src/version-attempt.ts +18 -0
  81. package/src/codec.ts +0 -108
  82. package/src/namespace-command.ts +0 -209
  83. package/src/source-connection.ts +0 -344
package/src/types.ts CHANGED
@@ -10,6 +10,7 @@ import type {
10
10
  ZeropsYamlSetup,
11
11
  } from './schema.generated'
12
12
  import type { ZeropsSourceRepository } from './source'
13
+ import type { ZeropsSourcePrivateBinding } from './source/port'
13
14
 
14
15
  export interface ZeropsResourceContext {
15
16
  env: string
@@ -119,15 +120,15 @@ export interface ZeropsRuntimeSource {
119
120
  runId: string
120
121
  repository: ZeropsSourceRepository
121
122
  commitSha: string
122
- githubInstallationId?: number
123
+ /** Absent for a public repository. */
124
+ privateBinding?: ZeropsSourcePrivateBinding
123
125
  }
124
126
 
125
- /** Credential-free recovery state persisted beside one Zerops app-version id. */
126
- export type ZeropsRunState =
127
- | { appVersionId: string; phase: 'version_created' }
128
- | { appVersionId: string; phase: 'source_uploaded' }
129
- | { appVersionId: string; phase: 'build_trigger_requested' }
130
- | { appVersionId: string; phase: 'build_triggered'; processId?: string }
127
+ /** The run checkpoint (ADR-0044): the reserved version, and whether its build request may have been sent. */
128
+ export interface ZeropsRunState {
129
+ appVersionId: string
130
+ triggered: boolean
131
+ }
131
132
 
132
133
  /** Ephemeral runtime coordinates. This value is encoded only after credentials are composed. */
133
134
  export interface ZeropsRuntimeTarget {
@@ -0,0 +1,18 @@
1
+ import { ZeropsApiError } from './api/errors'
2
+ import type { ZeropsApi } from './api/types'
3
+
4
+ export const isVersionNotFound = (error: unknown): boolean =>
5
+ error instanceof ZeropsApiError && error.status === 400 && error.code === 'appVersionNotFound'
6
+
7
+ /**
8
+ * Delete a reserved version whose build was never requested. Deletion is the barrier: a later upload to
9
+ * it leaves nothing buildable and a build request fails `appVersionNotFound` (ADR-0044). An already
10
+ * absent version counts as deleted. Never call this once a build may have been submitted.
11
+ */
12
+ export async function deleteVersion(api: Pick<ZeropsApi, 'deleteAppVersion'>, appVersionId: string, signal: AbortSignal): Promise<void> {
13
+ try {
14
+ await api.deleteAppVersion({ appVersionId, signal })
15
+ } catch (error) {
16
+ if (!isVersionNotFound(error)) throw error
17
+ }
18
+ }
package/src/codec.ts DELETED
@@ -1,108 +0,0 @@
1
- import type { JsonValue, ProviderCodec } from '@fabrika/provider-contract'
2
- import type { ZeropsRuntimeSource, ZeropsRuntimeTarget } from './types'
3
-
4
- const field = (payload: JsonValue, key: string): JsonValue | undefined => {
5
- if (typeof payload !== 'object' || payload === null || Array.isArray(payload)) {
6
- throw new Error('Zerops target must be an object')
7
- }
8
- return payload[key]
9
- }
10
-
11
- const requiredString = (payload: JsonValue, key: string): string => {
12
- const value = field(payload, key)
13
- if (typeof value !== 'string' || value === '') {
14
- throw new Error(`Zerops target ${key} must be a non-empty string`)
15
- }
16
- return value
17
- }
18
-
19
- const optionalString = (payload: JsonValue, key: string): string | undefined => {
20
- const value = field(payload, key)
21
- if (value === undefined) {
22
- return undefined
23
- }
24
- if (typeof value !== 'string' || value === '') {
25
- throw new Error(`Zerops target ${key} must be a non-empty string when present`)
26
- }
27
- return value
28
- }
29
-
30
- const IMMUTABLE_GIT_OBJECT = /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/
31
-
32
- const object = (value: JsonValue | undefined, label: string): { readonly [key: string]: JsonValue } => {
33
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
34
- throw new Error(`${label} must be an object`)
35
- }
36
- return value
37
- }
38
-
39
- const runtimeSource = (payload: JsonValue): ZeropsRuntimeSource | undefined => {
40
- const value = field(payload, 'source')
41
- if (value === undefined) return undefined
42
- const source = object(value, 'Zerops target source')
43
- const repository = object(source['repository'], 'Zerops target source repository')
44
- const allowedSourceKeys = ['runId', 'repository', 'commitSha', 'githubInstallationId']
45
- const allowedRepositoryKeys = ['owner', 'name']
46
- if (Object.keys(source).some((key) => !allowedSourceKeys.includes(key))) {
47
- throw new Error('Zerops target source contains an unknown field')
48
- }
49
- if (Object.keys(repository).some((key) => !allowedRepositoryKeys.includes(key))) {
50
- throw new Error('Zerops target source repository contains an unknown field')
51
- }
52
- const installation = source['githubInstallationId']
53
- if (installation !== undefined && (typeof installation !== 'number' || !Number.isSafeInteger(installation) || installation <= 0)) {
54
- throw new Error('Zerops target source githubInstallationId must be a positive safe integer')
55
- }
56
- const commitSha = requiredString(source, 'commitSha')
57
- if (!IMMUTABLE_GIT_OBJECT.test(commitSha)) {
58
- throw new Error('Zerops target source commitSha must be an exact lowercase Git object id')
59
- }
60
- return {
61
- runId: requiredString(source, 'runId'),
62
- repository: {
63
- owner: requiredString(repository, 'owner'),
64
- name: requiredString(repository, 'name'),
65
- },
66
- commitSha,
67
- ...(installation === undefined ? {} : { githubInstallationId: installation }),
68
- }
69
- }
70
-
71
- export const zeropsTargetCodec: ProviderCodec<ZeropsRuntimeTarget> = {
72
- version: 2,
73
- encode: (target) => ({
74
- projectId: target.projectId,
75
- serviceId: target.serviceId,
76
- accessToken: target.accessToken,
77
- ...(target.source === undefined
78
- ? {}
79
- : {
80
- source: {
81
- runId: target.source.runId,
82
- repository: { owner: target.source.repository.owner, name: target.source.repository.name },
83
- commitSha: target.source.commitSha,
84
- ...(target.source.githubInstallationId === undefined ? {} : { githubInstallationId: target.source.githubInstallationId }),
85
- },
86
- }),
87
- ...(target.apiBaseUrl !== undefined ? { apiBaseUrl: target.apiBaseUrl } : {}),
88
- ...(target.propustkaUrl !== undefined ? { propustkaUrl: target.propustkaUrl } : {}),
89
- ...(target.adminKey !== undefined ? { adminKey: target.adminKey } : {}),
90
- }),
91
- decode: (payload) => {
92
- const target = object(payload, 'Zerops target')
93
- const allowedKeys = ['projectId', 'serviceId', 'accessToken', 'source', 'apiBaseUrl', 'propustkaUrl', 'adminKey']
94
- if (Object.keys(target).some((key) => !allowedKeys.includes(key))) {
95
- throw new Error('Zerops target contains an unknown field')
96
- }
97
- const source = runtimeSource(payload)
98
- return {
99
- projectId: requiredString(payload, 'projectId'),
100
- serviceId: requiredString(payload, 'serviceId'),
101
- accessToken: requiredString(payload, 'accessToken'),
102
- ...(source === undefined ? {} : { source }),
103
- ...(optionalString(payload, 'apiBaseUrl') !== undefined ? { apiBaseUrl: optionalString(payload, 'apiBaseUrl') } : {}),
104
- ...(optionalString(payload, 'propustkaUrl') !== undefined ? { propustkaUrl: optionalString(payload, 'propustkaUrl') } : {}),
105
- ...(optionalString(payload, 'adminKey') !== undefined ? { adminKey: optionalString(payload, 'adminKey') } : {}),
106
- }
107
- },
108
- }
@@ -1,209 +0,0 @@
1
- import type { JsonValue, ProviderDeploymentNamespace, ProviderNamespacePlan } from '@fabrika/provider-contract'
2
- import type { ZeropsCliArgs } from './cli-args'
3
- import { createZeropsNamespaceOperator, type ZeropsNamespacePostgres, zeropsNamespaceTargetCodec } from './namespace'
4
-
5
- export interface ZeropsNamespaceCommandDependencies {
6
- readonly source: Readonly<Record<string, string | undefined>>
7
- readonly fetch: (input: string | URL | Request, init?: RequestInit) => Promise<Response>
8
- readonly write: (value: string) => void
9
- }
10
-
11
- const required = (value: string | undefined, label: string): string => {
12
- if (value === undefined || value.trim() === '') {
13
- throw new Error(`${label} is required`)
14
- }
15
- return value.trim()
16
- }
17
-
18
- const postgres = (args: ZeropsCliArgs): ZeropsNamespacePostgres | undefined => {
19
- const type = args.postgresType
20
- const profile = args.postgresProfile
21
- if (type === undefined && profile === undefined) return undefined
22
- if (type === undefined) {
23
- throw new Error('--postgres-type is required with --postgres-profile')
24
- }
25
- if (type === 'postgresql:ha@18') {
26
- if (
27
- profile !== undefined
28
- && profile !== 'oltp-staging'
29
- && profile !== 'oltp-production'
30
- && profile !== 'oltp-enterprise'
31
- && profile !== 'olap-production'
32
- && profile !== 'writeheavy-production'
33
- ) {
34
- throw new Error(`--postgres-profile ${profile} is not supported by ${type}`)
35
- }
36
- return profile === undefined ? { type } : { type, profile }
37
- }
38
- if (type === 'postgresql:single@18') {
39
- if (
40
- profile !== undefined
41
- && profile !== 'oltp-hobby'
42
- && profile !== 'oltp-staging'
43
- && profile !== 'oltp-production'
44
- && profile !== 'olap-production'
45
- && profile !== 'writeheavy-production'
46
- ) {
47
- throw new Error(`--postgres-profile ${profile} is not supported by ${type}`)
48
- }
49
- return profile === undefined ? { type } : { type, profile }
50
- }
51
- throw new Error('--postgres-type must be postgresql:ha@18 or postgresql:single@18')
52
- }
53
-
54
- const planOptions = (args: ZeropsCliArgs): { [key: string]: JsonValue } | undefined => {
55
- const values: { [key: string]: JsonValue } = {}
56
- if (args.projectName !== undefined) values['projectName'] = required(args.projectName, '--project-name')
57
- if (args.corePackage !== undefined) values['corePackage'] = required(args.corePackage, '--core-package')
58
- if (args.publicAccess !== undefined) values['publicAccess'] = required(args.publicAccess, '--public-access')
59
- const database = postgres(args)
60
- if (database !== undefined) {
61
- values['postgres'] = database.profile === undefined ? { type: database.type } : { type: database.type, profile: database.profile }
62
- }
63
- return Object.keys(values).length === 0 ? undefined : values
64
- }
65
-
66
- const namespacePlan = (
67
- args: ZeropsCliArgs,
68
- source: Readonly<Record<string, string | undefined>>,
69
- ): ProviderNamespacePlan => {
70
- // `FABRIKA_ZEROPS_*` because Zerops reserves the bare `ZEROPS_` prefix and refuses to store a custom
71
- // variable using it (`userDataZeropsPrefixForbidden`).
72
- const proxyBuildFromGit = required(
73
- args.proxyBuildFromGit ?? source['FABRIKA_ZEROPS_PROXY_BUILD_FROM_GIT'],
74
- '--proxy-build-from-git or FABRIKA_ZEROPS_PROXY_BUILD_FROM_GIT',
75
- )
76
- const operator = createZeropsNamespaceOperator({ proxyBuildFromGit })
77
- const options = planOptions(args)
78
- return operator.plan({
79
- id: required(args.namespaceId, '--id'),
80
- env: required(args.env, '--env'),
81
- preset: required(args.preset, '--preset'),
82
- ...(args.exclusiveAppId === undefined ? {} : { exclusiveAppId: required(args.exclusiveAppId, '--exclusive-app') }),
83
- ...(options === undefined ? {} : { options }),
84
- })
85
- }
86
-
87
- const outputJson = (value: unknown): string => `${JSON.stringify(value, null, '\t')}\n`
88
-
89
- const responseText = async (response: Response): Promise<string> => {
90
- const text = await response.text()
91
- if (response.ok) return text.trim() === '' ? 'null' : text
92
- let message = `Control API request failed (${response.status})`
93
- try {
94
- const body: unknown = JSON.parse(text)
95
- if (typeof body === 'object' && body !== null) {
96
- const error = Reflect.get(body, 'error')
97
- if (typeof error === 'string' && error !== '') message = error
98
- }
99
- } catch {
100
- if (text.trim() !== '' && text.length < 500) message = text
101
- }
102
- throw new Error(message)
103
- }
104
-
105
- const controlCoordinates = (
106
- args: ZeropsCliArgs,
107
- source: Readonly<Record<string, string | undefined>>,
108
- ): { baseUrl: URL; token: string } => {
109
- const value = required(args.controlUrl ?? source['FABRIKA_CONTROL_URL'], '--control-url or FABRIKA_CONTROL_URL')
110
- const token = required(source['FABRIKA_CONTROL_TOKEN'], 'FABRIKA_CONTROL_TOKEN')
111
- let baseUrl: URL
112
- try {
113
- baseUrl = new URL(value)
114
- } catch {
115
- throw new Error('--control-url or FABRIKA_CONTROL_URL must be an absolute URL')
116
- }
117
- if (baseUrl.protocol !== 'https:' && baseUrl.protocol !== 'http:') {
118
- throw new Error('--control-url or FABRIKA_CONTROL_URL must use http or https')
119
- }
120
- return { baseUrl, token }
121
- }
122
-
123
- const callControl = async (
124
- deps: ZeropsNamespaceCommandDependencies,
125
- coordinates: { baseUrl: URL; token: string },
126
- path: string,
127
- body?: unknown,
128
- ): Promise<void> => {
129
- const response = await deps.fetch(new URL(path, coordinates.baseUrl), {
130
- method: 'POST',
131
- headers: {
132
- accept: 'application/json',
133
- authorization: `Bearer ${coordinates.token}`,
134
- ...(body === undefined ? {} : { 'content-type': 'application/json' }),
135
- },
136
- ...(body === undefined ? {} : { body: JSON.stringify(body) }),
137
- })
138
- const text = await responseText(response)
139
- let parsed: unknown
140
- try {
141
- parsed = JSON.parse(text)
142
- } catch {
143
- parsed = text
144
- }
145
- deps.write(outputJson(parsed))
146
- }
147
-
148
- const createBody = (namespace: ProviderDeploymentNamespace): object => ({
149
- id: namespace.id,
150
- env: namespace.env,
151
- ...(namespace.exclusiveAppId === undefined ? {} : { exclusiveAppId: namespace.exclusiveAppId }),
152
- target: namespace.target,
153
- })
154
-
155
- const adoptBody = (namespace: ProviderDeploymentNamespace): object => ({
156
- env: namespace.env,
157
- ...(namespace.exclusiveAppId === undefined ? {} : { exclusiveAppId: namespace.exclusiveAppId }),
158
- target: namespace.target,
159
- })
160
-
161
- /** Execute one namespace command. All command inputs are validated before the first fetch. */
162
- export const runZeropsNamespaceCommand = async (
163
- args: ZeropsCliArgs,
164
- deps: ZeropsNamespaceCommandDependencies,
165
- ): Promise<void> => {
166
- const subcommand = required(args.subcommand, 'namespace command')
167
- if (subcommand === 'plan') {
168
- deps.write(outputJson(namespacePlan(args, deps.source)))
169
- return
170
- }
171
- if (subcommand === 'reconcile') {
172
- const id = required(args.namespaceId, '--id')
173
- const control = controlCoordinates(args, deps.source)
174
- await callControl(deps, control, `/api/namespaces/${encodeURIComponent(id)}/reconcile`)
175
- return
176
- }
177
- if (subcommand !== 'create' && subcommand !== 'adopt') {
178
- throw new Error(`Unknown namespace command: ${subcommand}`)
179
- }
180
-
181
- const planned = namespacePlan(args, deps.source)
182
- const control = controlCoordinates(args, deps.source)
183
- if (subcommand === 'create') {
184
- await callControl(deps, control, '/api/namespaces', createBody(planned.namespace))
185
- return
186
- }
187
-
188
- const projectId = required(args.projectId, '--project-id')
189
- const decoded = zeropsNamespaceTargetCodec.decode(planned.namespace.target.payload)
190
- const namespace: ProviderDeploymentNamespace = {
191
- ...planned.namespace,
192
- target: {
193
- provider: 'zerops',
194
- version: zeropsNamespaceTargetCodec.version,
195
- payload: zeropsNamespaceTargetCodec.encode({
196
- ...decoded,
197
- managed: false,
198
- projectId,
199
- ready: false,
200
- }),
201
- },
202
- }
203
- await callControl(
204
- deps,
205
- control,
206
- `/api/namespaces/${encodeURIComponent(namespace.id)}/adopt`,
207
- adoptBody(namespace),
208
- )
209
- }