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.
@@ -0,0 +1,461 @@
1
+ const path = require('node:path');
2
+ const {
3
+ exists,
4
+ listImmediateDirectories,
5
+ posixRelative,
6
+ readIfExists,
7
+ readJsonIfExists,
8
+ slugify,
9
+ uniqueBy,
10
+ } = require('../shared/utils.cjs');
11
+ const { deriveSourceName, withLocalRepository } = require('../shared/source.cjs');
12
+
13
+ const BACKEND_FRAMEWORKS = new Set([
14
+ 'django',
15
+ 'nestjs',
16
+ 'springboot',
17
+ 'gin',
18
+ 'echo',
19
+ 'fiber',
20
+ 'axum',
21
+ 'actix-web',
22
+ 'rails',
23
+ 'sinatra',
24
+ ]);
25
+
26
+ const FRONTEND_FRAMEWORKS = new Set(['react', 'nextjs', 'vue', 'angular']);
27
+
28
+ const PRIMARY_DATASTORES = new Set(['postgresql', 'mysql', 'sqlite', 'mongodb']);
29
+
30
+ async function detectPackageManager(rootPath) {
31
+ if (await exists(path.join(rootPath, 'pnpm-lock.yaml'))) {
32
+ return 'pnpm';
33
+ }
34
+ if (await exists(path.join(rootPath, 'yarn.lock'))) {
35
+ return 'yarn';
36
+ }
37
+ return 'npm';
38
+ }
39
+
40
+ function packageManagerCommands(manager, task) {
41
+ if (manager === 'yarn') {
42
+ if (task === 'install') {
43
+ return ['yarn', 'install', '--frozen-lockfile'];
44
+ }
45
+ return ['yarn', task];
46
+ }
47
+ if (manager === 'pnpm') {
48
+ if (task === 'install') {
49
+ return ['pnpm', 'install', '--frozen-lockfile'];
50
+ }
51
+ return ['pnpm', task];
52
+ }
53
+ if (task === 'install') {
54
+ return ['npm', 'install'];
55
+ }
56
+ return ['npm', 'run', task];
57
+ }
58
+
59
+ function resolveUnitBuild(framework, packageManager) {
60
+ if (framework === 'react') {
61
+ return {
62
+ install: packageManagerCommands(packageManager, 'install'),
63
+ build: packageManagerCommands(packageManager, 'build'),
64
+ start: ['npx', 'serve', '-s', 'dist', '-l', '3000'],
65
+ packageManager,
66
+ };
67
+ }
68
+ if (framework === 'nextjs') {
69
+ return {
70
+ install: packageManagerCommands(packageManager, 'install'),
71
+ build: packageManagerCommands(packageManager, 'build'),
72
+ start: packageManagerCommands(packageManager, 'start'),
73
+ packageManager,
74
+ };
75
+ }
76
+ if (framework === 'vue') {
77
+ return {
78
+ install: packageManagerCommands(packageManager, 'install'),
79
+ build: packageManagerCommands(packageManager, 'build'),
80
+ start: ['npx', 'serve', '-s', 'dist', '-l', '3000'],
81
+ packageManager,
82
+ };
83
+ }
84
+ if (framework === 'angular') {
85
+ return {
86
+ install: packageManagerCommands(packageManager, 'install'),
87
+ build: packageManagerCommands(packageManager, 'build'),
88
+ start: ['npx', 'http-server', 'dist', '-p', '3000'],
89
+ packageManager,
90
+ };
91
+ }
92
+ if (framework === 'nestjs') {
93
+ return {
94
+ install: packageManagerCommands(packageManager, 'install'),
95
+ build: packageManagerCommands(packageManager, 'build'),
96
+ start: ['node', 'dist/main.js'],
97
+ packageManager,
98
+ };
99
+ }
100
+ if (framework === 'django') {
101
+ return {
102
+ install: ['pip', 'install', '-r', 'requirements.txt'],
103
+ start: ['python', 'manage.py', 'runserver', '0.0.0.0:8000'],
104
+ };
105
+ }
106
+ if (framework === 'springboot') {
107
+ return {
108
+ install: ['sh', '-lc', './mvnw dependency:go-offline || mvn dependency:go-offline'],
109
+ build: ['sh', '-lc', './mvnw package -DskipTests || mvn package -DskipTests'],
110
+ start: ['sh', '-lc', 'java -jar $(find target build/libs -name "*.jar" | head -n 1)'],
111
+ };
112
+ }
113
+ if (framework === 'gin' || framework === 'echo' || framework === 'fiber') {
114
+ return {
115
+ install: ['go', 'mod', 'download'],
116
+ build: ['go', 'build', '-o', 'app'],
117
+ start: ['./app'],
118
+ };
119
+ }
120
+ if (framework === 'axum' || framework === 'actix-web') {
121
+ return {
122
+ install: ['cargo', 'fetch'],
123
+ build: ['cargo', 'build', '--release'],
124
+ start: ['sh', '-lc', './target/release/$(basename $(pwd) | tr -c "a-zA-Z0-9" "-")'],
125
+ };
126
+ }
127
+ if (framework === 'rails') {
128
+ return {
129
+ install: ['bundle', 'install'],
130
+ start: ['bundle', 'exec', 'rails', 'server', '-b', '0.0.0.0', '-p', '3000'],
131
+ };
132
+ }
133
+ if (framework === 'sinatra') {
134
+ return {
135
+ install: ['bundle', 'install'],
136
+ start: ['bundle', 'exec', 'ruby', 'app.rb', '-o', '0.0.0.0', '-p', '4567'],
137
+ };
138
+ }
139
+ if (framework === 'android') {
140
+ return {
141
+ install: ['sh', '-lc', './gradlew tasks >/dev/null'],
142
+ build: ['sh', '-lc', './gradlew assembleRelease'],
143
+ };
144
+ }
145
+ return {
146
+ install: ['flutter', 'pub', 'get'],
147
+ build: ['flutter', 'build', 'apk', '--release'],
148
+ };
149
+ }
150
+
151
+ function resolveLanguage(framework) {
152
+ if (framework === 'django') {
153
+ return 'python';
154
+ }
155
+ if (framework === 'springboot') {
156
+ return 'java';
157
+ }
158
+ if (framework === 'gin' || framework === 'echo' || framework === 'fiber') {
159
+ return 'go';
160
+ }
161
+ if (framework === 'axum' || framework === 'actix-web') {
162
+ return 'rust';
163
+ }
164
+ if (framework === 'rails' || framework === 'sinatra') {
165
+ return 'ruby';
166
+ }
167
+ if (framework === 'android' || framework === 'flutter') {
168
+ return 'mobile';
169
+ }
170
+ return 'javascript';
171
+ }
172
+
173
+ function resolvePort(framework) {
174
+ if (framework === 'django') {
175
+ return 8000;
176
+ }
177
+ if (framework === 'springboot') {
178
+ return 8080;
179
+ }
180
+ if (framework === 'gin' || framework === 'echo' || framework === 'fiber') {
181
+ return 8080;
182
+ }
183
+ if (framework === 'sinatra') {
184
+ return 4567;
185
+ }
186
+ return 3000;
187
+ }
188
+
189
+ async function detectFrameworkAtRoot(rootPath) {
190
+ const packageJson = await readJsonIfExists(path.join(rootPath, 'package.json'));
191
+ const dependencies = {
192
+ ...(packageJson?.dependencies || {}),
193
+ ...(packageJson?.devDependencies || {}),
194
+ };
195
+ const packageManager = await detectPackageManager(rootPath);
196
+
197
+ if (dependencies['@nestjs/core']) {
198
+ return { role: 'backend', framework: 'nestjs', packageManager };
199
+ }
200
+ if (dependencies.next) {
201
+ return { role: 'frontend', framework: 'nextjs', packageManager };
202
+ }
203
+ if (dependencies.react && dependencies['react-dom']) {
204
+ return { role: 'frontend', framework: 'react', packageManager };
205
+ }
206
+ if (dependencies.vue) {
207
+ return { role: 'frontend', framework: 'vue', packageManager };
208
+ }
209
+ if (dependencies['@angular/core'] || (await exists(path.join(rootPath, 'angular.json')))) {
210
+ return { role: 'frontend', framework: 'angular', packageManager };
211
+ }
212
+
213
+ const requirements = (await readIfExists(path.join(rootPath, 'requirements.txt'))) || '';
214
+ const pyproject = (await readIfExists(path.join(rootPath, 'pyproject.toml'))) || '';
215
+ if ((await exists(path.join(rootPath, 'manage.py'))) && /django/i.test(`${requirements}\n${pyproject}`)) {
216
+ return { role: 'backend', framework: 'django' };
217
+ }
218
+
219
+ const pom = (await readIfExists(path.join(rootPath, 'pom.xml'))) || '';
220
+ const gradle = `${(await readIfExists(path.join(rootPath, 'build.gradle'))) || ''}\n${
221
+ (await readIfExists(path.join(rootPath, 'build.gradle.kts'))) || ''
222
+ }`;
223
+ if (/spring-boot/i.test(`${pom}\n${gradle}`)) {
224
+ return { role: 'backend', framework: 'springboot' };
225
+ }
226
+
227
+ const goMod = (await readIfExists(path.join(rootPath, 'go.mod'))) || '';
228
+ if (/gin-gonic\/gin/i.test(goMod)) {
229
+ return { role: 'backend', framework: 'gin' };
230
+ }
231
+ if (/labstack\/echo/i.test(goMod)) {
232
+ return { role: 'backend', framework: 'echo' };
233
+ }
234
+ if (/gofiber\/fiber/i.test(goMod)) {
235
+ return { role: 'backend', framework: 'fiber' };
236
+ }
237
+
238
+ const cargoToml = (await readIfExists(path.join(rootPath, 'Cargo.toml'))) || '';
239
+ if (/^\s*axum\s*=|axum\s*=\s*\{/m.test(cargoToml)) {
240
+ return { role: 'backend', framework: 'axum' };
241
+ }
242
+ if (/^\s*actix-web\s*=|actix-web\s*=\s*\{/m.test(cargoToml)) {
243
+ return { role: 'backend', framework: 'actix-web' };
244
+ }
245
+
246
+ const gemfile = (await readIfExists(path.join(rootPath, 'Gemfile'))) || '';
247
+ if (/gem ['"]rails['"]/i.test(gemfile)) {
248
+ return { role: 'backend', framework: 'rails' };
249
+ }
250
+ if (/gem ['"]sinatra['"]/i.test(gemfile)) {
251
+ return { role: 'backend', framework: 'sinatra' };
252
+ }
253
+
254
+ const flutterPubspec = (await readIfExists(path.join(rootPath, 'pubspec.yaml'))) || '';
255
+ if (/^\s*flutter:\s*$/m.test(flutterPubspec) || /sdk:\s*flutter/m.test(flutterPubspec)) {
256
+ return { role: 'mobile', framework: 'flutter' };
257
+ }
258
+
259
+ const hasAndroidManifest = await exists(path.join(rootPath, 'app', 'src', 'main', 'AndroidManifest.xml'));
260
+ const hasGradle = (await exists(path.join(rootPath, 'app', 'build.gradle'))) || (await exists(path.join(rootPath, 'app', 'build.gradle.kts')));
261
+ if (hasAndroidManifest && hasGradle) {
262
+ return { role: 'mobile', framework: 'android' };
263
+ }
264
+
265
+ return null;
266
+ }
267
+
268
+ async function detectUnit(repoRoot, relativeRoot) {
269
+ const rootPath = relativeRoot === '.' ? repoRoot : path.join(repoRoot, relativeRoot);
270
+ const frameworkMatch = await detectFrameworkAtRoot(rootPath);
271
+ if (!frameworkMatch) {
272
+ return null;
273
+ }
274
+
275
+ const framework = frameworkMatch.framework;
276
+ return {
277
+ id: slugify(relativeRoot === '.' ? framework : `${path.basename(relativeRoot)}-${framework}`),
278
+ role: frameworkMatch.role,
279
+ framework,
280
+ root: relativeRoot,
281
+ language: resolveLanguage(framework),
282
+ port: frameworkMatch.role === 'mobile' ? undefined : resolvePort(framework),
283
+ build: resolveUnitBuild(framework, frameworkMatch.packageManager),
284
+ };
285
+ }
286
+
287
+ async function discoverCandidateRoots(repoRoot) {
288
+ const immediateDirectories = await listImmediateDirectories(repoRoot);
289
+ const priority = ['frontend', 'backend', 'client', 'server'];
290
+ const ordered = ['.', ...priority.filter((dir) => immediateDirectories.includes(dir))];
291
+ for (const directory of immediateDirectories) {
292
+ if (!ordered.includes(directory)) {
293
+ ordered.push(directory);
294
+ }
295
+ }
296
+ return ordered;
297
+ }
298
+
299
+ async function detectDatastores(repoRoot, units, overrides) {
300
+ const findings = [];
301
+ const candidateFiles = ['.env', '.env.local', '.env.example', 'application.yml', 'application.yaml', 'application.properties', 'settings.py', 'config/database.yml', 'go.mod', 'Cargo.toml', 'Gemfile', 'requirements.txt', 'pyproject.toml', 'package.json', 'pom.xml', 'build.gradle', 'build.gradle.kts'];
302
+ const searchRoots = ['.', ...units.map((unit) => unit.root).filter((root) => root !== '.')];
303
+ const seenPaths = new Set();
304
+
305
+ for (const root of searchRoots) {
306
+ const basePath = root === '.' ? repoRoot : path.join(repoRoot, root);
307
+ for (const fileName of candidateFiles) {
308
+ const filePath = path.join(basePath, fileName);
309
+ if (seenPaths.has(filePath) || !(await exists(filePath))) {
310
+ continue;
311
+ }
312
+ seenPaths.add(filePath);
313
+ const content = (await readIfExists(filePath)) || '';
314
+ const source = fileName.startsWith('.env') ? 'env' : fileName === 'package.json' ? 'dependency' : 'config';
315
+
316
+ if (
317
+ /postgres(ql)?:\/\//i.test(content) ||
318
+ /jdbc:postgresql/i.test(content) ||
319
+ /postgresql/i.test(content) ||
320
+ /"pg"\s*:/i.test(content) ||
321
+ /lib\/pq|jackc\/pgx/i.test(content)
322
+ ) {
323
+ findings.push({ kind: 'postgresql', role: 'primary', source });
324
+ }
325
+ if (/mysql:\/\//i.test(content) || /jdbc:mysql/i.test(content) || /mysqlclient|pymysql|mysql2|go-sql-driver\/mysql/i.test(content)) {
326
+ findings.push({ kind: 'mysql', role: 'primary', source });
327
+ }
328
+ if (/sqlite/i.test(content) || /better-sqlite3|sqlite3/i.test(content)) {
329
+ findings.push({ kind: 'sqlite', role: 'primary', source });
330
+ }
331
+ if (/mongodb(\+srv)?:\/\//i.test(content) || /mongoose|mongodb|mongo-driver|spring\.data\.mongodb/i.test(content)) {
332
+ findings.push({ kind: 'mongodb', role: 'primary', source });
333
+ }
334
+ if (/redis:\/\//i.test(content) || /ioredis|[^a-z]redis[^a-z]|spring\.data\.redis/i.test(content)) {
335
+ findings.push({ kind: 'redis', role: 'auxiliary', source });
336
+ }
337
+ if (/elasticsearch|@elastic\/elasticsearch|spring\.elasticsearch|go-elasticsearch/i.test(content)) {
338
+ findings.push({ kind: 'elasticsearch', role: 'auxiliary', source });
339
+ }
340
+ }
341
+ }
342
+
343
+ const uniqueFindings = uniqueBy(findings, (finding) => finding.kind);
344
+ const primaryKinds = uniqueFindings.filter((finding) => PRIMARY_DATASTORES.has(finding.kind));
345
+ if (primaryKinds.length > 1) {
346
+ const preferred = overrides && overrides.primaryDatastore;
347
+ if (!preferred) {
348
+ throw new Error(`Multiple primary datastores detected: ${primaryKinds.map((item) => item.kind).join(', ')}`);
349
+ }
350
+ return uniqueFindings.map((finding) => ({
351
+ ...finding,
352
+ role: finding.kind === preferred || !PRIMARY_DATASTORES.has(finding.kind) ? finding.role : 'auxiliary',
353
+ }));
354
+ }
355
+ return uniqueFindings;
356
+ }
357
+
358
+ function isNestedRoot(parentRoot, childRoot) {
359
+ if (parentRoot === childRoot) {
360
+ return false;
361
+ }
362
+ if (parentRoot === '.') {
363
+ return childRoot !== '.';
364
+ }
365
+ return childRoot.startsWith(`${parentRoot}/`);
366
+ }
367
+
368
+ function normalizeUnits(units) {
369
+ return units.filter((unit) => {
370
+ if (unit.role !== 'mobile') {
371
+ return true;
372
+ }
373
+
374
+ const parentMobile = units.find((candidate) => {
375
+ if (candidate === unit || candidate.role !== 'mobile') {
376
+ return false;
377
+ }
378
+ if (!isNestedRoot(candidate.root, unit.root)) {
379
+ return false;
380
+ }
381
+ if (candidate.framework === 'flutter') {
382
+ return true;
383
+ }
384
+ return candidate.root.length < unit.root.length;
385
+ });
386
+
387
+ return !parentMobile;
388
+ });
389
+ }
390
+
391
+ function classifyLayout(units) {
392
+ const backends = units.filter((unit) => BACKEND_FRAMEWORKS.has(unit.framework));
393
+ const frontends = units.filter((unit) => FRONTEND_FRAMEWORKS.has(unit.framework));
394
+ const mobile = units.filter((unit) => unit.role === 'mobile');
395
+
396
+ if (backends.length > 1) {
397
+ throw new Error(`Multiple backend roots detected: ${backends.map((unit) => unit.root).join(', ')}`);
398
+ }
399
+ if (frontends.length > 1) {
400
+ throw new Error(`Multiple frontend roots detected: ${frontends.map((unit) => unit.root).join(', ')}`);
401
+ }
402
+ if (mobile.length > 1) {
403
+ throw new Error(`Multiple mobile roots detected: ${mobile.map((unit) => unit.root).join(', ')}`);
404
+ }
405
+ if (backends.length === 1 && frontends.length === 1) {
406
+ return 'monorepo';
407
+ }
408
+ if (backends.length + frontends.length === 1) {
409
+ return 'single-role';
410
+ }
411
+ if (backends.length === 0 && frontends.length === 0 && mobile.length === 1) {
412
+ return 'single-role';
413
+ }
414
+ throw new Error('Unable to classify repository layout');
415
+ }
416
+
417
+ async function inspectRepository(source, overrides = {}, config = {}) {
418
+ return withLocalRepository(source, config.workspaceRoot, async (repoRoot) => {
419
+ const candidateRoots = await discoverCandidateRoots(repoRoot);
420
+ const units = [];
421
+
422
+ for (const relativeRoot of candidateRoots) {
423
+ const unit = await detectUnit(repoRoot, relativeRoot);
424
+ if (!unit) {
425
+ continue;
426
+ }
427
+ units.push(unit);
428
+ }
429
+
430
+ if (units.length === 0) {
431
+ throw new Error('No supported deployable units were detected');
432
+ }
433
+
434
+ const uniqueUnits = normalizeUnits(
435
+ uniqueBy(units, (unit) => `${unit.role}:${unit.root}`),
436
+ );
437
+ const datastores = await detectDatastores(repoRoot, uniqueUnits, overrides);
438
+ const layout = classifyLayout(uniqueUnits);
439
+ const projectSlug = slugify(overrides.projectName || deriveSourceName(source));
440
+ const warnings = [];
441
+
442
+ if (!datastores.some((item) => item.role === 'primary')) {
443
+ warnings.push('No primary datastore was detected');
444
+ }
445
+
446
+ return {
447
+ projectSlug,
448
+ layout,
449
+ units: uniqueUnits.map((unit) => ({
450
+ ...unit,
451
+ root: unit.root === '' ? '.' : unit.root,
452
+ })),
453
+ datastores,
454
+ warnings,
455
+ };
456
+ });
457
+ }
458
+
459
+ module.exports = {
460
+ inspectRepository,
461
+ };
@@ -0,0 +1,163 @@
1
+ const DEFAULT_API_BASE_URL = 'https://api.cloudflare.com/client/v4';
2
+
3
+ function normalizeCloudflareDns(config = {}) {
4
+ return {
5
+ apiToken: config.apiToken || undefined,
6
+ apiKey: config.apiKey || undefined,
7
+ email: config.email || undefined,
8
+ zoneId: config.zoneId || undefined,
9
+ targetIp: config.targetIp || undefined,
10
+ proxied: Boolean(config.proxied),
11
+ };
12
+ }
13
+
14
+ function hasCredentials(config) {
15
+ return Boolean(config.apiToken || (config.apiKey && config.email));
16
+ }
17
+
18
+ function createHeaders(config) {
19
+ const headers = {
20
+ 'content-type': 'application/json',
21
+ };
22
+
23
+ if (config.apiToken) {
24
+ headers.authorization = `Bearer ${config.apiToken}`;
25
+ return headers;
26
+ }
27
+
28
+ headers['x-auth-email'] = config.email;
29
+ headers['x-auth-key'] = config.apiKey;
30
+ return headers;
31
+ }
32
+
33
+ async function parseResponse(response) {
34
+ const text = await response.text();
35
+
36
+ if (!text) {
37
+ return {};
38
+ }
39
+
40
+ try {
41
+ return JSON.parse(text);
42
+ } catch {
43
+ return { raw: text };
44
+ }
45
+ }
46
+
47
+ async function requestJson(url, options = {}) {
48
+ const response = await fetch(url, options);
49
+ const payload = await parseResponse(response);
50
+
51
+ if (!response.ok || payload.success === false) {
52
+ const message =
53
+ payload?.errors?.[0]?.message ||
54
+ payload?.errors?.[0]?.error ||
55
+ payload?.messages?.[0]?.message ||
56
+ payload?.raw ||
57
+ `${response.status} ${response.statusText}`;
58
+ throw new Error(message);
59
+ }
60
+
61
+ return payload;
62
+ }
63
+
64
+ async function resolveZoneId(domain, config) {
65
+ if (config.zoneId) {
66
+ return config.zoneId;
67
+ }
68
+
69
+ const parts = String(domain || '')
70
+ .split('.')
71
+ .filter(Boolean);
72
+
73
+ if (parts.length < 2) {
74
+ throw new Error(`Invalid domain: ${domain}`);
75
+ }
76
+
77
+ const headers = createHeaders(config);
78
+
79
+ for (let index = 0; index < parts.length - 1; index += 1) {
80
+ const zoneName = parts.slice(index).join('.');
81
+ const url = new URL(`${DEFAULT_API_BASE_URL}/zones`);
82
+ url.searchParams.set('name', zoneName);
83
+
84
+ const payload = await requestJson(url, {
85
+ method: 'GET',
86
+ headers,
87
+ });
88
+
89
+ if (Array.isArray(payload.result) && payload.result[0]?.id) {
90
+ return payload.result[0].id;
91
+ }
92
+ }
93
+
94
+ throw new Error(`Zone ID not found for domain: ${domain}`);
95
+ }
96
+
97
+ async function upsertCloudflareDnsRecord(input) {
98
+ const config = normalizeCloudflareDns(input);
99
+ const domain = String(input.domain || '').trim().toLowerCase();
100
+ const targetIp = String(config.targetIp || '').trim();
101
+
102
+ if (!domain) {
103
+ throw new Error('A domain is required to publish Cloudflare DNS');
104
+ }
105
+
106
+ if (!targetIp) {
107
+ throw new Error(`A target IP is required to publish Cloudflare DNS for ${domain}`);
108
+ }
109
+
110
+ if (!hasCredentials(config)) {
111
+ throw new Error('Cloudflare credentials are not configured');
112
+ }
113
+
114
+ const zoneId = await resolveZoneId(domain, config);
115
+ const headers = createHeaders(config);
116
+ const listUrl = new URL(
117
+ `${DEFAULT_API_BASE_URL}/zones/${zoneId}/dns_records`,
118
+ );
119
+ listUrl.searchParams.set('type', 'A');
120
+ listUrl.searchParams.set('name', domain);
121
+
122
+ const existing = await requestJson(listUrl, {
123
+ method: 'GET',
124
+ headers,
125
+ });
126
+
127
+ const body = JSON.stringify({
128
+ type: 'A',
129
+ name: domain,
130
+ content: targetIp,
131
+ ttl: 1,
132
+ proxied: Boolean(config.proxied),
133
+ });
134
+
135
+ if (Array.isArray(existing.result) && existing.result[0]?.id) {
136
+ await requestJson(
137
+ `${DEFAULT_API_BASE_URL}/zones/${zoneId}/dns_records/${existing.result[0].id}`,
138
+ {
139
+ method: 'PUT',
140
+ headers,
141
+ body,
142
+ },
143
+ );
144
+ } else {
145
+ await requestJson(`${DEFAULT_API_BASE_URL}/zones/${zoneId}/dns_records`, {
146
+ method: 'POST',
147
+ headers,
148
+ body,
149
+ });
150
+ }
151
+
152
+ return {
153
+ provider: 'cloudflare',
154
+ type: 'A',
155
+ domain,
156
+ targetIp,
157
+ proxied: Boolean(config.proxied),
158
+ };
159
+ }
160
+
161
+ module.exports = {
162
+ upsertCloudflareDnsRecord,
163
+ };