@homeflare/alchemy 0.35.0 → 0.37.0
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/README.md +14 -0
- package/dist/argocd/cluster.d.ts +102 -0
- package/dist/argocd/cluster.d.ts.map +1 -0
- package/dist/argocd/credentials.d.ts +37 -0
- package/dist/argocd/credentials.d.ts.map +1 -0
- package/dist/argocd/fake-argocd.d.ts +42 -0
- package/dist/argocd/fake-argocd.d.ts.map +1 -0
- package/dist/argocd/git-credentials.d.ts +29 -0
- package/dist/argocd/git-credentials.d.ts.map +1 -0
- package/dist/argocd/index.d.ts +19 -0
- package/dist/argocd/index.d.ts.map +1 -0
- package/dist/argocd/index.js +306 -0
- package/dist/argocd/index.js.map +17 -0
- package/dist/argocd/providers.d.ts +13 -0
- package/dist/argocd/providers.d.ts.map +1 -0
- package/dist/argocd/repo-creds.d.ts +67 -0
- package/dist/argocd/repo-creds.d.ts.map +1 -0
- package/dist/argocd/repository.d.ts +87 -0
- package/dist/argocd/repository.d.ts.map +1 -0
- package/dist/argocd/resource.d.ts +90 -0
- package/dist/argocd/resource.d.ts.map +1 -0
- package/dist/cloudflare/index.js +4 -4
- package/dist/cloudflare/index.js.map +3 -3
- package/dist/cloudflare/r2-bucket-lock.d.ts +14 -5
- package/dist/cloudflare/r2-bucket-lock.d.ts.map +1 -1
- package/dist/grafana/index.d.ts +5 -4
- package/dist/grafana/index.d.ts.map +1 -1
- package/dist/grafana/index.js +204 -90
- package/dist/grafana/index.js.map +7 -4
- package/dist/grafana/notification-policy-drop-warning.d.ts +27 -0
- package/dist/grafana/notification-policy-drop-warning.d.ts.map +1 -0
- package/dist/grafana/notification-policy-fixtures.d.ts +34 -0
- package/dist/grafana/notification-policy-fixtures.d.ts.map +1 -0
- package/dist/grafana/notification-policy-model.d.ts +53 -0
- package/dist/grafana/notification-policy-model.d.ts.map +1 -0
- package/dist/grafana/notification-policy.d.ts +57 -0
- package/dist/grafana/notification-policy.d.ts.map +1 -0
- package/dist/grafana/providers.d.ts +1 -1
- package/dist/grafana/providers.d.ts.map +1 -1
- package/dist/litellm/index.js.map +2 -2
- package/dist/litellm/operations.d.ts.map +1 -1
- package/dist/proxmox/ceph-daemon-form.d.ts +4 -5
- package/dist/proxmox/ceph-daemon-form.d.ts.map +1 -1
- package/dist/proxmox/ceph-daemon-wire.d.ts +33 -0
- package/dist/proxmox/ceph-daemon-wire.d.ts.map +1 -0
- package/dist/proxmox/ceph-daemon.d.ts +35 -0
- package/dist/proxmox/ceph-daemon.d.ts.map +1 -1
- package/dist/proxmox/ceph-fs-distilled.d.ts +14 -0
- package/dist/proxmox/ceph-fs-distilled.d.ts.map +1 -0
- package/dist/proxmox/ceph-fs-wire.d.ts +20 -11
- package/dist/proxmox/ceph-fs-wire.d.ts.map +1 -1
- package/dist/proxmox/ceph-fs.d.ts +31 -6
- package/dist/proxmox/ceph-fs.d.ts.map +1 -1
- package/dist/proxmox/ceph-osd-tree.d.ts +12 -3
- package/dist/proxmox/ceph-osd-tree.d.ts.map +1 -1
- package/dist/proxmox/ceph-osd-write.d.ts +17 -2
- package/dist/proxmox/ceph-osd-write.d.ts.map +1 -1
- package/dist/proxmox/ceph-osd.d.ts +6 -0
- package/dist/proxmox/ceph-osd.d.ts.map +1 -1
- package/dist/proxmox/ceph-pool-settle.d.ts +10 -4
- package/dist/proxmox/ceph-pool-settle.d.ts.map +1 -1
- package/dist/proxmox/ceph-pool-wire.d.ts +46 -0
- package/dist/proxmox/ceph-pool-wire.d.ts.map +1 -0
- package/dist/proxmox/ceph-pool.d.ts +10 -5
- package/dist/proxmox/ceph-pool.d.ts.map +1 -1
- package/dist/proxmox/group-wire.d.ts +1 -1
- package/dist/proxmox/group-wire.d.ts.map +1 -1
- package/dist/proxmox/index.js +611 -477
- package/dist/proxmox/index.js.map +15 -12
- package/dist/proxmox/storage-wire.d.ts +8 -1
- package/dist/proxmox/storage-wire.d.ts.map +1 -1
- package/dist/proxmox/user-wire.d.ts +1 -1
- package/dist/proxmox/user-wire.d.ts.map +1 -1
- package/docs/argocd-kubernetes.md +100 -0
- package/docs/argocd.md +139 -0
- package/docs/credentials.md +9 -0
- package/docs/grafana-alerting.md +3 -2
- package/docs/grafana-notification-policy.md +127 -0
- package/docs/grafana.md +12 -8
- package/docs/litellm.md +17 -0
- package/docs/peers.md +7 -0
- package/docs/unifi.md +45 -0
- package/docs/upstream-conformance.md +19 -5
- package/package.json +8 -3
- package/src/argocd/cluster.test.ts +120 -0
- package/src/argocd/cluster.ts +222 -0
- package/src/argocd/credentials.ts +38 -0
- package/src/argocd/fake-argocd.ts +60 -0
- package/src/argocd/git-credentials.ts +71 -0
- package/src/argocd/index.ts +40 -0
- package/src/argocd/providers.ts +17 -0
- package/src/argocd/repo-creds.test.ts +123 -0
- package/src/argocd/repo-creds.ts +115 -0
- package/src/argocd/repository.test.ts +167 -0
- package/src/argocd/repository.ts +165 -0
- package/src/argocd/resource.ts +150 -0
- package/src/cloudflare/r2-bucket-lock.test.ts +54 -18
- package/src/cloudflare/r2-bucket-lock.ts +25 -18
- package/src/grafana/index.ts +11 -4
- package/src/grafana/notification-policy-drop-warning.test.ts +163 -0
- package/src/grafana/notification-policy-drop-warning.ts +60 -0
- package/src/grafana/notification-policy-fixtures.ts +27 -0
- package/src/grafana/notification-policy-model.test.ts +104 -0
- package/src/grafana/notification-policy-model.ts +100 -0
- package/src/grafana/notification-policy-refusals.test.ts +119 -0
- package/src/grafana/notification-policy.test.ts +86 -0
- package/src/grafana/notification-policy.ts +191 -0
- package/src/grafana/providers.ts +2 -0
- package/src/litellm/operations.ts +8 -4
- package/src/proxmox/ceph-daemon-form.ts +7 -8
- package/src/proxmox/ceph-daemon-wire.ts +115 -0
- package/src/proxmox/ceph-daemon-write.test.ts +158 -0
- package/src/proxmox/ceph-daemon.ts +65 -47
- package/src/proxmox/ceph-fs-delete.test.ts +81 -0
- package/src/proxmox/ceph-fs-distilled.ts +102 -0
- package/src/proxmox/ceph-fs-wire.ts +17 -31
- package/src/proxmox/ceph-fs-write.test.ts +67 -0
- package/src/proxmox/ceph-fs.ts +66 -69
- package/src/proxmox/ceph-osd-tree.ts +14 -5
- package/src/proxmox/ceph-osd-write.test.ts +61 -0
- package/src/proxmox/ceph-osd-write.ts +33 -6
- package/src/proxmox/ceph-osd.ts +6 -0
- package/src/proxmox/ceph-pool-settle.ts +13 -7
- package/src/proxmox/ceph-pool-wire.ts +110 -0
- package/src/proxmox/ceph-pool-write.test.ts +145 -0
- package/src/proxmox/ceph-pool.ts +87 -98
- package/src/proxmox/distilled-task-await.test.ts +0 -124
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@homeflare/alchemy",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.0",
|
|
4
4
|
"description": "Custom Alchemy providers for Cloudflare, Proxmox, OpenBao, Forgejo, Talos, launchd and Caddy, and a verifier that proves adoptions are no-ops.",
|
|
5
5
|
"homepage": "https://github.com/taslabs-net/homeflare-kit/tree/main/packages/alchemy#readme",
|
|
6
6
|
"bugs": "https://github.com/taslabs-net/homeflare-kit/issues",
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
"types": "./dist/index.d.ts",
|
|
29
29
|
"default": "./dist/index.js"
|
|
30
30
|
},
|
|
31
|
+
"./argocd": {
|
|
32
|
+
"types": "./dist/argocd/index.d.ts",
|
|
33
|
+
"default": "./dist/argocd/index.js"
|
|
34
|
+
},
|
|
31
35
|
"./cloudflare": {
|
|
32
36
|
"types": "./dist/cloudflare/index.d.ts",
|
|
33
37
|
"default": "./dist/cloudflare/index.js"
|
|
@@ -120,11 +124,12 @@
|
|
|
120
124
|
"@distilled.cloud/netbox": "npm:@homeflare/distilled-netbox@0.3.0",
|
|
121
125
|
"@distilled.cloud/opnsense": "npm:@homeflare/distilled-opnsense@0.2.0",
|
|
122
126
|
"@distilled.cloud/paperless-ngx": "npm:@homeflare/distilled-paperless-ngx@0.3.0",
|
|
123
|
-
"@distilled.cloud/proxmox": "npm:@homeflare/distilled-proxmox@0.
|
|
124
|
-
"@distilled.cloud/proxmox-backup": "npm:@homeflare/distilled-proxmox-backup@0.
|
|
127
|
+
"@distilled.cloud/proxmox": "npm:@homeflare/distilled-proxmox@0.3.0",
|
|
128
|
+
"@distilled.cloud/proxmox-backup": "npm:@homeflare/distilled-proxmox-backup@0.3.0",
|
|
125
129
|
"@distilled.cloud/unifi-network": "npm:@homeflare/distilled-unifi-network@0.2.0"
|
|
126
130
|
},
|
|
127
131
|
"peerDependencies": {
|
|
132
|
+
"@distilled.cloud/argocd": "1.0.0-rc.12",
|
|
128
133
|
"@distilled.cloud/cloudflare": "1.0.0-rc.12",
|
|
129
134
|
"@distilled.cloud/discord": "1.0.0-rc.12",
|
|
130
135
|
"@distilled.cloud/forgejo": "1.0.0-rc.12",
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Argocd.Cluster`'s `spec` against a fake Argo CD — the same three doctrine-required shapes as
|
|
3
|
+
* `repository.test.ts`, plus the nested `config.tlsClientConfig` credential path.
|
|
4
|
+
*/
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
6
|
+
import * as Effect from 'effect/Effect';
|
|
7
|
+
import { fakeArgocd, fakeArgocdLayer, fakeFailure } from './fake-argocd.ts';
|
|
8
|
+
import { argocdOperations } from './resource.ts';
|
|
9
|
+
import { spec } from './cluster.ts';
|
|
10
|
+
|
|
11
|
+
const SERVER = 'https://talos.example.com:6443';
|
|
12
|
+
const ENV_VAR = 'ARGOCD_CLUSTER_TALOS_BEARER_TOKEN';
|
|
13
|
+
|
|
14
|
+
const liveCluster = (overrides: Record<string, unknown> = {}) =>
|
|
15
|
+
Response.json({ name: 'talos', server: SERVER, ...overrides });
|
|
16
|
+
|
|
17
|
+
describe('Argocd.Cluster spec.fetchLive', () => {
|
|
18
|
+
test('a live cluster decodes into attributes, including nested tls config', async () => {
|
|
19
|
+
const fake = fakeArgocd((method) =>
|
|
20
|
+
method === 'GET'
|
|
21
|
+
? liveCluster({
|
|
22
|
+
config: { tlsClientConfig: { insecure: false, serverName: 'talos.internal' } },
|
|
23
|
+
})
|
|
24
|
+
: fakeFailure(404, 'not found'),
|
|
25
|
+
);
|
|
26
|
+
const live = await Effect.runPromise(
|
|
27
|
+
spec.fetchLive({ server: SERVER }).pipe(Effect.provide(fakeArgocdLayer(fake.fetch))),
|
|
28
|
+
);
|
|
29
|
+
expect(live?.server).toBe(SERVER);
|
|
30
|
+
expect(live?.config?.tlsClientConfig?.serverName).toBe('talos.internal');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
test('a 404 folds to undefined', async () => {
|
|
34
|
+
const fake = fakeArgocd(() => fakeFailure(404, 'cluster not found'));
|
|
35
|
+
const result = await Effect.runPromise(
|
|
36
|
+
spec.fetchLive({ server: SERVER }).pipe(Effect.provide(fakeArgocdLayer(fake.fetch))),
|
|
37
|
+
);
|
|
38
|
+
expect(result).toBeUndefined();
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
test('a 403 propagates — not folded to absent', async () => {
|
|
42
|
+
const fake = fakeArgocd(() => fakeFailure(403, 'forbidden'));
|
|
43
|
+
const failure = await Effect.runPromise(
|
|
44
|
+
Effect.flip(
|
|
45
|
+
spec.fetchLive({ server: SERVER }).pipe(Effect.provide(fakeArgocdLayer(fake.fetch))),
|
|
46
|
+
),
|
|
47
|
+
);
|
|
48
|
+
expect(failure._tag).toBe('Forbidden');
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('Argocd.Cluster destroy — idempotent (S11)', () => {
|
|
53
|
+
test('deleting an already-absent cluster makes no DELETE call', async () => {
|
|
54
|
+
const fake = fakeArgocd(() => fakeFailure(404, 'cluster not found'));
|
|
55
|
+
await Effect.runPromise(
|
|
56
|
+
argocdOperations(spec)
|
|
57
|
+
.destroy({ server: SERVER })
|
|
58
|
+
.pipe(Effect.provide(fakeArgocdLayer(fake.fetch))),
|
|
59
|
+
);
|
|
60
|
+
expect(fake.seen.every((call) => call.method !== 'DELETE')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
test('the DELETE call itself returning 404 (a race with an external removal) still succeeds', async () => {
|
|
64
|
+
// ⚠️ fetchLive's pre-check finds the cluster live, but something else deregisters it before
|
|
65
|
+
// this call's own DELETE lands — matching upstream's Hetzner/Certificate.ts `deleteById`.
|
|
66
|
+
const fake = fakeArgocd((method) =>
|
|
67
|
+
method === 'GET' ? liveCluster() : fakeFailure(404, 'cluster not found'),
|
|
68
|
+
);
|
|
69
|
+
await Effect.runPromise(
|
|
70
|
+
argocdOperations(spec)
|
|
71
|
+
.destroy({ server: SERVER })
|
|
72
|
+
.pipe(Effect.provide(fakeArgocdLayer(fake.fetch))),
|
|
73
|
+
);
|
|
74
|
+
expect(fake.seen.filter((call) => call.method === 'DELETE')).toHaveLength(1);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
describe('Argocd.Cluster spec.upsert — write-only bearer token', () => {
|
|
79
|
+
beforeEach(() => {
|
|
80
|
+
delete process.env[ENV_VAR];
|
|
81
|
+
});
|
|
82
|
+
afterEach(() => {
|
|
83
|
+
delete process.env[ENV_VAR];
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test('refuses with a typed domain error when the declared env var is unset', async () => {
|
|
87
|
+
const fake = fakeArgocd(() => fakeFailure(404, 'unused'));
|
|
88
|
+
const failure = await Effect.runPromise(
|
|
89
|
+
Effect.flip(
|
|
90
|
+
spec
|
|
91
|
+
.upsert({ credentials: { bearerToken: { fromEnv: ENV_VAR } }, server: SERVER })
|
|
92
|
+
.pipe(Effect.provide(fakeArgocdLayer(fake.fetch))),
|
|
93
|
+
),
|
|
94
|
+
);
|
|
95
|
+
expect(failure._tag).toBe('ArgocdClusterSecretEnvUnsetError');
|
|
96
|
+
expect(fake.seen).toEqual([]);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('resolves the bearer token into config, never the FromEnv reference itself', async () => {
|
|
100
|
+
process.env[ENV_VAR] = 'kube-bearer-abc123';
|
|
101
|
+
const fake = fakeArgocd((method) =>
|
|
102
|
+
method === 'POST' ? Response.json({ server: SERVER }) : fakeFailure(404, 'x'),
|
|
103
|
+
);
|
|
104
|
+
await Effect.runPromise(
|
|
105
|
+
spec
|
|
106
|
+
.upsert({
|
|
107
|
+
credentials: { bearerToken: { fromEnv: ENV_VAR } },
|
|
108
|
+
namespaces: ['argocd', 'apps'],
|
|
109
|
+
server: SERVER,
|
|
110
|
+
})
|
|
111
|
+
.pipe(Effect.provide(fakeArgocdLayer(fake.fetch))),
|
|
112
|
+
);
|
|
113
|
+
const [create] = fake.seen.filter((call) => call.method === 'POST');
|
|
114
|
+
expect(create?.path).toContain('upsert=true');
|
|
115
|
+
const body = create?.body as { config?: { bearerToken?: string }; namespaces?: string[] };
|
|
116
|
+
expect(body.config?.bearerToken).toBe('kube-bearer-abc123');
|
|
117
|
+
expect(body.namespaces).toEqual(['argocd', 'apps']);
|
|
118
|
+
expect(JSON.stringify(body)).not.toContain('fromEnv');
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `Argocd.Cluster` — a target Kubernetes cluster registered with an Argo CD instance
|
|
3
|
+
* (`/api/v1/clusters`) so Applications can deploy onto it. REST-managed, not a CRD; see
|
|
4
|
+
* `docs/argocd.md`. This is how the house will register the Talos-on-PVE cluster itself with
|
|
5
|
+
* Argo CD once both exist — Argo CD's own control-plane cluster (where Argo CD is installed) is
|
|
6
|
+
* registered implicitly as `https://kubernetes.default.svc` and never needs this resource.
|
|
7
|
+
*
|
|
8
|
+
* ⚠️ ONLY BEARER-TOKEN AND STATIC TLS CLIENT-CERT AUTH ARE SUPPORTED. `V1alpha1ClusterConfig` also
|
|
9
|
+
* carries `awsAuthConfig` (EKS IAM) and `execProviderConfig` (an exec plugin) — measured in
|
|
10
|
+
* `services/argocd.ts`, distilled homeflare/base — neither of which applies to a Talos cluster;
|
|
11
|
+
* left unbuilt rather than modeled and untested.
|
|
12
|
+
*
|
|
13
|
+
* ⚠️ NO LIVE ARGO CD INSTANCE EXISTS ON THE ESTATE (2026-09-24) — see `docs/argocd.md`.
|
|
14
|
+
*
|
|
15
|
+
* ⛔ `defaultRemovalPolicy: 'retain'` — deregistering a cluster Applications still target breaks
|
|
16
|
+
* their sync. `delete` below is fully implemented; opt in with `.pipe(RemovalPolicy.destroy())`.
|
|
17
|
+
*/
|
|
18
|
+
import { Resource } from 'alchemy';
|
|
19
|
+
import * as Provider from 'alchemy/Provider';
|
|
20
|
+
import * as argocd from '@distilled.cloud/argocd/argocd';
|
|
21
|
+
import * as Data from 'effect/Data';
|
|
22
|
+
import * as Effect from 'effect/Effect';
|
|
23
|
+
import { type FromEnv, resolveAll } from '../secrets/write-only.ts';
|
|
24
|
+
import { type ArgocdRequirements, type ArgocdSpec, argocdHandlers, present } from './resource.ts';
|
|
25
|
+
|
|
26
|
+
export interface ClusterCredentialRefs {
|
|
27
|
+
readonly password?: FromEnv;
|
|
28
|
+
readonly bearerToken?: FromEnv;
|
|
29
|
+
readonly tlsClientKeyData?: FromEnv;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ClusterProps {
|
|
33
|
+
/** The cluster's API server URL — identity key; changing it is a replace. */
|
|
34
|
+
server: string;
|
|
35
|
+
name?: string;
|
|
36
|
+
/** Restricts this cluster to one Argo CD project; omit for every project. */
|
|
37
|
+
project?: string;
|
|
38
|
+
namespaces?: readonly string[];
|
|
39
|
+
clusterResources?: boolean;
|
|
40
|
+
labels?: Readonly<Record<string, string>>;
|
|
41
|
+
annotations?: Readonly<Record<string, string>>;
|
|
42
|
+
proxyUrl?: string;
|
|
43
|
+
username?: string;
|
|
44
|
+
tlsCaData?: string;
|
|
45
|
+
/** The client certificate itself — public, unlike its key. */
|
|
46
|
+
tlsCertData?: string;
|
|
47
|
+
tlsInsecure?: boolean;
|
|
48
|
+
tlsServerName?: string;
|
|
49
|
+
/** Write-only — never a value here, only env var names. See the module header. */
|
|
50
|
+
credentials?: ClusterCredentialRefs;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export interface ClusterAttributes {
|
|
54
|
+
server: string;
|
|
55
|
+
name: string | undefined;
|
|
56
|
+
project: string | undefined;
|
|
57
|
+
namespaces: readonly string[] | undefined;
|
|
58
|
+
clusterResources: boolean | undefined;
|
|
59
|
+
labels: Readonly<Record<string, string>> | undefined;
|
|
60
|
+
annotations: Readonly<Record<string, string>> | undefined;
|
|
61
|
+
proxyUrl: string | undefined;
|
|
62
|
+
username: string | undefined;
|
|
63
|
+
tlsCaData: string | undefined;
|
|
64
|
+
tlsCertData: string | undefined;
|
|
65
|
+
tlsInsecure: boolean | undefined;
|
|
66
|
+
tlsServerName: string | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ArgocdCluster extends Resource<
|
|
70
|
+
'Argocd.Cluster',
|
|
71
|
+
ClusterProps,
|
|
72
|
+
ClusterAttributes,
|
|
73
|
+
never,
|
|
74
|
+
ArgocdRequirements
|
|
75
|
+
> {}
|
|
76
|
+
|
|
77
|
+
export const ArgocdCluster = Resource<ArgocdCluster>('Argocd.Cluster', {
|
|
78
|
+
defaultRemovalPolicy: 'retain',
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
/** Domain refusal, not a distilled error — a declared credential's env var is unset at call time. */
|
|
82
|
+
export class ArgocdClusterSecretEnvUnsetError extends Data.TaggedError(
|
|
83
|
+
'ArgocdClusterSecretEnvUnsetError',
|
|
84
|
+
)<{
|
|
85
|
+
readonly message: string;
|
|
86
|
+
}> {}
|
|
87
|
+
|
|
88
|
+
interface ResolvedClusterCredentials {
|
|
89
|
+
readonly password?: string;
|
|
90
|
+
readonly bearerToken?: string;
|
|
91
|
+
readonly tlsClientKeyData?: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const resolveClusterCredentials = (
|
|
95
|
+
refs: ClusterCredentialRefs,
|
|
96
|
+
): Effect.Effect<ResolvedClusterCredentials, ArgocdClusterSecretEnvUnsetError> => {
|
|
97
|
+
const declared: Record<string, FromEnv> = {};
|
|
98
|
+
if (refs.password !== undefined) declared.password = refs.password;
|
|
99
|
+
if (refs.bearerToken !== undefined) declared.bearerToken = refs.bearerToken;
|
|
100
|
+
if (refs.tlsClientKeyData !== undefined) declared.tlsClientKeyData = refs.tlsClientKeyData;
|
|
101
|
+
const { values, missing } = resolveAll(declared);
|
|
102
|
+
if (missing.length > 0) {
|
|
103
|
+
return Effect.fail(
|
|
104
|
+
new ArgocdClusterSecretEnvUnsetError({
|
|
105
|
+
message:
|
|
106
|
+
`credential write requires ${missing.join(', ')}, unset or empty in the ` +
|
|
107
|
+
'deploying environment. Export it and deploy again.',
|
|
108
|
+
}),
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
return Effect.succeed(values as ResolvedClusterCredentials);
|
|
112
|
+
};
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* `V1alpha1Cluster.labels`/`.annotations` decode as `{ [key: string]: string | undefined }` — a
|
|
116
|
+
* generated index-signature artifact, not a real possibility on the wire (a label/annotation map
|
|
117
|
+
* never carries an `undefined` value). Cast at this one boundary rather than threading the looser
|
|
118
|
+
* type through `ClusterAttributes`.
|
|
119
|
+
*/
|
|
120
|
+
const attributesOf = (live: argocd.V1alpha1Cluster): ClusterAttributes => ({
|
|
121
|
+
annotations: live.annotations as Record<string, string> | undefined,
|
|
122
|
+
clusterResources: live.clusterResources,
|
|
123
|
+
labels: live.labels as Record<string, string> | undefined,
|
|
124
|
+
name: live.name,
|
|
125
|
+
namespaces: live.namespaces,
|
|
126
|
+
project: live.project,
|
|
127
|
+
proxyUrl: live.config?.proxyUrl,
|
|
128
|
+
server: live.server ?? '',
|
|
129
|
+
tlsCaData: live.config?.tlsClientConfig?.caData,
|
|
130
|
+
tlsCertData: live.config?.tlsClientConfig?.certData,
|
|
131
|
+
tlsInsecure: live.config?.tlsClientConfig?.insecure,
|
|
132
|
+
tlsServerName: live.config?.tlsClientConfig?.serverName,
|
|
133
|
+
username: live.config?.username,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
const sameRecord = (
|
|
137
|
+
a: Readonly<Record<string, string>> | undefined,
|
|
138
|
+
b: Readonly<Record<string, string>> | undefined,
|
|
139
|
+
) =>
|
|
140
|
+
JSON.stringify(Object.entries(a ?? {}).sort()) === JSON.stringify(Object.entries(b ?? {}).sort());
|
|
141
|
+
|
|
142
|
+
const sameList = (a: readonly string[] | undefined, b: readonly string[] | undefined) =>
|
|
143
|
+
JSON.stringify([...(a ?? [])].sort()) === JSON.stringify([...(b ?? [])].sort());
|
|
144
|
+
|
|
145
|
+
/** Only the plain fields — Argo CD never returns the password/bearer token/private key back. */
|
|
146
|
+
const plainMatches = (attrs: ClusterAttributes, props: ClusterProps): boolean =>
|
|
147
|
+
(props.name ?? undefined) === (attrs.name ?? undefined) &&
|
|
148
|
+
(props.project ?? undefined) === (attrs.project ?? undefined) &&
|
|
149
|
+
sameList(props.namespaces, attrs.namespaces) &&
|
|
150
|
+
Boolean(props.clusterResources) === Boolean(attrs.clusterResources) &&
|
|
151
|
+
sameRecord(props.labels, attrs.labels) &&
|
|
152
|
+
sameRecord(props.annotations, attrs.annotations) &&
|
|
153
|
+
(props.proxyUrl ?? undefined) === (attrs.proxyUrl ?? undefined) &&
|
|
154
|
+
(props.username ?? undefined) === (attrs.username ?? undefined) &&
|
|
155
|
+
(props.tlsCaData ?? undefined) === (attrs.tlsCaData ?? undefined) &&
|
|
156
|
+
(props.tlsCertData ?? undefined) === (attrs.tlsCertData ?? undefined) &&
|
|
157
|
+
Boolean(props.tlsInsecure) === Boolean(attrs.tlsInsecure) &&
|
|
158
|
+
(props.tlsServerName ?? undefined) === (attrs.tlsServerName ?? undefined);
|
|
159
|
+
|
|
160
|
+
type ClusterError =
|
|
161
|
+
| argocd.GetClusterServiceError
|
|
162
|
+
| argocd.CreateClusterServiceError
|
|
163
|
+
| argocd.DeleteClusterServiceError
|
|
164
|
+
| ArgocdClusterSecretEnvUnsetError;
|
|
165
|
+
|
|
166
|
+
/** ★ EXPORTED for direct testing against `fake-argocd.ts` — see `cluster.test.ts`. */
|
|
167
|
+
export const spec: ArgocdSpec<
|
|
168
|
+
ClusterProps,
|
|
169
|
+
argocd.V1alpha1Cluster,
|
|
170
|
+
ClusterAttributes,
|
|
171
|
+
ClusterError
|
|
172
|
+
> = {
|
|
173
|
+
attributes: (live) => attributesOf(live),
|
|
174
|
+
// ★ Catches the DELETE call's own NotFound too — closes the TOCTOU race between the pre-check
|
|
175
|
+
// `fetchLive` above and this call. Matches upstream's Hetzner/Certificate.ts `deleteById`.
|
|
176
|
+
destroy: (props) =>
|
|
177
|
+
argocd
|
|
178
|
+
.deleteClusterService({ id_value: props.server })
|
|
179
|
+
.pipe(Effect.catchTag('NotFound', () => Effect.void)),
|
|
180
|
+
identityOfAttributes: (attrs) => attrs.server,
|
|
181
|
+
identityOfProps: (props) => props.server,
|
|
182
|
+
fetchLive: (props) =>
|
|
183
|
+
argocd
|
|
184
|
+
.getClusterService({ id_value: props.server })
|
|
185
|
+
.pipe(Effect.catchTag('NotFound', () => Effect.succeed(undefined))),
|
|
186
|
+
matches: plainMatches,
|
|
187
|
+
upsert: (props) =>
|
|
188
|
+
resolveClusterCredentials(props.credentials ?? {}).pipe(
|
|
189
|
+
Effect.flatMap((creds) =>
|
|
190
|
+
argocd.createClusterService(
|
|
191
|
+
present({
|
|
192
|
+
upsert: true,
|
|
193
|
+
annotations: props.annotations,
|
|
194
|
+
clusterResources: props.clusterResources,
|
|
195
|
+
config: present({
|
|
196
|
+
bearerToken: creds.bearerToken,
|
|
197
|
+
password: creds.password,
|
|
198
|
+
proxyUrl: props.proxyUrl,
|
|
199
|
+
tlsClientConfig: present({
|
|
200
|
+
caData: props.tlsCaData,
|
|
201
|
+
certData: props.tlsCertData,
|
|
202
|
+
insecure: props.tlsInsecure,
|
|
203
|
+
keyData: creds.tlsClientKeyData,
|
|
204
|
+
serverName: props.tlsServerName,
|
|
205
|
+
}),
|
|
206
|
+
username: props.username,
|
|
207
|
+
}),
|
|
208
|
+
labels: props.labels,
|
|
209
|
+
name: props.name,
|
|
210
|
+
namespaces: props.namespaces === undefined ? undefined : [...props.namespaces],
|
|
211
|
+
project: props.project,
|
|
212
|
+
server: props.server,
|
|
213
|
+
}),
|
|
214
|
+
),
|
|
215
|
+
),
|
|
216
|
+
),
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
export const handlers = argocdHandlers(spec);
|
|
220
|
+
|
|
221
|
+
export const ArgocdClusterProvider = () =>
|
|
222
|
+
Provider.effect(ArgocdCluster, Effect.succeed(ArgocdCluster.Provider.of(handlers)));
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argo CD credentials — a bearer-token REFERENCE, resolved lazily, never a prop.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ NO TOKEN IS EVER A PROP OR AN ATTRIBUTE. Alchemy persists Attributes unencrypted
|
|
5
|
+
* (State/StateEncoding.ts@v2.0.0-beta.79#encodeState), so nothing a stack file holds may be a
|
|
6
|
+
* credential. `@distilled.cloud/argocd/Credentials` already ships exactly the shape this asks
|
|
7
|
+
* for: `Credentials` is a lazy `Context.Service<Credentials, Effect.Effect<Config>>`, resolved
|
|
8
|
+
* on the calling fiber inside each operation (`ArgocdProtocol`'s `credentials:` field), never at
|
|
9
|
+
* layer build. This file re-exports it rather than re-implementing it — the same choice
|
|
10
|
+
* `../discord/credentials.ts` and `../forgejo` (via `@distilled.cloud/forgejo/Credentials`) made.
|
|
11
|
+
*
|
|
12
|
+
* ★ `CredentialsFromEnv` reads `ARGOCD_TOKEN` (required) and `ARGOCD_SERVER` (optional instance
|
|
13
|
+
* origin, default `https://localhost:8080`) — the distilled package's own names, unchanged here
|
|
14
|
+
* so a consumer never has to guess which var this family reads.
|
|
15
|
+
*
|
|
16
|
+
* ⚠️ NO `credentials(...)` FACTORY EXISTS ON THIS SDK, unlike Discord/Grafana's — measured against
|
|
17
|
+
* `packages/argocd/src/credentials.ts` (distilled homeflare/base): only `fromToken({ token,
|
|
18
|
+
* apiBaseUrl? })` and `CredentialsFromEnv`. `fake-argocd.ts` builds its test layer from
|
|
19
|
+
* `fromToken` directly.
|
|
20
|
+
*
|
|
21
|
+
* ⚠️ NO OpenBao MINT YET, same as every other family in this kit today (`Alchemy.Stack({ secrets
|
|
22
|
+
* })` is unreleased, PR 1728, `main`-only). A deploy process exports `ARGOCD_TOKEN` itself —
|
|
23
|
+
* this package never reads it from a file.
|
|
24
|
+
*
|
|
25
|
+
* ⚠️ NO LIVE ARGO CD INSTANCE EXISTS ON THE ESTATE YET (2026-09-24) — this family is built ahead
|
|
26
|
+
* of the Talos-on-PVE cluster it targets, per Tim's "get the providers ready even if we don't
|
|
27
|
+
* use it yet but we know we will." Nothing here has been exercised against a real server; every
|
|
28
|
+
* test in this family runs against `fake-argocd.ts`.
|
|
29
|
+
*/
|
|
30
|
+
import type { Config } from '@distilled.cloud/argocd/Credentials';
|
|
31
|
+
import { Credentials, CredentialsFromEnv, fromToken } from '@distilled.cloud/argocd/Credentials';
|
|
32
|
+
|
|
33
|
+
/** Re-exported so a consumer never has to reach into `@distilled.cloud/argocd` by hand. */
|
|
34
|
+
export { Credentials, CredentialsFromEnv, fromToken };
|
|
35
|
+
export type { Config as ArgocdCredentialsConfig };
|
|
36
|
+
|
|
37
|
+
/** `ArgocdCredentials.fromEnv()` — the layer every `argocdProviders()` stack uses by default. */
|
|
38
|
+
export const fromEnv = () => CredentialsFromEnv;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A minimal in-memory Argo CD REST API for this family's tests — an in-memory `fetch` handed to
|
|
3
|
+
* Effect's real `FetchHttpClient` through its `Fetch` reference, mirroring `../discord/fake-discord.ts`
|
|
4
|
+
* (itself mirroring `../forgejo/fake-forgejo.ts` and `cloudflare/fake-mesh.ts`). Tests exercise
|
|
5
|
+
* distilled's REAL path assembly, JSON encode/decode and status→typed-error matching — nothing
|
|
6
|
+
* about any of that is re-implemented here, only the wire responses a scenario needs.
|
|
7
|
+
*
|
|
8
|
+
* ⛔ TEST-ONLY, AND NO NETWORK. No provider imports this file. The base URL is RFC 2606
|
|
9
|
+
* `argocd.example.com`, and the token is a placeholder. NO LIVE ARGO CD INSTANCE EXISTS ON THE
|
|
10
|
+
* ESTATE (2026-09-24) — this is the only server any test in this family talks to.
|
|
11
|
+
*/
|
|
12
|
+
import { type Credentials, fromToken } from '@distilled.cloud/argocd/Credentials';
|
|
13
|
+
import * as Layer from 'effect/Layer';
|
|
14
|
+
import * as FetchHttpClient from 'effect/unstable/http/FetchHttpClient';
|
|
15
|
+
|
|
16
|
+
export const FAKE_BASE = 'https://argocd.example.com';
|
|
17
|
+
export const FAKE_TOKEN = 'placeholder-argocd-token';
|
|
18
|
+
|
|
19
|
+
export type Seen = { readonly method: string; readonly path: string; readonly body: unknown };
|
|
20
|
+
|
|
21
|
+
/** Argo CD's grpc-gateway `{ code?, message, error? }` failure envelope. */
|
|
22
|
+
export const fakeFailure = (status: number, message: string, code?: number) =>
|
|
23
|
+
Response.json({ code, error: message, message }, { status });
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* `route` decides every response; this wrapper only records what was sent and adapts to
|
|
27
|
+
* `globalThis.fetch`'s signature. A test that expects no request at all asserts `seen` is empty.
|
|
28
|
+
*/
|
|
29
|
+
export const fakeArgocd = (
|
|
30
|
+
route: (method: string, url: URL, body: unknown, callNumber: number) => Response,
|
|
31
|
+
) => {
|
|
32
|
+
const seen: Seen[] = [];
|
|
33
|
+
let callNumber = 0;
|
|
34
|
+
const fetch = (async (input: string | URL | Request, init?: RequestInit) => {
|
|
35
|
+
const request =
|
|
36
|
+
input instanceof Request ? new Request(input, init) : new Request(String(input), init);
|
|
37
|
+
const url = new URL(request.url);
|
|
38
|
+
const bodyText = await request.clone().text();
|
|
39
|
+
const body: unknown = bodyText === '' ? undefined : JSON.parse(bodyText);
|
|
40
|
+
seen.push({ body, method: request.method, path: `${url.pathname}${url.search}` });
|
|
41
|
+
callNumber += 1;
|
|
42
|
+
return route(request.method, url, body, callNumber);
|
|
43
|
+
}) as typeof globalThis.fetch;
|
|
44
|
+
return { fetch, seen };
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* distilled Credentials + the real FetchHttpClient over the fake — `ArgocdOpContext`, what every
|
|
49
|
+
* resource file's exported `spec.fetchLive`/`spec.upsert`/`spec.destroy` needs.
|
|
50
|
+
*
|
|
51
|
+
* ⚠️ NOT PAIRED WITH `handlers` (each resource file's `argocdHandlers(spec)` export). `handlers`
|
|
52
|
+
* bakes in `CredentialsFromEnv`, which resolves through Effect's `Config` snapshot of
|
|
53
|
+
* `process.env` — untestable against a fake server without mutating the real environment. So a
|
|
54
|
+
* test calls `spec.fetchLive`/`spec.upsert`/`spec.destroy` directly, with this explicit layer,
|
|
55
|
+
* exercising exactly the same production code without that env seam.
|
|
56
|
+
*/
|
|
57
|
+
export const fakeArgocdLayer = (
|
|
58
|
+
fetchFn: typeof globalThis.fetch,
|
|
59
|
+
creds: Layer.Layer<Credentials> = fromToken({ apiBaseUrl: FAKE_BASE, token: FAKE_TOKEN }),
|
|
60
|
+
) => Layer.mergeAll(creds, FetchHttpClient.layer, Layer.succeed(FetchHttpClient.Fetch, fetchFn));
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The write-only credential shape `Repository` and `RepoCreds` share — Argo CD's own REST schema
|
|
3
|
+
* gives both the same four secret-shaped fields (`password`, `sshPrivateKey`, `bearerToken`,
|
|
4
|
+
* `tlsClientCertKey` — measured against `V1alpha1Repository`/`V1alpha1RepoCreds` in the generated
|
|
5
|
+
* `services/argocd.ts`, distilled homeflare/base). One helper here instead of two copies.
|
|
6
|
+
*
|
|
7
|
+
* ⛔ NO SECRET VALUE IS EVER A PROP OR AN ATTRIBUTE — `../secrets/write-only.ts`'s rule. A prop
|
|
8
|
+
* carries only the NAME of the environment variable holding a value (`FromEnv`); the value is
|
|
9
|
+
* read at call time and sent on the wire, never stored.
|
|
10
|
+
*
|
|
11
|
+
* ⚠️ NO DRIFT DETECTION ON THE SECRET FIELDS, ON PURPOSE — same simplification
|
|
12
|
+
* `../forgejo/org-actions-secrets.ts` makes for the identical problem (Argo CD's GET never
|
|
13
|
+
* returns `password`/`sshPrivateKey`/`bearerToken`/`tlsClientCertKey` back, so there is nothing
|
|
14
|
+
* to diff against). `matches` in each resource file compares only the PLAIN fields the API does
|
|
15
|
+
* return; a changed credential is written once on create and never rotated automatically —
|
|
16
|
+
* rotate by destroy-then-create, or a manual call outside Alchemy. A seal-based drift check
|
|
17
|
+
* (`../secrets/write-only.ts#seal`) is possible later if that limitation becomes a real problem;
|
|
18
|
+
* `proxmox/pbs-notification-target-lifecycle.ts` is the reference for what that costs in file size.
|
|
19
|
+
*/
|
|
20
|
+
import * as Data from 'effect/Data';
|
|
21
|
+
import * as Effect from 'effect/Effect';
|
|
22
|
+
import { type FromEnv, resolveAll } from '../secrets/write-only.ts';
|
|
23
|
+
|
|
24
|
+
export interface GitCredentialRefs {
|
|
25
|
+
readonly password?: FromEnv;
|
|
26
|
+
readonly sshPrivateKey?: FromEnv;
|
|
27
|
+
readonly bearerToken?: FromEnv;
|
|
28
|
+
readonly tlsClientCertKey?: FromEnv;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface ResolvedGitCredentials {
|
|
32
|
+
readonly password?: string;
|
|
33
|
+
readonly sshPrivateKey?: string;
|
|
34
|
+
readonly bearerToken?: string;
|
|
35
|
+
readonly tlsClientCertKey?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Domain refusal, not a distilled error — a declared credential's env var is unset at call time. */
|
|
39
|
+
export class ArgocdSecretEnvUnsetError extends Data.TaggedError('ArgocdSecretEnvUnsetError')<{
|
|
40
|
+
readonly message: string;
|
|
41
|
+
}> {}
|
|
42
|
+
|
|
43
|
+
/** Only the refs actually declared — `resolveAll` takes no `undefined` values. */
|
|
44
|
+
const declaredRefs = (refs: GitCredentialRefs): Readonly<Record<string, FromEnv>> => {
|
|
45
|
+
const out: Record<string, FromEnv> = {};
|
|
46
|
+
if (refs.password !== undefined) out.password = refs.password;
|
|
47
|
+
if (refs.sshPrivateKey !== undefined) out.sshPrivateKey = refs.sshPrivateKey;
|
|
48
|
+
if (refs.bearerToken !== undefined) out.bearerToken = refs.bearerToken;
|
|
49
|
+
if (refs.tlsClientCertKey !== undefined) out.tlsClientCertKey = refs.tlsClientCertKey;
|
|
50
|
+
return out;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolves every declared ref from the deploying process's own environment, or fails with the
|
|
55
|
+
* names of whichever are missing — never a value, in the failure or anywhere else.
|
|
56
|
+
*/
|
|
57
|
+
export const resolveGitCredentials = (
|
|
58
|
+
refs: GitCredentialRefs,
|
|
59
|
+
): Effect.Effect<ResolvedGitCredentials, ArgocdSecretEnvUnsetError> => {
|
|
60
|
+
const { values, missing } = resolveAll(declaredRefs(refs));
|
|
61
|
+
if (missing.length > 0) {
|
|
62
|
+
return Effect.fail(
|
|
63
|
+
new ArgocdSecretEnvUnsetError({
|
|
64
|
+
message:
|
|
65
|
+
`credential write requires ${missing.join(', ')}, unset or empty in the ` +
|
|
66
|
+
'deploying environment. Export it and deploy again.',
|
|
67
|
+
}),
|
|
68
|
+
);
|
|
69
|
+
}
|
|
70
|
+
return Effect.succeed(values as ResolvedGitCredentials);
|
|
71
|
+
};
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Argo CD providers for Alchemy.
|
|
3
|
+
*
|
|
4
|
+
* ⛔ THIS BARREL IS THE PUBLIC API, AND IT IS DELIBERATELY SMALLER THAN THE DIRECTORY. `resource.ts`
|
|
5
|
+
* and `git-credentials.ts` are internals the three resource files share — a consumer builds
|
|
6
|
+
* against the resources and `providers()`, not the engine underneath.
|
|
7
|
+
*
|
|
8
|
+
* ★ `Application` AND `AppProject` ARE NOT HERE — they are Kubernetes CRDs, not Argo CD REST-only
|
|
9
|
+
* objects, and the upstream-correct owner is Alchemy's own `Kubernetes.Manifest`
|
|
10
|
+
* (`alchemy/Kubernetes`, v2.0.0-beta.79). See `docs/argocd.md#applications-and-appprojects` for
|
|
11
|
+
* the measured reasoning and worked examples — no code for those two lives in this kit.
|
|
12
|
+
*/
|
|
13
|
+
export {
|
|
14
|
+
ArgocdCluster,
|
|
15
|
+
ArgocdClusterProvider,
|
|
16
|
+
type ClusterAttributes,
|
|
17
|
+
type ClusterCredentialRefs,
|
|
18
|
+
type ClusterProps,
|
|
19
|
+
} from './cluster.ts';
|
|
20
|
+
export {
|
|
21
|
+
type ArgocdCredentialsConfig,
|
|
22
|
+
Credentials,
|
|
23
|
+
CredentialsFromEnv,
|
|
24
|
+
fromEnv,
|
|
25
|
+
fromToken,
|
|
26
|
+
} from './credentials.ts';
|
|
27
|
+
export { type ArgocdSecretEnvUnsetError, type GitCredentialRefs } from './git-credentials.ts';
|
|
28
|
+
export { providers } from './providers.ts';
|
|
29
|
+
export {
|
|
30
|
+
ArgocdRepoCreds,
|
|
31
|
+
ArgocdRepoCredsProvider,
|
|
32
|
+
type RepoCredsAttributes,
|
|
33
|
+
type RepoCredsProps,
|
|
34
|
+
} from './repo-creds.ts';
|
|
35
|
+
export {
|
|
36
|
+
ArgocdRepository,
|
|
37
|
+
ArgocdRepositoryProvider,
|
|
38
|
+
type RepositoryAttributes,
|
|
39
|
+
type RepositoryProps,
|
|
40
|
+
} from './repository.ts';
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* All three Argo CD providers as one layer for a stack.
|
|
3
|
+
*
|
|
4
|
+
* Layer.mergeAll(argocdProviders(), …the stack's other providers)
|
|
5
|
+
* .pipe(Layer.provide(FetchHttpClient.layer))
|
|
6
|
+
*
|
|
7
|
+
* Mirrors `../discord/providers.ts`: `FetchHttpClient.layer` is the stack's own responsibility to
|
|
8
|
+
* provide (the shared transport every family needs), not something a single vendor's `providers()`
|
|
9
|
+
* should pin.
|
|
10
|
+
*/
|
|
11
|
+
import * as Layer from 'effect/Layer';
|
|
12
|
+
import { ArgocdClusterProvider } from './cluster.ts';
|
|
13
|
+
import { ArgocdRepoCredsProvider } from './repo-creds.ts';
|
|
14
|
+
import { ArgocdRepositoryProvider } from './repository.ts';
|
|
15
|
+
|
|
16
|
+
export const providers = () =>
|
|
17
|
+
Layer.mergeAll(ArgocdRepositoryProvider(), ArgocdRepoCredsProvider(), ArgocdClusterProvider());
|