k3s-deployer 0.1.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/index.cjs +1 -0
- package/index.d.ts +193 -0
- package/index.mjs +4 -0
- package/package.json +47 -0
- package/src/bootstrap/index.cjs +211 -0
- package/src/detect/index.cjs +461 -0
- package/src/dns/cloudflare.cjs +163 -0
- package/src/execute/index.cjs +386 -0
- package/src/index.cjs +67 -0
- package/src/mobile/index.cjs +1093 -0
- package/src/mobile/web-android-emulator-adapter.cjs +701 -0
- package/src/plan/index.cjs +200 -0
- package/src/runtime/index.cjs +16 -0
- package/src/runtime/local-runner.cjs +69 -0
- package/src/runtime/ssh-runner.cjs +206 -0
- package/src/shared/source.cjs +145 -0
- package/src/shared/utils.cjs +104 -0
- package/src/templates/dockerfile.cjs +114 -0
- package/src/templates/manifests.cjs +291 -0
package/index.cjs
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./src/index.cjs');
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
export type RepositorySource =
|
|
2
|
+
| { kind: 'git'; url: string; branch: string; fullName?: string; accessToken?: string }
|
|
3
|
+
| { kind: 'local'; path: string; branch?: string };
|
|
4
|
+
|
|
5
|
+
export type DatastoreKind =
|
|
6
|
+
| 'postgresql'
|
|
7
|
+
| 'mysql'
|
|
8
|
+
| 'sqlite'
|
|
9
|
+
| 'mongodb'
|
|
10
|
+
| 'redis'
|
|
11
|
+
| 'elasticsearch';
|
|
12
|
+
|
|
13
|
+
export type UnitRole = 'backend' | 'frontend' | 'mobile';
|
|
14
|
+
|
|
15
|
+
export interface InspectionUnit {
|
|
16
|
+
id: string;
|
|
17
|
+
role: UnitRole;
|
|
18
|
+
framework: string;
|
|
19
|
+
root: string;
|
|
20
|
+
language: string;
|
|
21
|
+
port?: number;
|
|
22
|
+
build: {
|
|
23
|
+
install: string[];
|
|
24
|
+
build?: string[];
|
|
25
|
+
start?: string[];
|
|
26
|
+
packageManager?: string;
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface InspectionResult {
|
|
31
|
+
projectSlug: string;
|
|
32
|
+
layout: 'monorepo' | 'single-role';
|
|
33
|
+
units: InspectionUnit[];
|
|
34
|
+
datastores: Array<{
|
|
35
|
+
kind: DatastoreKind;
|
|
36
|
+
role: 'primary' | 'auxiliary';
|
|
37
|
+
source: 'config' | 'dependency' | 'env' | 'heuristic';
|
|
38
|
+
}>;
|
|
39
|
+
warnings: string[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface DeploymentPlan {
|
|
43
|
+
inspection: InspectionResult;
|
|
44
|
+
cluster: {
|
|
45
|
+
bootstrap: 'if-missing';
|
|
46
|
+
registryHost: string;
|
|
47
|
+
namespace: string;
|
|
48
|
+
};
|
|
49
|
+
env: {
|
|
50
|
+
backend: Record<string, string>;
|
|
51
|
+
frontendPublic: Record<string, string>;
|
|
52
|
+
};
|
|
53
|
+
manifests: {
|
|
54
|
+
apps: string[];
|
|
55
|
+
data: string[];
|
|
56
|
+
};
|
|
57
|
+
mobile?: {
|
|
58
|
+
framework: 'android' | 'flutter';
|
|
59
|
+
apkPath: string;
|
|
60
|
+
applicationId?: string;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface DeployResult {
|
|
65
|
+
releaseId: string;
|
|
66
|
+
unitUrls: Record<string, string>;
|
|
67
|
+
mobilePreviewUrl?: string;
|
|
68
|
+
mobileArtifactPath?: string;
|
|
69
|
+
mobileApplicationId?: string;
|
|
70
|
+
dnsRecord?: {
|
|
71
|
+
provider: 'cloudflare';
|
|
72
|
+
type: 'A';
|
|
73
|
+
domain: string;
|
|
74
|
+
targetIp: string;
|
|
75
|
+
proxied: boolean;
|
|
76
|
+
} | null;
|
|
77
|
+
logs: string[];
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export interface CloudflareDnsConfig {
|
|
81
|
+
apiToken?: string;
|
|
82
|
+
apiKey?: string;
|
|
83
|
+
email?: string;
|
|
84
|
+
zoneId?: string;
|
|
85
|
+
targetIp?: string;
|
|
86
|
+
proxied?: boolean;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface DetectOverrides {
|
|
90
|
+
projectName?: string;
|
|
91
|
+
backendRoot?: string;
|
|
92
|
+
frontendRoot?: string;
|
|
93
|
+
mobileRoot?: string;
|
|
94
|
+
primaryDatastore?: DatastoreKind;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export interface PlanOverrides extends DetectOverrides {
|
|
98
|
+
namespace?: string;
|
|
99
|
+
registryHost?: string;
|
|
100
|
+
domain?: string;
|
|
101
|
+
privateEnv?: Record<string, string>;
|
|
102
|
+
publicEnv?: Record<string, string>;
|
|
103
|
+
serviceBasePort?: number;
|
|
104
|
+
deploymentStrategy?: 'rolling-update' | 'blue-green' | 'canary';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export type RuntimeTarget =
|
|
108
|
+
| { kind: 'local'; workspaceRoot?: string; hostAddress?: string }
|
|
109
|
+
| {
|
|
110
|
+
kind: 'ssh';
|
|
111
|
+
host: string;
|
|
112
|
+
user: string;
|
|
113
|
+
port?: number;
|
|
114
|
+
workspaceRoot?: string;
|
|
115
|
+
privateKey?: string;
|
|
116
|
+
privateKeyPath?: string;
|
|
117
|
+
password?: string;
|
|
118
|
+
sudoPassword?: string;
|
|
119
|
+
connectTimeoutSec?: number;
|
|
120
|
+
commandTimeoutMs?: number;
|
|
121
|
+
hostAddress?: string;
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
export interface ClusterStatus {
|
|
125
|
+
docker: boolean;
|
|
126
|
+
k3s: boolean;
|
|
127
|
+
registry: boolean;
|
|
128
|
+
ingress: boolean;
|
|
129
|
+
certManager: boolean;
|
|
130
|
+
namespace: string;
|
|
131
|
+
target: 'local' | 'ssh';
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface MobilePreviewAdapter {
|
|
135
|
+
start(input: {
|
|
136
|
+
projectSlug: string;
|
|
137
|
+
framework: 'android' | 'flutter';
|
|
138
|
+
artifactPath?: string;
|
|
139
|
+
applicationId?: string;
|
|
140
|
+
plan: DeploymentPlan;
|
|
141
|
+
options?: MobilePreviewOptions;
|
|
142
|
+
}): Promise<{ url: string }>;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export interface MobilePreviewOptions {
|
|
146
|
+
artifactPath?: string;
|
|
147
|
+
installArtifact?: boolean;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface SupplementalFile {
|
|
151
|
+
path: string;
|
|
152
|
+
contentBase64: string;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
export interface DeployOptions {
|
|
156
|
+
target?: RuntimeTarget;
|
|
157
|
+
overrides?: PlanOverrides;
|
|
158
|
+
dryRun?: boolean;
|
|
159
|
+
onLog?: (line: string) => void | Promise<void>;
|
|
160
|
+
source?: RepositorySource;
|
|
161
|
+
workspaceName?: string;
|
|
162
|
+
supplementalFiles?: SupplementalFile[];
|
|
163
|
+
cloudflareDns?: CloudflareDnsConfig;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export interface K3sDeployerConfig {
|
|
167
|
+
workspaceRoot?: string;
|
|
168
|
+
registryHost?: string;
|
|
169
|
+
namespacePrefix?: string;
|
|
170
|
+
mobileBuildTimeoutMs?: number;
|
|
171
|
+
defaultTarget?: RuntimeTarget;
|
|
172
|
+
mobilePreviewAdapter?: MobilePreviewAdapter;
|
|
173
|
+
runnerFactory?: (target: RuntimeTarget) => Promise<unknown> | unknown;
|
|
174
|
+
cloudflareDns?: CloudflareDnsConfig;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export interface WebAndroidEmulatorAdapterOptions {
|
|
178
|
+
namespace?: string;
|
|
179
|
+
domain?: string;
|
|
180
|
+
registryHost?: string;
|
|
181
|
+
previewUrl?: string;
|
|
182
|
+
cloudflareDns?: CloudflareDnsConfig;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function createWebAndroidEmulatorAdapter(options?: WebAndroidEmulatorAdapterOptions): MobilePreviewAdapter;
|
|
186
|
+
|
|
187
|
+
export function createK3sDeployer(config?: K3sDeployerConfig): {
|
|
188
|
+
inspectRepository(source: RepositorySource, overrides?: DetectOverrides): Promise<InspectionResult>;
|
|
189
|
+
planDeployment(source: RepositorySource, overrides?: PlanOverrides): Promise<DeploymentPlan>;
|
|
190
|
+
ensureCluster(target: RuntimeTarget): Promise<ClusterStatus>;
|
|
191
|
+
deploy(source: RepositorySource | DeploymentPlan, options?: DeployOptions): Promise<DeployResult>;
|
|
192
|
+
startMobilePreview(plan: DeploymentPlan, options?: MobilePreviewOptions): Promise<{ url: string }>;
|
|
193
|
+
};
|
package/index.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "k3s-deployer",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Automatic k3s deployment planner and executor for full stack applications",
|
|
5
|
+
"main": "./index.cjs",
|
|
6
|
+
"module": "./index.mjs",
|
|
7
|
+
"types": "./index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"require": "./index.cjs",
|
|
11
|
+
"import": "./index.mjs",
|
|
12
|
+
"types": "./index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"index.cjs",
|
|
17
|
+
"index.mjs",
|
|
18
|
+
"index.d.ts",
|
|
19
|
+
"src/"
|
|
20
|
+
],
|
|
21
|
+
"scripts": {
|
|
22
|
+
"test": "node --test",
|
|
23
|
+
"prepublishOnly": "node --test"
|
|
24
|
+
},
|
|
25
|
+
"keywords": [
|
|
26
|
+
"k3s",
|
|
27
|
+
"kubernetes",
|
|
28
|
+
"deploy",
|
|
29
|
+
"deployment",
|
|
30
|
+
"docker",
|
|
31
|
+
"devops",
|
|
32
|
+
"fullstack"
|
|
33
|
+
],
|
|
34
|
+
"author": "3x-haust",
|
|
35
|
+
"repository": {
|
|
36
|
+
"type": "git",
|
|
37
|
+
"url": "https://github.com/3x-haust/k3s-deployer"
|
|
38
|
+
},
|
|
39
|
+
"bugs": {
|
|
40
|
+
"url": "https://github.com/3x-haust/k3s-deployer/issues"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/3x-haust/k3s-deployer#readme",
|
|
43
|
+
"dependencies": {
|
|
44
|
+
"ssh2": "^1.17.0"
|
|
45
|
+
},
|
|
46
|
+
"license": "MIT"
|
|
47
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
const { createRunner } = require('../runtime/index.cjs');
|
|
2
|
+
const { shellQuote } = require('../shared/utils.cjs');
|
|
3
|
+
|
|
4
|
+
async function commandSucceeds(runner, command, options = {}) {
|
|
5
|
+
try {
|
|
6
|
+
await runner.run(command, { ...options, allowFailure: false });
|
|
7
|
+
return true;
|
|
8
|
+
} catch {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getRegistryProbeUrl(registryHost) {
|
|
14
|
+
const normalized = String(registryHost || 'localhost:5000').replace(/\/+$/, '');
|
|
15
|
+
if (normalized.startsWith('http://') || normalized.startsWith('https://')) {
|
|
16
|
+
return `${normalized}/v2/`;
|
|
17
|
+
}
|
|
18
|
+
return `http://${normalized}/v2/`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function getRegistryProbeUrls(registryHost) {
|
|
22
|
+
const urls = [
|
|
23
|
+
'http://localhost:5000/v2/',
|
|
24
|
+
'http://127.0.0.1:5000/v2/',
|
|
25
|
+
getRegistryProbeUrl(registryHost),
|
|
26
|
+
];
|
|
27
|
+
return Array.from(new Set(urls));
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function findHealthyRegistryProbe(runner, probeUrls, dryRun) {
|
|
31
|
+
for (const probeUrl of probeUrls) {
|
|
32
|
+
if (
|
|
33
|
+
await commandSucceeds(
|
|
34
|
+
runner,
|
|
35
|
+
`curl -fsS ${shellQuote(probeUrl)} >/dev/null 2>&1`,
|
|
36
|
+
{ dryRun },
|
|
37
|
+
)
|
|
38
|
+
) {
|
|
39
|
+
return probeUrl;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function ensureCluster(target, config = {}, options = {}) {
|
|
46
|
+
const runner = options.runner || (await createRunner(target, config));
|
|
47
|
+
const namespace = options.namespace || `${config.namespacePrefix || 'apps'}-default`;
|
|
48
|
+
const dryRun = options.dryRun || false;
|
|
49
|
+
const log = options.onLog || (() => {});
|
|
50
|
+
const registryHost = config.registryHost || 'localhost:5000';
|
|
51
|
+
const registryProbeUrls = getRegistryProbeUrls(registryHost);
|
|
52
|
+
const runLogged = (command, extra = {}) =>
|
|
53
|
+
runner.run(command, {
|
|
54
|
+
dryRun,
|
|
55
|
+
onStdout: log,
|
|
56
|
+
onStderr: log,
|
|
57
|
+
...extra,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
const dockerInstalled = await commandSucceeds(runner, 'command -v docker >/dev/null 2>&1', { dryRun });
|
|
61
|
+
if (!dockerInstalled) {
|
|
62
|
+
await log('Installing Docker');
|
|
63
|
+
await runLogged('curl -fsSL https://get.docker.com | sh');
|
|
64
|
+
await log('Installed Docker');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const k3sInstalled = await commandSucceeds(runner, 'command -v k3s >/dev/null 2>&1', { dryRun });
|
|
68
|
+
if (!k3sInstalled) {
|
|
69
|
+
await log('Installing k3s');
|
|
70
|
+
await runLogged(
|
|
71
|
+
'curl -sfL https://get.k3s.io | sh -s - --write-kubeconfig-mode 644',
|
|
72
|
+
);
|
|
73
|
+
await log('Installed k3s');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const healthyRegistryProbe = await findHealthyRegistryProbe(
|
|
77
|
+
runner,
|
|
78
|
+
registryProbeUrls,
|
|
79
|
+
dryRun,
|
|
80
|
+
);
|
|
81
|
+
if (healthyRegistryProbe) {
|
|
82
|
+
await log(`Using existing registry at ${healthyRegistryProbe.replace(/\/v2\/$/, '')}`);
|
|
83
|
+
} else {
|
|
84
|
+
await log('Starting local registry');
|
|
85
|
+
try {
|
|
86
|
+
await runLogged(
|
|
87
|
+
'docker rm -f k3s-deployer-registry >/dev/null 2>&1 || true && docker run -d -p 5000:5000 --restart always --name k3s-deployer-registry registry:2',
|
|
88
|
+
);
|
|
89
|
+
await log('Started local registry');
|
|
90
|
+
} catch (error) {
|
|
91
|
+
const registryNowHealthy = await findHealthyRegistryProbe(
|
|
92
|
+
runner,
|
|
93
|
+
registryProbeUrls,
|
|
94
|
+
dryRun,
|
|
95
|
+
);
|
|
96
|
+
if (registryNowHealthy) {
|
|
97
|
+
await log(`Using existing registry at ${registryNowHealthy.replace(/\/v2\/$/, '')}`);
|
|
98
|
+
} else if (
|
|
99
|
+
error instanceof Error &&
|
|
100
|
+
/port is already allocated|address already in use/i.test(error.message)
|
|
101
|
+
) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`Port 5000 is already in use on the target host and no registry responded at ${registryProbeUrls.join(', ')}`,
|
|
104
|
+
);
|
|
105
|
+
} else {
|
|
106
|
+
throw error;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
await runLogged(
|
|
112
|
+
`sudo mkdir -p /etc/docker && sudo python3 - <<'PY'
|
|
113
|
+
import json
|
|
114
|
+
import os
|
|
115
|
+
|
|
116
|
+
path = '/etc/docker/daemon.json'
|
|
117
|
+
data = {}
|
|
118
|
+
if os.path.exists(path):
|
|
119
|
+
try:
|
|
120
|
+
with open(path, 'r', encoding='utf-8') as handle:
|
|
121
|
+
data = json.load(handle)
|
|
122
|
+
except Exception:
|
|
123
|
+
data = {}
|
|
124
|
+
|
|
125
|
+
registries = data.get('insecure-registries') or []
|
|
126
|
+
for item in ['localhost:5000', '127.0.0.1:5000', '${registryHost}']:
|
|
127
|
+
if item not in registries:
|
|
128
|
+
registries.append(item)
|
|
129
|
+
|
|
130
|
+
data['insecure-registries'] = registries
|
|
131
|
+
with open(path, 'w', encoding='utf-8') as handle:
|
|
132
|
+
json.dump(data, handle)
|
|
133
|
+
PY`,
|
|
134
|
+
);
|
|
135
|
+
await runLogged('sudo systemctl restart docker || sudo service docker restart || true', {
|
|
136
|
+
allowFailure: true,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
await runLogged(
|
|
140
|
+
`sudo mkdir -p /etc/rancher/k3s && sudo sh -lc "cat > /etc/rancher/k3s/registries.yaml <<'EOF'
|
|
141
|
+
mirrors:
|
|
142
|
+
${registryHost}:
|
|
143
|
+
endpoint:
|
|
144
|
+
- http://${registryHost}
|
|
145
|
+
configs:
|
|
146
|
+
${registryHost}:
|
|
147
|
+
tls:
|
|
148
|
+
insecure_skip_verify: true
|
|
149
|
+
EOF
|
|
150
|
+
"`,
|
|
151
|
+
{ allowFailure: true },
|
|
152
|
+
);
|
|
153
|
+
await runLogged('sudo systemctl restart k3s || sudo service k3s restart || true', {
|
|
154
|
+
allowFailure: true,
|
|
155
|
+
});
|
|
156
|
+
await runLogged('kubectl wait --for=condition=Ready node --all --timeout=120s || kubectl get nodes || true', {
|
|
157
|
+
allowFailure: true,
|
|
158
|
+
});
|
|
159
|
+
await runLogged(`TARGET_NODE=$(hostname)
|
|
160
|
+
if ! kubectl get node "$TARGET_NODE" >/dev/null 2>&1; then
|
|
161
|
+
TARGET_NODE=$(hostname -s)
|
|
162
|
+
fi
|
|
163
|
+
if [ -n "$TARGET_NODE" ] && kubectl get node "$TARGET_NODE" >/dev/null 2>&1; then
|
|
164
|
+
kubectl label nodes --all k3s-deployer-target- >/dev/null 2>&1 || true
|
|
165
|
+
kubectl label node "$TARGET_NODE" k3s-deployer-target=true --overwrite
|
|
166
|
+
fi`, {
|
|
167
|
+
allowFailure: true,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
if (namespace !== 'default') {
|
|
171
|
+
await runLogged(`kubectl get namespace ${shellQuote(namespace)} >/dev/null 2>&1 || kubectl create namespace ${shellQuote(namespace)}`, {
|
|
172
|
+
allowFailure: true,
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const ingressInstalled = await commandSucceeds(runner, 'kubectl get namespace ingress-nginx >/dev/null 2>&1', {
|
|
177
|
+
dryRun,
|
|
178
|
+
});
|
|
179
|
+
if (!ingressInstalled) {
|
|
180
|
+
await log('Installing ingress-nginx');
|
|
181
|
+
await runLogged(
|
|
182
|
+
'kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/main/deploy/static/provider/cloud/deploy.yaml',
|
|
183
|
+
);
|
|
184
|
+
await log('Installed ingress-nginx');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const certManagerInstalled = await commandSucceeds(runner, 'kubectl get namespace cert-manager >/dev/null 2>&1', {
|
|
188
|
+
dryRun,
|
|
189
|
+
});
|
|
190
|
+
if (!certManagerInstalled) {
|
|
191
|
+
await log('Installing cert-manager');
|
|
192
|
+
await runLogged(
|
|
193
|
+
'kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.15.1/cert-manager.yaml',
|
|
194
|
+
);
|
|
195
|
+
await log('Installed cert-manager');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return {
|
|
199
|
+
docker: true,
|
|
200
|
+
k3s: true,
|
|
201
|
+
registry: true,
|
|
202
|
+
ingress: true,
|
|
203
|
+
certManager: true,
|
|
204
|
+
namespace,
|
|
205
|
+
target: target && target.kind === 'ssh' ? 'ssh' : 'local',
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
module.exports = {
|
|
210
|
+
ensureCluster,
|
|
211
|
+
};
|