@kikimoradev/kcloud 0.17.0 → 0.18.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.
- package/dist/main.js +884 -36
- package/package.json +1 -1
package/dist/main.js
CHANGED
|
@@ -795,6 +795,18 @@ const errorCodes = Object.keys({
|
|
|
795
795
|
"agent.backup_unavailable": "rule_violation",
|
|
796
796
|
"agent.backup_without_export": "rule_violation",
|
|
797
797
|
"agent.backup_in_progress": "conflict",
|
|
798
|
+
"node.machine_missing": "rule_violation",
|
|
799
|
+
"node.operation_running": "conflict",
|
|
800
|
+
"node.operation_not_failed": "conflict",
|
|
801
|
+
"node.size_unavailable": "rule_violation",
|
|
802
|
+
"node.size_not_larger": "rule_violation",
|
|
803
|
+
"node.region_unavailable": "rule_violation",
|
|
804
|
+
"provider.not_found": "not_found",
|
|
805
|
+
"provider.name_taken": "conflict",
|
|
806
|
+
"provider.in_use": "conflict",
|
|
807
|
+
"provider.token_unauthorized": "rule_violation",
|
|
808
|
+
"provider.token_scopes_missing": "rule_violation",
|
|
809
|
+
"provider.unavailable": "rule_violation",
|
|
798
810
|
"project.last_admin": "conflict",
|
|
799
811
|
"agent.node_unassigned": "rule_violation",
|
|
800
812
|
"stream.too_many": "rate_limited",
|
|
@@ -1440,6 +1452,83 @@ pageSchema(z.object({
|
|
|
1440
1452
|
finishedAt: z.iso.datetime().nullable()
|
|
1441
1453
|
}).meta({ id: "TaskCost" })).meta({ id: "TaskCostPage" });
|
|
1442
1454
|
costQuerySchema.extend({ cursor: cursorSchema.optional() });
|
|
1455
|
+
const providerKindSchema = z.enum(["digitalocean"]);
|
|
1456
|
+
const providerAccountNameSchema = z.string().trim().min(1).max(64);
|
|
1457
|
+
const providerTokenSchema = z.string().trim().min(16).max(512).regex(/^[\x21-\x7e]+$/u);
|
|
1458
|
+
const providerRegionSlugSchema = z.string().trim().regex(/^[a-z]{3}[0-9]$/u);
|
|
1459
|
+
const providerSizeSlugSchema = z.string().trim().regex(/^[a-z0-9][a-z0-9-]{0,63}$/u);
|
|
1460
|
+
const providerTokenScopes = [
|
|
1461
|
+
"regions:read",
|
|
1462
|
+
"sizes:read",
|
|
1463
|
+
"image:read",
|
|
1464
|
+
"vpc:read",
|
|
1465
|
+
"actions:read",
|
|
1466
|
+
"droplet:create",
|
|
1467
|
+
"droplet:read",
|
|
1468
|
+
"droplet:update",
|
|
1469
|
+
"droplet:delete",
|
|
1470
|
+
"block_storage:create",
|
|
1471
|
+
"block_storage:read",
|
|
1472
|
+
"block_storage:delete",
|
|
1473
|
+
"block_storage_action:create",
|
|
1474
|
+
"firewall:create",
|
|
1475
|
+
"firewall:read",
|
|
1476
|
+
"firewall:delete",
|
|
1477
|
+
"tag:create",
|
|
1478
|
+
"tag:read",
|
|
1479
|
+
"tag:delete"
|
|
1480
|
+
];
|
|
1481
|
+
const providerTokenScopeSchema = z.enum(providerTokenScopes).meta({ id: "ProviderTokenScope" });
|
|
1482
|
+
const providerVerificationSchema = z.object({
|
|
1483
|
+
checkedAt: z.iso.datetime(),
|
|
1484
|
+
missingScopes: z.array(providerTokenScopeSchema),
|
|
1485
|
+
failure: z.string().nullable()
|
|
1486
|
+
}).meta({ id: "ProviderVerification" });
|
|
1487
|
+
const providerAccountSchema = z.object({
|
|
1488
|
+
id: idSchema,
|
|
1489
|
+
kind: providerKindSchema,
|
|
1490
|
+
name: z.string(),
|
|
1491
|
+
defaultRegion: z.string(),
|
|
1492
|
+
revision: z.int().nonnegative(),
|
|
1493
|
+
verification: providerVerificationSchema,
|
|
1494
|
+
nodeCount: z.int().nonnegative(),
|
|
1495
|
+
createdAt: z.iso.datetime(),
|
|
1496
|
+
updatedAt: z.iso.datetime()
|
|
1497
|
+
}).meta({ id: "ProviderAccount" });
|
|
1498
|
+
z.object({ items: z.array(providerAccountSchema) }).meta({ id: "ProviderAccountList" });
|
|
1499
|
+
const createProviderAccountBodySchema = z.object({
|
|
1500
|
+
kind: providerKindSchema,
|
|
1501
|
+
name: providerAccountNameSchema,
|
|
1502
|
+
token: providerTokenSchema,
|
|
1503
|
+
defaultRegion: providerRegionSlugSchema
|
|
1504
|
+
}).meta({ id: "CreateProviderAccountBody" });
|
|
1505
|
+
z.object({
|
|
1506
|
+
revision: z.int().nonnegative(),
|
|
1507
|
+
name: providerAccountNameSchema.optional(),
|
|
1508
|
+
token: providerTokenSchema.optional(),
|
|
1509
|
+
defaultRegion: providerRegionSlugSchema.optional()
|
|
1510
|
+
}).meta({ id: "UpdateProviderAccountBody" });
|
|
1511
|
+
const providerRegionSchema = z.object({
|
|
1512
|
+
slug: z.string(),
|
|
1513
|
+
name: z.string(),
|
|
1514
|
+
available: z.boolean()
|
|
1515
|
+
}).meta({ id: "ProviderRegion" });
|
|
1516
|
+
const providerSizeSchema = z.object({
|
|
1517
|
+
slug: z.string(),
|
|
1518
|
+
vcpus: z.int().positive(),
|
|
1519
|
+
memoryMb: z.int().positive(),
|
|
1520
|
+
diskGb: z.int().positive(),
|
|
1521
|
+
priceMonthly: z.number().nonnegative(),
|
|
1522
|
+
regions: z.array(z.string())
|
|
1523
|
+
}).meta({ id: "ProviderSize" });
|
|
1524
|
+
z.object({
|
|
1525
|
+
regions: z.array(providerRegionSchema),
|
|
1526
|
+
sizes: z.array(providerSizeSchema)
|
|
1527
|
+
}).meta({ id: "ProviderCatalog" });
|
|
1528
|
+
z.object({
|
|
1529
|
+
organizationId: idSchema,
|
|
1530
|
+
accountId: idSchema
|
|
1531
|
+
});
|
|
1443
1532
|
z.enum(["monitor", "executor"]);
|
|
1444
1533
|
const sessionSlotCountSchema = z.int().min(1).max(64);
|
|
1445
1534
|
const sessionQueueEntrySchema = z.object({
|
|
@@ -1482,7 +1571,8 @@ const nodeStateSchema = z.enum([
|
|
|
1482
1571
|
"online",
|
|
1483
1572
|
"offline"
|
|
1484
1573
|
]);
|
|
1485
|
-
const
|
|
1574
|
+
const nodeProviders = ["self_hosted", "digitalocean"];
|
|
1575
|
+
const nodeProviderSchema = z.enum(nodeProviders);
|
|
1486
1576
|
const nodeNameSchema = z.string().trim().regex(slugPattern);
|
|
1487
1577
|
const nodeHostSchema = z.object({
|
|
1488
1578
|
cpus: z.number().nonnegative(),
|
|
@@ -1557,6 +1647,52 @@ const nodePoolsSchema = z.object({
|
|
|
1557
1647
|
monitor: nodePoolSchema,
|
|
1558
1648
|
executor: nodePoolSchema
|
|
1559
1649
|
}).meta({ id: "NodePools" });
|
|
1650
|
+
const nodeVolumeGbSchema = z.int().min(10).max(4096);
|
|
1651
|
+
const nodeMachineSchema = z.object({
|
|
1652
|
+
account: z.object({
|
|
1653
|
+
id: idSchema,
|
|
1654
|
+
name: z.string().nullable()
|
|
1655
|
+
}),
|
|
1656
|
+
region: z.string(),
|
|
1657
|
+
size: z.string(),
|
|
1658
|
+
volumeGb: z.int().positive(),
|
|
1659
|
+
dropletId: z.int().positive().nullable(),
|
|
1660
|
+
publicIpv4: z.string().nullable()
|
|
1661
|
+
}).meta({ id: "NodeMachine" });
|
|
1662
|
+
const nodeOperationKindSchema = z.enum([
|
|
1663
|
+
"create",
|
|
1664
|
+
"resize",
|
|
1665
|
+
"delete"
|
|
1666
|
+
]);
|
|
1667
|
+
const nodeOperationStateSchema = z.enum(["running", "failed"]);
|
|
1668
|
+
const nodeOperationStepSchema = z.enum([
|
|
1669
|
+
"tag",
|
|
1670
|
+
"firewall",
|
|
1671
|
+
"volume",
|
|
1672
|
+
"droplet",
|
|
1673
|
+
"boot",
|
|
1674
|
+
"installing",
|
|
1675
|
+
"power_off",
|
|
1676
|
+
"resize_droplet",
|
|
1677
|
+
"resize_volume",
|
|
1678
|
+
"power_on",
|
|
1679
|
+
"delete_droplet",
|
|
1680
|
+
"delete_volume",
|
|
1681
|
+
"delete_firewall",
|
|
1682
|
+
"delete_tag"
|
|
1683
|
+
]);
|
|
1684
|
+
const nodeOperationSchema = z.object({
|
|
1685
|
+
kind: nodeOperationKindSchema,
|
|
1686
|
+
state: nodeOperationStateSchema,
|
|
1687
|
+
step: nodeOperationStepSchema.nullable(),
|
|
1688
|
+
error: z.string().nullable(),
|
|
1689
|
+
target: z.object({
|
|
1690
|
+
size: z.string(),
|
|
1691
|
+
volumeGb: z.int().positive()
|
|
1692
|
+
}).nullable(),
|
|
1693
|
+
startedAt: z.iso.datetime(),
|
|
1694
|
+
updatedAt: z.iso.datetime()
|
|
1695
|
+
}).meta({ id: "NodeOperation" });
|
|
1560
1696
|
const nodeSchema = z.object({
|
|
1561
1697
|
id: idSchema,
|
|
1562
1698
|
name: nodeNameSchema,
|
|
@@ -1574,6 +1710,8 @@ const nodeSchema = z.object({
|
|
|
1574
1710
|
lastSeenAt: z.iso.datetime().nullable(),
|
|
1575
1711
|
offlineSince: z.iso.datetime().nullable(),
|
|
1576
1712
|
registrationExpiresAt: z.iso.datetime().nullable(),
|
|
1713
|
+
machine: nodeMachineSchema.nullable(),
|
|
1714
|
+
operation: nodeOperationSchema.nullable(),
|
|
1577
1715
|
createdAt: z.iso.datetime()
|
|
1578
1716
|
}).meta({ id: "Node" });
|
|
1579
1717
|
const nodeUpdateSchema = z.object({
|
|
@@ -1656,15 +1794,34 @@ z.object({
|
|
|
1656
1794
|
revision: z.int().nonnegative(),
|
|
1657
1795
|
split: nodeSplitSchema.nullable()
|
|
1658
1796
|
}).meta({ id: "UpdateNodeSplitBody" });
|
|
1659
|
-
const
|
|
1797
|
+
const nodeMachineRequestSchema = z.object({
|
|
1798
|
+
accountId: idSchema,
|
|
1799
|
+
region: providerRegionSlugSchema,
|
|
1800
|
+
size: providerSizeSlugSchema,
|
|
1801
|
+
volumeGb: nodeVolumeGbSchema
|
|
1802
|
+
}).meta({ id: "NodeMachineRequest" });
|
|
1803
|
+
const createNodeBodySchema = z.object({
|
|
1804
|
+
name: nodeNameSchema,
|
|
1805
|
+
machine: nodeMachineRequestSchema.optional()
|
|
1806
|
+
}).meta({ id: "CreateNodeBody" });
|
|
1807
|
+
const resizeNodeBodySchema = z.object({
|
|
1808
|
+
revision: z.int().nonnegative(),
|
|
1809
|
+
size: providerSizeSlugSchema,
|
|
1810
|
+
volumeGb: nodeVolumeGbSchema
|
|
1811
|
+
}).meta({ id: "ResizeNodeBody" });
|
|
1812
|
+
z.object({ revision: z.int().nonnegative() }).meta({ id: "RetryNodeOperationBody" });
|
|
1660
1813
|
const nodeRegistrationSchema = z.object({
|
|
1661
1814
|
token: z.string(),
|
|
1662
1815
|
expiresAt: z.iso.datetime()
|
|
1663
1816
|
}).meta({ id: "NodeRegistration" });
|
|
1817
|
+
z.object({
|
|
1818
|
+
node: nodeSchema,
|
|
1819
|
+
registration: nodeRegistrationSchema.nullable()
|
|
1820
|
+
}).meta({ id: "CreatedNode" });
|
|
1664
1821
|
z.object({
|
|
1665
1822
|
node: nodeSchema,
|
|
1666
1823
|
registration: nodeRegistrationSchema
|
|
1667
|
-
}).meta({ id: "
|
|
1824
|
+
}).meta({ id: "RenewedNodeRegistration" });
|
|
1668
1825
|
z.object({
|
|
1669
1826
|
token: z.string().min(1).max(256),
|
|
1670
1827
|
version: z.string().min(1).max(128),
|
|
@@ -1672,6 +1829,16 @@ z.object({
|
|
|
1672
1829
|
host: nodeHostSchema.nullable(),
|
|
1673
1830
|
capabilities: nodeCapabilitiesSchema
|
|
1674
1831
|
}).meta({ id: "RegisterNodeBody" });
|
|
1832
|
+
z.object({ token: z.string().min(1).max(256) }).meta({ id: "NodeInstallationBody" });
|
|
1833
|
+
z.object({
|
|
1834
|
+
platformVersion: z.string(),
|
|
1835
|
+
image: z.string().nullable(),
|
|
1836
|
+
registry: z.object({
|
|
1837
|
+
server: z.string(),
|
|
1838
|
+
username: z.string(),
|
|
1839
|
+
password: z.string()
|
|
1840
|
+
}).nullable()
|
|
1841
|
+
}).meta({ id: "NodeInstallation" });
|
|
1675
1842
|
z.object({
|
|
1676
1843
|
nodeId: idSchema,
|
|
1677
1844
|
credential: z.string()
|
|
@@ -1881,6 +2048,18 @@ const auditActionSchema = z.enum([
|
|
|
1881
2048
|
"node.registration_renewed",
|
|
1882
2049
|
"node.deleted",
|
|
1883
2050
|
"node.split_changed",
|
|
2051
|
+
"node.resize_requested",
|
|
2052
|
+
"node.removal_requested",
|
|
2053
|
+
"node.operation_retried",
|
|
2054
|
+
"node.operation_failed",
|
|
2055
|
+
"node.machine_created",
|
|
2056
|
+
"node.machine_resized",
|
|
2057
|
+
"node.machine_removed",
|
|
2058
|
+
"provider_account.created",
|
|
2059
|
+
"provider_account.updated",
|
|
2060
|
+
"provider_account.token_replaced",
|
|
2061
|
+
"provider_account.verified",
|
|
2062
|
+
"provider_account.deleted",
|
|
1884
2063
|
"master_key.reencrypted",
|
|
1885
2064
|
"dead_letter.retried",
|
|
1886
2065
|
"dead_letter.discarded"
|
|
@@ -1901,7 +2080,8 @@ const auditTargetTypeSchema = z.enum([
|
|
|
1901
2080
|
"host_operation",
|
|
1902
2081
|
"volume",
|
|
1903
2082
|
"queue",
|
|
1904
|
-
"node"
|
|
2083
|
+
"node",
|
|
2084
|
+
"provider_account"
|
|
1905
2085
|
]);
|
|
1906
2086
|
const auditEntrySchema = z.object({
|
|
1907
2087
|
id: idSchema,
|
|
@@ -2869,7 +3049,8 @@ const masterKeyVersionSchema = z.object({
|
|
|
2869
3049
|
secrets: z.object({
|
|
2870
3050
|
credentials: secretCountSchema,
|
|
2871
3051
|
toolSecrets: secretCountSchema,
|
|
2872
|
-
platformSecrets: secretCountSchema
|
|
3052
|
+
platformSecrets: secretCountSchema,
|
|
3053
|
+
providerAccounts: secretCountSchema.default(0)
|
|
2873
3054
|
}),
|
|
2874
3055
|
backups: secretCountSchema,
|
|
2875
3056
|
newestBackupExpiresAt: instantSchema$1.nullable()
|
|
@@ -3748,20 +3929,125 @@ const validated = (schema, document, what) => {
|
|
|
3748
3929
|
if (!parsed.success) throw new CliError(`That is not ${what}.`, z.prettifyError(parsed.error));
|
|
3749
3930
|
};
|
|
3750
3931
|
//#endregion
|
|
3932
|
+
//#region src/commands/providers/shared.ts
|
|
3933
|
+
const providerHints = {
|
|
3934
|
+
...organizationHints,
|
|
3935
|
+
"auth.forbidden": "Changing a provider account takes an admin of the organization.",
|
|
3936
|
+
"provider.not_found": "kcloud providers list shows the provider accounts.",
|
|
3937
|
+
"provider.name_taken": "Another provider account of the organization has that name.",
|
|
3938
|
+
"provider.in_use": "Delete the nodes created with it first; kcloud nodes list shows the provider of each node.",
|
|
3939
|
+
"provider.token_unauthorized": "DigitalOcean refused the token. Create a new one under API, Tokens in its control panel.",
|
|
3940
|
+
"concurrency.conflict": "Someone changed the account meanwhile; run the command again."
|
|
3941
|
+
};
|
|
3942
|
+
const reasonOf$1 = (error) => {
|
|
3943
|
+
const reason = error.details.reason;
|
|
3944
|
+
return typeof reason === "string" && reason !== "" ? `: ${reason}` : "";
|
|
3945
|
+
};
|
|
3946
|
+
const missingScopesOf = (error) => {
|
|
3947
|
+
const scopes = error.details.missingScopes;
|
|
3948
|
+
return typeof scopes === "string" && scopes !== "" ? scopes : "some scopes";
|
|
3949
|
+
};
|
|
3950
|
+
const apiErrorOf$1 = (error) => {
|
|
3951
|
+
if (error instanceof ApiError) return error;
|
|
3952
|
+
return error instanceof Error ? apiErrorOf$1(error.cause) : null;
|
|
3953
|
+
};
|
|
3954
|
+
const explained = (error) => {
|
|
3955
|
+
const failure = apiErrorOf$1(error);
|
|
3956
|
+
switch (failure?.code) {
|
|
3957
|
+
case "provider.token_scopes_missing": return new CliError(failure.message, `The token lacks ${missingScopesOf(failure)}. kcloud providers add --help lists every scope it needs.`, { cause: failure });
|
|
3958
|
+
case "provider.unavailable": return new CliError(failure.message, `DigitalOcean did not answer as expected${reasonOf$1(failure)}. Try again in a moment.`, { cause: failure });
|
|
3959
|
+
default: return error;
|
|
3960
|
+
}
|
|
3961
|
+
};
|
|
3962
|
+
const providerDataOf = (action, answer, hints = providerHints) => {
|
|
3963
|
+
try {
|
|
3964
|
+
return dataOf(action, answer, hints);
|
|
3965
|
+
} catch (error) {
|
|
3966
|
+
throw explained(error);
|
|
3967
|
+
}
|
|
3968
|
+
};
|
|
3969
|
+
const providerDoneWith = (action, answer, hints = providerHints) => {
|
|
3970
|
+
try {
|
|
3971
|
+
doneWith(action, answer, hints);
|
|
3972
|
+
} catch (error) {
|
|
3973
|
+
throw explained(error);
|
|
3974
|
+
}
|
|
3975
|
+
};
|
|
3976
|
+
const providerAccountsOf = async (api, organizationId) => providerDataOf("list the provider accounts", await api.GET("/organizations/{organizationId}/provider-accounts", { params: { path: { organizationId } } })).items;
|
|
3977
|
+
const providerAccountOf = async (api, organizationId, accountId) => providerDataOf("describe the provider account", await api.GET("/organizations/{organizationId}/provider-accounts/{accountId}", { params: { path: {
|
|
3978
|
+
organizationId,
|
|
3979
|
+
accountId
|
|
3980
|
+
} } }));
|
|
3981
|
+
const resolveProviderAccount = async (scope, reference) => {
|
|
3982
|
+
const organization = await scope.organization();
|
|
3983
|
+
if (isId(reference)) return providerAccountOf(scope.api, organization.id, reference);
|
|
3984
|
+
const accounts = await providerAccountsOf(scope.api, organization.id);
|
|
3985
|
+
const found = accounts.find((account) => account.name === reference);
|
|
3986
|
+
if (found === void 0) throw new CliError(`There is no provider account ${reference} in ${organization.slug}.`, accounts.length === 0 ? "kcloud providers add <name> --region <slug> adds one." : `kcloud providers list shows ${accounts.map(({ name }) => name).join(", ")}.`);
|
|
3987
|
+
return found;
|
|
3988
|
+
};
|
|
3989
|
+
const verifiedAccount = async (api, organizationId, accountId) => providerDataOf("verify the provider account", await api.POST("/organizations/{organizationId}/provider-accounts/{accountId}/verification", { params: { path: {
|
|
3990
|
+
organizationId,
|
|
3991
|
+
accountId
|
|
3992
|
+
} } }));
|
|
3993
|
+
const providerCatalogOf = async (api, organizationId, accountId) => providerDataOf("read the regions and sizes of the provider", await api.GET("/organizations/{organizationId}/provider-accounts/{accountId}/catalog", { params: { path: {
|
|
3994
|
+
organizationId,
|
|
3995
|
+
accountId
|
|
3996
|
+
} } }));
|
|
3997
|
+
const catalogInRegion = (catalog, region) => {
|
|
3998
|
+
if (!catalog.regions.some(({ slug }) => slug === region)) throw new CliError(`The account offers no region ${region} for a node.`, `Its regions are ${catalog.regions.map(({ slug }) => slug).join(", ")}.`);
|
|
3999
|
+
return {
|
|
4000
|
+
region,
|
|
4001
|
+
regions: catalog.regions,
|
|
4002
|
+
sizes: catalog.sizes.filter(({ regions }) => regions.includes(region))
|
|
4003
|
+
};
|
|
4004
|
+
};
|
|
4005
|
+
const deleteProviderAccount = async (api, organizationId, accountId) => {
|
|
4006
|
+
providerDoneWith("delete the provider account", await api.DELETE("/organizations/{organizationId}/provider-accounts/{accountId}", { params: { path: {
|
|
4007
|
+
organizationId,
|
|
4008
|
+
accountId
|
|
4009
|
+
} } }));
|
|
4010
|
+
};
|
|
4011
|
+
const verificationText = ({ failure, missingScopes }) => {
|
|
4012
|
+
if (failure === "unauthorized") return "token refused";
|
|
4013
|
+
if (failure !== null) return `not checked: ${failure}`;
|
|
4014
|
+
return missingScopes.length === 0 ? "ok" : `missing ${missingScopes.length} of ${providerTokenScopes.length} scopes`;
|
|
4015
|
+
};
|
|
4016
|
+
const scopeLineWidth = 64;
|
|
4017
|
+
const scopeLines = () => providerTokenScopes.reduce((lines, scope) => {
|
|
4018
|
+
const last = lines.at(-1);
|
|
4019
|
+
if (last !== void 0 && last.length + scope.length + 2 <= scopeLineWidth) {
|
|
4020
|
+
lines[lines.length - 1] = `${last}, ${scope}`;
|
|
4021
|
+
return lines;
|
|
4022
|
+
}
|
|
4023
|
+
return [...lines, scope];
|
|
4024
|
+
}, []);
|
|
4025
|
+
//#endregion
|
|
3751
4026
|
//#region src/commands/nodes/shared.ts
|
|
3752
4027
|
const nodeHints = {
|
|
3753
4028
|
...organizationHints,
|
|
3754
4029
|
"node.not_found": "kcloud nodes list shows the nodes of the organization.",
|
|
3755
4030
|
"node.name_taken": "Another node of the organization has that name.",
|
|
3756
4031
|
"node.in_use": "Delete the agents placed on it first; kcloud nodes show <node> lists them.",
|
|
3757
|
-
"node.registration_used": "The node has joined and holds its own credential; it needs no token."
|
|
4032
|
+
"node.registration_used": "The node has joined and holds its own credential; it needs no token.",
|
|
4033
|
+
"node.machine_missing": "The node runs on a host of its own, which the platform does not resize.",
|
|
4034
|
+
"node.operation_running": "The platform is still working on the node; kcloud nodes show <node> shows the step.",
|
|
4035
|
+
"node.operation_not_failed": "Only a failed operation can be retried; kcloud nodes show <node> shows the operation.",
|
|
4036
|
+
"node.region_unavailable": "kcloud providers catalog <account> lists the regions a node can go to.",
|
|
4037
|
+
"node.size_unavailable": "kcloud providers catalog <account> --region <slug> lists the sizes of a region.",
|
|
4038
|
+
"node.size_not_larger": "A node only grows: name a size with more CPUs or memory, or a larger volume.",
|
|
4039
|
+
"concurrency.conflict": "Someone changed the node meanwhile; run the command again."
|
|
4040
|
+
};
|
|
4041
|
+
const machineHints = {
|
|
4042
|
+
...providerHints,
|
|
4043
|
+
...nodeHints,
|
|
4044
|
+
"auth.forbidden": "Creating, resizing and deleting nodes take an admin of the organization."
|
|
3758
4045
|
};
|
|
3759
4046
|
const splitHints = {
|
|
3760
4047
|
...nodeHints,
|
|
3761
4048
|
"auth.forbidden": "Splitting a node takes an admin of its organization.",
|
|
3762
4049
|
"node.host_unknown": "The node reports its CPUs and memory once its service runs; kcloud nodes show <node> shows its state.",
|
|
3763
|
-
"node.split_exceeds_host": "The split asks for more than the host has; kcloud nodes split <node> shows the host."
|
|
3764
|
-
"concurrency.conflict": "Someone changed the node meanwhile; run the command again."
|
|
4050
|
+
"node.split_exceeds_host": "The split asks for more than the host has; kcloud nodes split <node> shows the host."
|
|
3765
4051
|
};
|
|
3766
4052
|
const nodesOf = async (api, organizationId) => dataOf("list the nodes", await api.GET("/organizations/{organizationId}/nodes", { params: { path: { organizationId } } }), nodeHints).items;
|
|
3767
4053
|
const namesOf = (nodes) => nodes.map(({ name }) => name).join(", ");
|
|
@@ -3786,24 +4072,57 @@ const nodeDetailOf = async (api, { organizationId, nodeId }) => dataOf("describe
|
|
|
3786
4072
|
organizationId,
|
|
3787
4073
|
nodeId
|
|
3788
4074
|
} } }), nodeHints);
|
|
3789
|
-
const createdNode = async (api, organizationId,
|
|
3790
|
-
const body = { name };
|
|
4075
|
+
const createdNode = async (api, organizationId, body) => {
|
|
3791
4076
|
validated(createNodeBodySchema, body, "a node");
|
|
3792
|
-
return
|
|
4077
|
+
return providerDataOf("create the node", await api.POST("/organizations/{organizationId}/nodes", {
|
|
3793
4078
|
params: { path: { organizationId } },
|
|
3794
4079
|
body
|
|
3795
|
-
}),
|
|
4080
|
+
}), machineHints);
|
|
3796
4081
|
};
|
|
3797
4082
|
const renewedRegistration = async (api, { organizationId, nodeId }) => dataOf("issue a registration token for the node", await api.POST("/organizations/{organizationId}/nodes/{nodeId}/registration", { params: { path: {
|
|
3798
4083
|
organizationId,
|
|
3799
4084
|
nodeId
|
|
3800
4085
|
} } }), nodeHints);
|
|
3801
4086
|
const deleteNode = async (api, { organizationId, nodeId }) => {
|
|
3802
|
-
|
|
4087
|
+
const answer = await api.DELETE("/organizations/{organizationId}/nodes/{nodeId}", { params: { path: {
|
|
3803
4088
|
organizationId,
|
|
3804
4089
|
nodeId
|
|
3805
|
-
} } })
|
|
4090
|
+
} } });
|
|
4091
|
+
providerDoneWith("delete the node", answer, machineHints);
|
|
4092
|
+
return answer.response.status === 202 ? answer.data ?? null : null;
|
|
4093
|
+
};
|
|
4094
|
+
const resizeTargetOf = (node, { size, volumeGb }) => {
|
|
4095
|
+
const { machine } = node;
|
|
4096
|
+
if (machine === null) throw new CliError(`${node.name} runs on a host of its own, so the platform cannot resize it.`, "Only a node created with --provider digitalocean can be resized.");
|
|
4097
|
+
const target = {
|
|
4098
|
+
size: size ?? machine.size,
|
|
4099
|
+
volumeGb: volumeGb ?? machine.volumeGb
|
|
4100
|
+
};
|
|
4101
|
+
if (target.volumeGb < machine.volumeGb) throw new CliError(`The volume of ${node.name} holds ${machine.volumeGb} GB, and a volume cannot shrink.`);
|
|
4102
|
+
if (target.size === machine.size && target.volumeGb === machine.volumeGb) throw new CliError(`${node.name} is ${machine.size} with a volume of ${machine.volumeGb} GB already.`, "Name a larger size, a larger volume or both.");
|
|
4103
|
+
return target;
|
|
3806
4104
|
};
|
|
4105
|
+
const resizedNode = async (api, { organizationId, nodeId }, node, change) => {
|
|
4106
|
+
const body = {
|
|
4107
|
+
revision: node.revision,
|
|
4108
|
+
...resizeTargetOf(node, change)
|
|
4109
|
+
};
|
|
4110
|
+
validated(resizeNodeBodySchema, body, "a size of a node");
|
|
4111
|
+
return providerDataOf("resize the node", await api.POST("/organizations/{organizationId}/nodes/{nodeId}/resize", {
|
|
4112
|
+
params: { path: {
|
|
4113
|
+
organizationId,
|
|
4114
|
+
nodeId
|
|
4115
|
+
} },
|
|
4116
|
+
body
|
|
4117
|
+
}), machineHints);
|
|
4118
|
+
};
|
|
4119
|
+
const retriedOperation = async (api, { organizationId, nodeId }, revision) => providerDataOf("retry the operation of the node", await api.POST("/organizations/{organizationId}/nodes/{nodeId}/operation/retry", {
|
|
4120
|
+
params: { path: {
|
|
4121
|
+
organizationId,
|
|
4122
|
+
nodeId
|
|
4123
|
+
} },
|
|
4124
|
+
body: { revision }
|
|
4125
|
+
}), machineHints);
|
|
3807
4126
|
const splitNode = async (api, { organizationId, nodeId }, revision, split) => dataOf(split === null ? "clear the split of the node" : "split the node", await api.PUT("/organizations/{organizationId}/nodes/{nodeId}/split", {
|
|
3808
4127
|
params: { path: {
|
|
3809
4128
|
organizationId,
|
|
@@ -6337,7 +6656,7 @@ const nodeTools = [
|
|
|
6337
6656
|
defineTool({
|
|
6338
6657
|
name: "nodes_list",
|
|
6339
6658
|
title: "List nodes",
|
|
6340
|
-
description: "List the nodes of the organization this server acts in, the hosts its agents run on, with their state, engine, version, capabilities, split and
|
|
6659
|
+
description: "List the nodes of the organization this server acts in, the hosts its agents run on, with their provider, state, engine, version, capabilities, split, agent count, the DigitalOcean machine of a node created there, and the operation the platform runs on it.",
|
|
6341
6660
|
operations: ["listNodes"],
|
|
6342
6661
|
safety: "read",
|
|
6343
6662
|
input: z.object({}),
|
|
@@ -6346,7 +6665,7 @@ const nodeTools = [
|
|
|
6346
6665
|
defineTool({
|
|
6347
6666
|
name: "node_show",
|
|
6348
6667
|
title: "Show a node",
|
|
6349
|
-
description: "Show one node with its host, its capabilities, its split, the agents placed on it with their pool limits, and the runtime and pools it reported last.",
|
|
6668
|
+
description: "Show one node with its provider, its DigitalOcean machine and the step and error of its operation, its host, its capabilities, its split, the agents placed on it with their pool limits, and the runtime and pools it reported last.",
|
|
6350
6669
|
operations: ["getNode"],
|
|
6351
6670
|
safety: "read",
|
|
6352
6671
|
input: z.object({ node: nodeField }),
|
|
@@ -6389,17 +6708,51 @@ const nodeTools = [
|
|
|
6389
6708
|
return splitViewOf(await splitNode(run.api, reference, detail.revision, null));
|
|
6390
6709
|
}
|
|
6391
6710
|
}),
|
|
6711
|
+
defineTool({
|
|
6712
|
+
name: "node_resize",
|
|
6713
|
+
title: "Resize a node on DigitalOcean",
|
|
6714
|
+
description: "Give a node created on DigitalOcean a larger droplet size, a larger volume for its agents, or both; a field left out keeps its value, and a node never shrinks. A new size powers the droplet off, so its agents stop until it runs again. Returns the node with the operation that started; node_show follows its steps. Takes an admin of the organization.",
|
|
6715
|
+
operations: ["getNode", "resizeNode"],
|
|
6716
|
+
safety: "destructive",
|
|
6717
|
+
input: z.object({
|
|
6718
|
+
node: nodeField,
|
|
6719
|
+
size: providerSizeSlugSchema.optional().describe("The new droplet size, from provider_catalog."),
|
|
6720
|
+
volumeGb: nodeVolumeGbSchema.optional().describe("The new size of the volume in GB.")
|
|
6721
|
+
}),
|
|
6722
|
+
run: async ({ node, size, volumeGb }, run) => {
|
|
6723
|
+
if (size === void 0 && volumeGb === void 0) throw new CliError("Name a size, a volume or both.");
|
|
6724
|
+
const reference = await resolveNode(run, node);
|
|
6725
|
+
const detail = await nodeDetailOf(run.api, reference);
|
|
6726
|
+
return resizedNode(run.api, reference, detail, {
|
|
6727
|
+
size,
|
|
6728
|
+
volumeGb
|
|
6729
|
+
});
|
|
6730
|
+
}
|
|
6731
|
+
}),
|
|
6732
|
+
defineTool({
|
|
6733
|
+
name: "node_retry",
|
|
6734
|
+
title: "Retry the operation of a node",
|
|
6735
|
+
description: "Run the failed create, resize or delete operation of a node on DigitalOcean again from the step that failed. Returns the node; node_show follows its steps. Takes an admin of the organization.",
|
|
6736
|
+
operations: ["getNode", "retryNodeOperation"],
|
|
6737
|
+
safety: "write",
|
|
6738
|
+
input: z.object({ node: nodeField }),
|
|
6739
|
+
run: async ({ node }, run) => {
|
|
6740
|
+
const reference = await resolveNode(run, node);
|
|
6741
|
+
const detail = await nodeDetailOf(run.api, reference);
|
|
6742
|
+
return retriedOperation(run.api, reference, detail.revision);
|
|
6743
|
+
}
|
|
6744
|
+
}),
|
|
6392
6745
|
defineTool({
|
|
6393
6746
|
name: "node_delete",
|
|
6394
6747
|
title: "Delete a node",
|
|
6395
|
-
description: "Delete a node, after which its host can no longer reach the platform. The instance refuses while agents are placed on it.",
|
|
6748
|
+
description: "Delete a node, after which its host can no longer reach the platform. The instance refuses while agents are placed on it. A self-hosted node is deleted at once and returns deleted; for a node on DigitalOcean the platform destroys its droplet and its volume first and returns the node as removing.",
|
|
6396
6749
|
operations: ["deleteNode"],
|
|
6397
6750
|
safety: "destructive",
|
|
6398
6751
|
input: z.object({ node: nodeField }),
|
|
6399
6752
|
run: async ({ node }, run) => {
|
|
6400
6753
|
const reference = await resolveNode(run, node);
|
|
6401
|
-
await deleteNode(run.api, reference);
|
|
6402
|
-
return { deleted: reference.nodeId };
|
|
6754
|
+
const removing = await deleteNode(run.api, reference);
|
|
6755
|
+
return removing === null ? { deleted: reference.nodeId } : { removing };
|
|
6403
6756
|
}
|
|
6404
6757
|
})
|
|
6405
6758
|
];
|
|
@@ -8127,6 +8480,71 @@ const projectTools = [
|
|
|
8127
8480
|
})
|
|
8128
8481
|
];
|
|
8129
8482
|
//#endregion
|
|
8483
|
+
//#region src/mcp/tools/providers.ts
|
|
8484
|
+
const accountField = z.string().trim().min(1).describe("The provider account, by its name or its id, from providers_list.");
|
|
8485
|
+
const providerTools = [
|
|
8486
|
+
defineTool({
|
|
8487
|
+
name: "providers_list",
|
|
8488
|
+
title: "List provider accounts",
|
|
8489
|
+
description: "List the DigitalOcean accounts of the organization this server acts in, with their default region, the number of nodes created with each, and the last check of their token, never the token itself.",
|
|
8490
|
+
operations: ["listProviderAccounts"],
|
|
8491
|
+
safety: "read",
|
|
8492
|
+
input: z.object({}),
|
|
8493
|
+
run: async (_, run) => providerAccountsOf(run.api, (await run.organization()).id)
|
|
8494
|
+
}),
|
|
8495
|
+
defineTool({
|
|
8496
|
+
name: "provider_show",
|
|
8497
|
+
title: "Show a provider account",
|
|
8498
|
+
description: "Show one provider account with its default region, its node count and the last check of its token: when it ran, the scopes it lacked and why it failed.",
|
|
8499
|
+
operations: ["getProviderAccount"],
|
|
8500
|
+
safety: "read",
|
|
8501
|
+
input: z.object({ account: accountField }),
|
|
8502
|
+
run: async ({ account }, run) => resolveProviderAccount(run, account)
|
|
8503
|
+
}),
|
|
8504
|
+
defineTool({
|
|
8505
|
+
name: "provider_catalog",
|
|
8506
|
+
title: "List regions and sizes",
|
|
8507
|
+
description: "List the regions of a provider account that offer volumes, and the sizes with at least 2 GB of memory a node can have in one region, with CPUs, memory in MB, disk in GB and the monthly price in USD. The region defaults to the default region of the account.",
|
|
8508
|
+
operations: ["getProviderCatalog"],
|
|
8509
|
+
safety: "read",
|
|
8510
|
+
input: z.object({
|
|
8511
|
+
account: accountField,
|
|
8512
|
+
region: providerRegionSlugSchema.optional().describe("The region whose sizes to list, as in fra1.")
|
|
8513
|
+
}),
|
|
8514
|
+
run: async ({ account, region }, run) => {
|
|
8515
|
+
const found = await resolveProviderAccount(run, account);
|
|
8516
|
+
const catalog = await providerCatalogOf(run.api, (await run.organization()).id, found.id);
|
|
8517
|
+
return catalogInRegion(catalog, region ?? found.defaultRegion);
|
|
8518
|
+
}
|
|
8519
|
+
}),
|
|
8520
|
+
defineTool({
|
|
8521
|
+
name: "provider_verify",
|
|
8522
|
+
title: "Check the token of a provider account",
|
|
8523
|
+
description: "Check the token of a provider account against DigitalOcean again and return the account with the result: missingScopes lists the scopes it lacks, and failure is unauthorized when DigitalOcean refused it. Takes an admin of the organization.",
|
|
8524
|
+
operations: ["verifyProviderAccount"],
|
|
8525
|
+
safety: "write",
|
|
8526
|
+
idempotent: true,
|
|
8527
|
+
input: z.object({ account: accountField }),
|
|
8528
|
+
run: async ({ account }, run) => {
|
|
8529
|
+
const found = await resolveProviderAccount(run, account);
|
|
8530
|
+
return verifiedAccount(run.api, (await run.organization()).id, found.id);
|
|
8531
|
+
}
|
|
8532
|
+
}),
|
|
8533
|
+
defineTool({
|
|
8534
|
+
name: "provider_delete",
|
|
8535
|
+
title: "Delete a provider account",
|
|
8536
|
+
description: "Delete a provider account and erase its token. The instance refuses while nodes created with it exist. Takes an admin of the organization.",
|
|
8537
|
+
operations: ["deleteProviderAccount"],
|
|
8538
|
+
safety: "destructive",
|
|
8539
|
+
input: z.object({ account: accountField }),
|
|
8540
|
+
run: async ({ account }, run) => {
|
|
8541
|
+
const found = await resolveProviderAccount(run, account);
|
|
8542
|
+
await deleteProviderAccount(run.api, (await run.organization()).id, found.id);
|
|
8543
|
+
return { deleted: found.id };
|
|
8544
|
+
}
|
|
8545
|
+
})
|
|
8546
|
+
];
|
|
8547
|
+
//#endregion
|
|
8130
8548
|
//#region src/mcp/tools/releases.ts
|
|
8131
8549
|
const releaseTools = [defineTool({
|
|
8132
8550
|
name: "releases_list",
|
|
@@ -8534,6 +8952,7 @@ const kcloudTools = [
|
|
|
8534
8952
|
...credentialTools,
|
|
8535
8953
|
...repositoryTools,
|
|
8536
8954
|
...projectToolTools,
|
|
8955
|
+
...providerTools,
|
|
8537
8956
|
...nodeTools,
|
|
8538
8957
|
...agentTools,
|
|
8539
8958
|
...agentBackupTools,
|
|
@@ -8551,7 +8970,7 @@ const instructions = [
|
|
|
8551
8970
|
"These tools act on one Kikimora Cloud instance, as the account kcloud is signed in with, in one organization of it. whoami names all three.",
|
|
8552
8971
|
"Name a project by its slug, its name or its id. Name an agent as <project>/<agent>, as its slug or name together with project, or by its id.",
|
|
8553
8972
|
"A refusal quotes the error code of the instance. Its hint may name a kcloud command, such as kcloud agents list; the tool for the same resource and verb, agents_list, does that work here.",
|
|
8554
|
-
"Secrets never pass through these tools. For them the person at the terminal runs kcloud credentials add, kcloud credentials rotate, kcloud tokens create, kcloud auth password, kcloud orgs members add, kcloud orgs members reset-password, or sets the secrets of a tool in the panel."
|
|
8973
|
+
"Secrets never pass through these tools. For them the person at the terminal runs kcloud credentials add, kcloud credentials rotate, kcloud providers add, kcloud providers update, kcloud tokens create, kcloud auth password, kcloud orgs members add, kcloud orgs members reset-password, or sets the secrets of a tool in the panel."
|
|
8555
8974
|
].join("\n");
|
|
8556
8975
|
const createKcloudServer = ({ version, open }) => {
|
|
8557
8976
|
const server = new McpServer({
|
|
@@ -8615,6 +9034,7 @@ const createMcpCommand = (adaptersOf, cliVersion) => defineCommand({
|
|
|
8615
9034
|
});
|
|
8616
9035
|
//#endregion
|
|
8617
9036
|
//#region src/commands/nodes/index.ts
|
|
9037
|
+
const defaultVolumeGb = 40;
|
|
8618
9038
|
const gibibytes = (bytes) => `${(bytes / 1024 ** 3).toFixed(1)} GiB`;
|
|
8619
9039
|
const hostOf = (node) => node.host === null ? "-" : `${node.host.cpus} CPU, ${gibibytes(node.host.memoryBytes)} of memory`;
|
|
8620
9040
|
const capabilitiesOf = (node) => {
|
|
@@ -8624,6 +9044,25 @@ const capabilitiesOf = (node) => {
|
|
|
8624
9044
|
};
|
|
8625
9045
|
const registrationOf = (node) => node.registrationExpiresAt === null ? "-" : `a token waits until ${node.registrationExpiresAt}`;
|
|
8626
9046
|
const storageOf = ({ state, freeBytes, totalBytes, path }) => `${state}, ${gibibytes(freeBytes)} free of ${gibibytes(totalBytes)} at ${path}`;
|
|
9047
|
+
const shapeText = ({ size, volumeGb }) => `${size} with a volume of ${volumeGb} GB`;
|
|
9048
|
+
const machineText = (machine) => `${machine.size} in ${machine.region} with a volume of ${machine.volumeGb} GB`;
|
|
9049
|
+
const dropletText = ({ dropletId, publicIpv4 }) => dropletId === null ? "not created yet" : `${dropletId} at ${publicIpv4 ?? "no address yet"}`;
|
|
9050
|
+
const operationText = ({ kind, state, step }) => `${kind} ${state}${step === null ? "" : ` at ${step}`}`;
|
|
9051
|
+
const machineRowsOf = (machine) => machine === null ? [] : [
|
|
9052
|
+
["machine", machineText(machine)],
|
|
9053
|
+
["account", machine.account.name ?? machine.account.id],
|
|
9054
|
+
["droplet", dropletText(machine)]
|
|
9055
|
+
];
|
|
9056
|
+
const operationRowsOf = (operation) => {
|
|
9057
|
+
if (operation === null) return [];
|
|
9058
|
+
const { target, error } = operation;
|
|
9059
|
+
return [
|
|
9060
|
+
["operation", operationText(operation)],
|
|
9061
|
+
...target === null ? [] : [["target", shapeText(target)]],
|
|
9062
|
+
["started", operation.startedAt],
|
|
9063
|
+
...error === null ? [] : [["error", error]]
|
|
9064
|
+
];
|
|
9065
|
+
};
|
|
8627
9066
|
const runtimeRowsOf = (runtime) => [
|
|
8628
9067
|
["reported", runtime.reportedAt],
|
|
8629
9068
|
["docker", runtime.docker.lastError === null ? runtime.docker.state : `${runtime.docker.state}: ${runtime.docker.lastError}`],
|
|
@@ -8654,6 +9093,8 @@ const nodeDetailsText = (node) => [
|
|
|
8654
9093
|
["name", node.name],
|
|
8655
9094
|
["state", node.state],
|
|
8656
9095
|
["provider", node.provider],
|
|
9096
|
+
...machineRowsOf(node.machine),
|
|
9097
|
+
...operationRowsOf(node.operation),
|
|
8657
9098
|
["engine", node.engine ?? "-"],
|
|
8658
9099
|
["version", node.version ?? "-"],
|
|
8659
9100
|
["host", hostOf(node)],
|
|
@@ -8692,11 +9133,13 @@ const nodeDetailsText = (node) => [
|
|
|
8692
9133
|
poolsText(node.runtime.pools)
|
|
8693
9134
|
]
|
|
8694
9135
|
].join("\n");
|
|
8695
|
-
const
|
|
8696
|
-
"
|
|
8697
|
-
`
|
|
8698
|
-
|
|
8699
|
-
|
|
9136
|
+
const installCommandsOf = (origin) => [
|
|
9137
|
+
"Install the node with it, putting the token in place of <token>. On a Linux host:",
|
|
9138
|
+
` curl -fsSLo install-node.sh ${origin}/install-node.sh`,
|
|
9139
|
+
` printf '%s\\n' '<token>' | sudo sh install-node.sh --platform-url ${origin} --registration-token -`,
|
|
9140
|
+
"On Windows, in PowerShell as administrator:",
|
|
9141
|
+
` Invoke-WebRequest -UseBasicParsing -OutFile install-node.ps1 ${origin}/install-node.ps1`,
|
|
9142
|
+
` powershell -NoProfile -ExecutionPolicy Bypass -File .\\install-node.ps1 -PlatformUrl ${origin} -RegistrationToken <token>`
|
|
8700
9143
|
].join("\n");
|
|
8701
9144
|
const printedRegistration = (run, created) => {
|
|
8702
9145
|
if (run.settings.json) {
|
|
@@ -8704,9 +9147,56 @@ const printedRegistration = (run, created) => {
|
|
|
8704
9147
|
return exitCodes.ok;
|
|
8705
9148
|
}
|
|
8706
9149
|
run.context.terminal.line(created.registration.token);
|
|
8707
|
-
run.context.terminal.error([`The registration token of ${created.node.name} is shown only here and works once, until ${created.registration.expiresAt}.`,
|
|
9150
|
+
run.context.terminal.error([`The registration token of ${created.node.name} is shown only here and works once, until ${created.registration.expiresAt}.`, installCommandsOf(run.origin)].join("\n"));
|
|
8708
9151
|
return exitCodes.ok;
|
|
8709
9152
|
};
|
|
9153
|
+
const printedCreation = (run, created) => {
|
|
9154
|
+
const { node, registration } = created;
|
|
9155
|
+
if (registration !== null) return printedRegistration(run, {
|
|
9156
|
+
node,
|
|
9157
|
+
registration
|
|
9158
|
+
});
|
|
9159
|
+
return printed(run, created, () => [`Creating ${node.name} on DigitalOcean${node.machine === null ? "" : `, ${machineText(node.machine)}`}.`, `It joins the platform on its own; kcloud nodes show ${node.name} follows the steps.`].join("\n"));
|
|
9160
|
+
};
|
|
9161
|
+
const createOptions = {
|
|
9162
|
+
provider: { type: "string" },
|
|
9163
|
+
account: { type: "string" },
|
|
9164
|
+
region: { type: "string" },
|
|
9165
|
+
size: { type: "string" },
|
|
9166
|
+
"volume-gb": { type: "string" }
|
|
9167
|
+
};
|
|
9168
|
+
const volumeGbOf = (given) => {
|
|
9169
|
+
if (given === void 0) return void 0;
|
|
9170
|
+
if (!/^[0-9]+$/u.test(given)) throw new UsageError(`--volume-gb ${given} is not a whole number of gigabytes.`);
|
|
9171
|
+
return Number(given);
|
|
9172
|
+
};
|
|
9173
|
+
const machineFlags = [
|
|
9174
|
+
"account",
|
|
9175
|
+
"region",
|
|
9176
|
+
"size",
|
|
9177
|
+
"volume-gb"
|
|
9178
|
+
];
|
|
9179
|
+
const nodeBodyOf = async (run, name, values) => {
|
|
9180
|
+
const provider = nodeProviders.find((candidate) => candidate === (values.provider ?? "self_hosted"));
|
|
9181
|
+
if (provider === void 0) throw new UsageError(`--provider ${values.provider ?? ""} is not a provider of a node.`, `The providers are ${nodeProviders.join(", ")}.`);
|
|
9182
|
+
if (provider === "self_hosted") {
|
|
9183
|
+
const given = machineFlags.filter((flag) => values[flag] !== void 0);
|
|
9184
|
+
if (given.length > 0) throw new UsageError(`${given.map((flag) => `--${flag}`).join(", ")} need --provider digitalocean.`);
|
|
9185
|
+
return { name };
|
|
9186
|
+
}
|
|
9187
|
+
if (values.account === void 0 || values.size === void 0) throw new UsageError("A node on DigitalOcean needs --account <account> and --size <slug>.", "kcloud providers catalog <account> lists the sizes.");
|
|
9188
|
+
const volumeGb = volumeGbOf(values["volume-gb"]) ?? defaultVolumeGb;
|
|
9189
|
+
const account = await resolveProviderAccount(run, values.account);
|
|
9190
|
+
return {
|
|
9191
|
+
name,
|
|
9192
|
+
machine: {
|
|
9193
|
+
accountId: account.id,
|
|
9194
|
+
region: values.region ?? account.defaultRegion,
|
|
9195
|
+
size: values.size,
|
|
9196
|
+
volumeGb
|
|
9197
|
+
}
|
|
9198
|
+
};
|
|
9199
|
+
};
|
|
8710
9200
|
const createNodesCommand = (adaptersOf) => defineGroup({
|
|
8711
9201
|
name: "nodes",
|
|
8712
9202
|
summary: "The nodes of the organization, the hosts its agents run on.",
|
|
@@ -8727,7 +9217,9 @@ const createNodesCommand = (adaptersOf) => defineGroup({
|
|
|
8727
9217
|
const items = await nodesOf(run.api, organization.id);
|
|
8728
9218
|
return printed(run, items, (nodes) => nodes.length === 0 ? `${organization.slug} has no node yet; kcloud nodes create <name> creates one.` : renderTable([
|
|
8729
9219
|
{ header: "NAME" },
|
|
9220
|
+
{ header: "PROVIDER" },
|
|
8730
9221
|
{ header: "STATE" },
|
|
9222
|
+
{ header: "OPERATION" },
|
|
8731
9223
|
{ header: "ENGINE" },
|
|
8732
9224
|
{ header: "VERSION" },
|
|
8733
9225
|
{
|
|
@@ -8738,7 +9230,9 @@ const createNodesCommand = (adaptersOf) => defineGroup({
|
|
|
8738
9230
|
{ header: "ID" }
|
|
8739
9231
|
], nodes.map((node) => [
|
|
8740
9232
|
node.name,
|
|
9233
|
+
node.provider,
|
|
8741
9234
|
node.state,
|
|
9235
|
+
node.operation === null ? "-" : operationText(node.operation),
|
|
8742
9236
|
node.engine ?? "-",
|
|
8743
9237
|
node.version ?? "-",
|
|
8744
9238
|
String(node.agentCount),
|
|
@@ -8749,7 +9243,7 @@ const createNodesCommand = (adaptersOf) => defineGroup({
|
|
|
8749
9243
|
}),
|
|
8750
9244
|
defineCommand({
|
|
8751
9245
|
name: "show",
|
|
8752
|
-
summary: "Show a node with its host, its split,
|
|
9246
|
+
summary: "Show a node with its machine, its host, its split, its agents, its runtime and its pools.",
|
|
8753
9247
|
usage: "nodes show <node>",
|
|
8754
9248
|
optionsUsage: nodeUsage,
|
|
8755
9249
|
options: {},
|
|
@@ -8766,10 +9260,18 @@ const createNodesCommand = (adaptersOf) => defineGroup({
|
|
|
8766
9260
|
createSplitCommand(adaptersOf),
|
|
8767
9261
|
defineCommand({
|
|
8768
9262
|
name: "create",
|
|
8769
|
-
summary: "Create a node
|
|
8770
|
-
usage: "nodes create <name>",
|
|
8771
|
-
optionsUsage: [
|
|
8772
|
-
|
|
9263
|
+
summary: "Create a node on a host of your own, printing its one-time registration token, or on DigitalOcean.",
|
|
9264
|
+
usage: "nodes create <name> [--provider digitalocean --account <account> --size <slug>]",
|
|
9265
|
+
optionsUsage: [
|
|
9266
|
+
["", "the name holds lowercase letters, digits and hyphens"],
|
|
9267
|
+
["--provider <provider>", `one of ${nodeProviders.join(", ")} (default: self_hosted)`],
|
|
9268
|
+
["", "self_hosted: the token goes to stdout alone, how to install to stderr"],
|
|
9269
|
+
["--account <account>", "the DigitalOcean account, by name or id"],
|
|
9270
|
+
["--size <slug>", "the droplet size, as kcloud providers catalog lists them"],
|
|
9271
|
+
["--region <slug>", "the region (default: the region of the account)"],
|
|
9272
|
+
["--volume-gb <n>", `the volume for the agents (default: ${defaultVolumeGb})`]
|
|
9273
|
+
],
|
|
9274
|
+
options: createOptions,
|
|
8773
9275
|
positionals: {
|
|
8774
9276
|
min: 1,
|
|
8775
9277
|
max: 1
|
|
@@ -8777,7 +9279,65 @@ const createNodesCommand = (adaptersOf) => defineGroup({
|
|
|
8777
9279
|
run: async (invocation) => {
|
|
8778
9280
|
const run = await runOf(adaptersOf, invocation);
|
|
8779
9281
|
const organization = await run.organization();
|
|
8780
|
-
|
|
9282
|
+
const body = await nodeBodyOf(run, invocation.positionals[0] ?? "", invocation.values);
|
|
9283
|
+
return printedCreation(run, await createdNode(run.api, organization.id, body));
|
|
9284
|
+
}
|
|
9285
|
+
}),
|
|
9286
|
+
defineCommand({
|
|
9287
|
+
name: "resize",
|
|
9288
|
+
summary: "Give a node on DigitalOcean a larger size, a larger volume or both.",
|
|
9289
|
+
usage: "nodes resize <node> [--size <slug>] [--volume-gb <n>]",
|
|
9290
|
+
optionsUsage: [
|
|
9291
|
+
...nodeUsage,
|
|
9292
|
+
["--size <slug>", "the new droplet size, with more CPUs or memory"],
|
|
9293
|
+
["--volume-gb <n>", "the new size of the volume for the agents"],
|
|
9294
|
+
["", "an option left out keeps its value; a node never shrinks"]
|
|
9295
|
+
],
|
|
9296
|
+
options: {
|
|
9297
|
+
size: { type: "string" },
|
|
9298
|
+
"volume-gb": { type: "string" }
|
|
9299
|
+
},
|
|
9300
|
+
positionals: {
|
|
9301
|
+
min: 1,
|
|
9302
|
+
max: 1
|
|
9303
|
+
},
|
|
9304
|
+
run: async (invocation) => {
|
|
9305
|
+
const { values } = invocation;
|
|
9306
|
+
const change = {
|
|
9307
|
+
size: values.size,
|
|
9308
|
+
volumeGb: volumeGbOf(values["volume-gb"])
|
|
9309
|
+
};
|
|
9310
|
+
if (change.size === void 0 && change.volumeGb === void 0) throw new UsageError("nodes resize needs --size <slug>, --volume-gb <n> or both.");
|
|
9311
|
+
const run = await runOf(adaptersOf, invocation);
|
|
9312
|
+
const given = invocation.positionals[0] ?? "";
|
|
9313
|
+
const reference = await resolveNode(run, given);
|
|
9314
|
+
const node = await nodeDetailOf(run.api, reference);
|
|
9315
|
+
const target = resizeTargetOf(node, change);
|
|
9316
|
+
await confirmed({
|
|
9317
|
+
prompt: run.prompt,
|
|
9318
|
+
settings: run.settings,
|
|
9319
|
+
question: `Resize ${given} to ${shapeText(target)}? ${target.size === node.machine?.size ? "The volume cannot shrink afterwards." : "The droplet is off during the resize, so its agents stop until it runs again, and it cannot shrink afterwards."}`
|
|
9320
|
+
});
|
|
9321
|
+
const resized = await resizedNode(run.api, reference, node, change);
|
|
9322
|
+
return printed(run, resized, (detail) => `Resizing ${detail.name} to ${shapeText(target)}; kcloud nodes show ${detail.name} follows the steps.`);
|
|
9323
|
+
}
|
|
9324
|
+
}),
|
|
9325
|
+
defineCommand({
|
|
9326
|
+
name: "retry",
|
|
9327
|
+
summary: "Retry the failed operation of a node on DigitalOcean from the step that failed.",
|
|
9328
|
+
usage: "nodes retry <node>",
|
|
9329
|
+
optionsUsage: nodeUsage,
|
|
9330
|
+
options: {},
|
|
9331
|
+
positionals: {
|
|
9332
|
+
min: 1,
|
|
9333
|
+
max: 1
|
|
9334
|
+
},
|
|
9335
|
+
run: async (invocation) => {
|
|
9336
|
+
const run = await runOf(adaptersOf, invocation);
|
|
9337
|
+
const reference = await resolveNode(run, invocation.positionals[0] ?? "");
|
|
9338
|
+
const node = await nodeDetailOf(run.api, reference);
|
|
9339
|
+
const retried = await retriedOperation(run.api, reference, node.revision);
|
|
9340
|
+
return printed(run, retried, (detail) => detail.operation === null ? `${detail.name} has no operation left to run.` : `Retrying ${operationText(detail.operation)} of ${detail.name}; kcloud nodes show ${detail.name} follows the steps.`);
|
|
8781
9341
|
}
|
|
8782
9342
|
}),
|
|
8783
9343
|
defineCommand({
|
|
@@ -8798,7 +9358,7 @@ const createNodesCommand = (adaptersOf) => defineGroup({
|
|
|
8798
9358
|
}),
|
|
8799
9359
|
defineCommand({
|
|
8800
9360
|
name: "delete",
|
|
8801
|
-
summary: "Delete a node that runs no agent.",
|
|
9361
|
+
summary: "Delete a node that runs no agent, with its droplet on DigitalOcean.",
|
|
8802
9362
|
usage: "nodes delete <node>",
|
|
8803
9363
|
optionsUsage: nodeUsage,
|
|
8804
9364
|
options: {},
|
|
@@ -8810,13 +9370,300 @@ const createNodesCommand = (adaptersOf) => defineGroup({
|
|
|
8810
9370
|
const run = await runOf(adaptersOf, invocation);
|
|
8811
9371
|
const given = invocation.positionals[0] ?? "";
|
|
8812
9372
|
const reference = await resolveNode(run, given);
|
|
9373
|
+
const node = await nodeDetailOf(run.api, reference);
|
|
9374
|
+
await confirmed({
|
|
9375
|
+
prompt: run.prompt,
|
|
9376
|
+
settings: run.settings,
|
|
9377
|
+
question: node.machine === null ? `Delete the node ${given}? Its host can no longer reach the platform.` : `Delete the node ${given}? The platform destroys its droplet and its volume with the data of its agents.`
|
|
9378
|
+
});
|
|
9379
|
+
const removing = await deleteNode(run.api, reference);
|
|
9380
|
+
if (removing === null) return said(run, `Deleted the node ${given}.`);
|
|
9381
|
+
return printed(run, removing, (removed) => `The removal of ${removed.name} started: the platform destroys its droplet, its volume and its firewall, then forgets the node. kcloud nodes list shows it until then.`);
|
|
9382
|
+
}
|
|
9383
|
+
})
|
|
9384
|
+
]
|
|
9385
|
+
});
|
|
9386
|
+
//#endregion
|
|
9387
|
+
//#region src/commands/providers/index.ts
|
|
9388
|
+
const accountUsage = [["", "an account is named by its name or its id"]];
|
|
9389
|
+
const regionOption = { region: { type: "string" } };
|
|
9390
|
+
const tokenOf = async (run, name, values) => {
|
|
9391
|
+
const token = await secretOf({
|
|
9392
|
+
run,
|
|
9393
|
+
file: values["secret-file"],
|
|
9394
|
+
fromStdin: values["secret-stdin"],
|
|
9395
|
+
question: `The DigitalOcean token of ${name}`,
|
|
9396
|
+
stdinHint: "Pipe the token in, or drop --secret-stdin to be asked for it."
|
|
9397
|
+
});
|
|
9398
|
+
validated(providerTokenSchema, token, "a DigitalOcean token");
|
|
9399
|
+
return token;
|
|
9400
|
+
};
|
|
9401
|
+
const gigabytes = (megabytes) => `${megabytes / 1024} GB`;
|
|
9402
|
+
const accountDetailsText = (account) => [renderTable([{ header: "FIELD" }, { header: "VALUE" }], [
|
|
9403
|
+
["name", account.name],
|
|
9404
|
+
["kind", account.kind],
|
|
9405
|
+
["default region", account.defaultRegion],
|
|
9406
|
+
["nodes", String(account.nodeCount)],
|
|
9407
|
+
["token", verificationText(account.verification)],
|
|
9408
|
+
["checked", account.verification.checkedAt],
|
|
9409
|
+
["created", account.createdAt],
|
|
9410
|
+
["updated", account.updatedAt],
|
|
9411
|
+
["id", account.id]
|
|
9412
|
+
]), ...account.verification.missingScopes.length === 0 ? [] : ["", `The token lacks ${account.verification.missingScopes.join(", ")}.`]].join("\n");
|
|
9413
|
+
const verificationSentence = (account) => {
|
|
9414
|
+
const { failure, missingScopes } = account.verification;
|
|
9415
|
+
if (failure === "unauthorized") return `DigitalOcean refused the token of ${account.name}; kcloud providers update ${account.name} --token replaces it.`;
|
|
9416
|
+
if (failure !== null) return `The token of ${account.name} could not be checked: ${failure}.`;
|
|
9417
|
+
return missingScopes.length === 0 ? `The token of ${account.name} has every scope a node needs.` : `The token of ${account.name} lacks ${missingScopes.join(", ")}.`;
|
|
9418
|
+
};
|
|
9419
|
+
const catalogText = (catalog) => [
|
|
9420
|
+
renderTable([
|
|
9421
|
+
{ header: "REGION" },
|
|
9422
|
+
{ header: "NAME" },
|
|
9423
|
+
{ header: "AVAILABLE" }
|
|
9424
|
+
], catalog.regions.map((region) => [
|
|
9425
|
+
region.slug === catalog.region ? `${region.slug} *` : region.slug,
|
|
9426
|
+
region.name,
|
|
9427
|
+
region.available ? "yes" : "no"
|
|
9428
|
+
])),
|
|
9429
|
+
"",
|
|
9430
|
+
catalog.sizes.length === 0 ? `No size for a node is offered in ${catalog.region}.` : renderTable([
|
|
9431
|
+
{ header: `SIZE IN ${catalog.region.toUpperCase()}` },
|
|
9432
|
+
{
|
|
9433
|
+
header: "CPU",
|
|
9434
|
+
align: "right"
|
|
9435
|
+
},
|
|
9436
|
+
{
|
|
9437
|
+
header: "MEMORY",
|
|
9438
|
+
align: "right"
|
|
9439
|
+
},
|
|
9440
|
+
{
|
|
9441
|
+
header: "DISK",
|
|
9442
|
+
align: "right"
|
|
9443
|
+
},
|
|
9444
|
+
{
|
|
9445
|
+
header: "USD A MONTH",
|
|
9446
|
+
align: "right"
|
|
9447
|
+
}
|
|
9448
|
+
], catalog.sizes.map((size) => [
|
|
9449
|
+
size.slug,
|
|
9450
|
+
String(size.vcpus),
|
|
9451
|
+
gigabytes(size.memoryMb),
|
|
9452
|
+
`${size.diskGb} GB`,
|
|
9453
|
+
size.priceMonthly.toFixed(2)
|
|
9454
|
+
]))
|
|
9455
|
+
].join("\n");
|
|
9456
|
+
const createProvidersCommand = (adaptersOf) => defineGroup({
|
|
9457
|
+
name: "providers",
|
|
9458
|
+
summary: "The cloud accounts the platform creates the nodes of the organization in.",
|
|
9459
|
+
usage: "providers <command>",
|
|
9460
|
+
subcommands: [
|
|
9461
|
+
defineCommand({
|
|
9462
|
+
name: "list",
|
|
9463
|
+
summary: "List the provider accounts of the organization.",
|
|
9464
|
+
usage: "providers list",
|
|
9465
|
+
options: {},
|
|
9466
|
+
positionals: {
|
|
9467
|
+
min: 0,
|
|
9468
|
+
max: 0
|
|
9469
|
+
},
|
|
9470
|
+
run: async (invocation) => {
|
|
9471
|
+
const run = await runOf(adaptersOf, invocation);
|
|
9472
|
+
const organization = await run.organization();
|
|
9473
|
+
const items = await providerAccountsOf(run.api, organization.id);
|
|
9474
|
+
return printed(run, items, (accounts) => accounts.length === 0 ? `${organization.slug} has no provider account yet; kcloud providers add <name> --region <slug> adds one.` : renderTable([
|
|
9475
|
+
{ header: "NAME" },
|
|
9476
|
+
{ header: "KIND" },
|
|
9477
|
+
{ header: "REGION" },
|
|
9478
|
+
{
|
|
9479
|
+
header: "NODES",
|
|
9480
|
+
align: "right"
|
|
9481
|
+
},
|
|
9482
|
+
{ header: "TOKEN" },
|
|
9483
|
+
{ header: "ID" }
|
|
9484
|
+
], accounts.map((account) => [
|
|
9485
|
+
account.name,
|
|
9486
|
+
account.kind,
|
|
9487
|
+
account.defaultRegion,
|
|
9488
|
+
String(account.nodeCount),
|
|
9489
|
+
verificationText(account.verification),
|
|
9490
|
+
account.id
|
|
9491
|
+
])));
|
|
9492
|
+
}
|
|
9493
|
+
}),
|
|
9494
|
+
defineCommand({
|
|
9495
|
+
name: "show",
|
|
9496
|
+
summary: "Show a provider account with the state of its token.",
|
|
9497
|
+
usage: "providers show <account>",
|
|
9498
|
+
optionsUsage: accountUsage,
|
|
9499
|
+
options: {},
|
|
9500
|
+
positionals: {
|
|
9501
|
+
min: 1,
|
|
9502
|
+
max: 1
|
|
9503
|
+
},
|
|
9504
|
+
run: async (invocation) => {
|
|
9505
|
+
const run = await runOf(adaptersOf, invocation);
|
|
9506
|
+
const account = await resolveProviderAccount(run, invocation.positionals[0] ?? "");
|
|
9507
|
+
return printed(run, account, accountDetailsText);
|
|
9508
|
+
}
|
|
9509
|
+
}),
|
|
9510
|
+
defineCommand({
|
|
9511
|
+
name: "add",
|
|
9512
|
+
summary: "Add a DigitalOcean account, asking for its token.",
|
|
9513
|
+
usage: "providers add <name> --region <slug> [--secret-file <path>]",
|
|
9514
|
+
optionsUsage: [
|
|
9515
|
+
["--region <slug>", "the region a new node goes to by default, as in fra1"],
|
|
9516
|
+
...secretUsage,
|
|
9517
|
+
["", "the instance checks the token before it stores it; the token needs"],
|
|
9518
|
+
...scopeLines().map((line) => ["", ` ${line}`])
|
|
9519
|
+
],
|
|
9520
|
+
options: {
|
|
9521
|
+
...regionOption,
|
|
9522
|
+
...secretOptions
|
|
9523
|
+
},
|
|
9524
|
+
positionals: {
|
|
9525
|
+
min: 1,
|
|
9526
|
+
max: 1
|
|
9527
|
+
},
|
|
9528
|
+
run: async (invocation) => {
|
|
9529
|
+
const { values } = invocation;
|
|
9530
|
+
const run = await runOf(adaptersOf, invocation);
|
|
9531
|
+
const organization = await run.organization();
|
|
9532
|
+
const name = invocation.positionals[0] ?? "";
|
|
9533
|
+
if (values.region === void 0) throw new UsageError("providers add needs --region <slug>.", "Name the region a new node goes to by default, as in fra1.");
|
|
9534
|
+
const draft = {
|
|
9535
|
+
kind: "digitalocean",
|
|
9536
|
+
name,
|
|
9537
|
+
defaultRegion: values.region
|
|
9538
|
+
};
|
|
9539
|
+
validated(createProviderAccountBodySchema.omit({ token: true }), draft, "a provider account");
|
|
9540
|
+
const token = await tokenOf(run, name, values);
|
|
9541
|
+
const account = providerDataOf("add the provider account", await run.api.POST("/organizations/{organizationId}/provider-accounts", {
|
|
9542
|
+
params: { path: { organizationId: organization.id } },
|
|
9543
|
+
body: {
|
|
9544
|
+
...draft,
|
|
9545
|
+
token
|
|
9546
|
+
}
|
|
9547
|
+
}));
|
|
9548
|
+
return printed(run, account, (added) => `Added ${added.name} as ${added.id}; its token has every scope a node needs.`);
|
|
9549
|
+
}
|
|
9550
|
+
}),
|
|
9551
|
+
defineCommand({
|
|
9552
|
+
name: "update",
|
|
9553
|
+
summary: "Rename a provider account, change its default region or replace its token.",
|
|
9554
|
+
usage: "providers update <account> [--name <name>] [--region <slug>] [--token]",
|
|
9555
|
+
optionsUsage: [
|
|
9556
|
+
...accountUsage,
|
|
9557
|
+
["--name <name>", "the new name"],
|
|
9558
|
+
["--region <slug>", "the new default region of new nodes"],
|
|
9559
|
+
["--token", "replace the token, asking for the new one"],
|
|
9560
|
+
...secretUsage,
|
|
9561
|
+
["", `the new token needs the ${providerTokenScopes.length} scopes add lists`]
|
|
9562
|
+
],
|
|
9563
|
+
options: {
|
|
9564
|
+
name: { type: "string" },
|
|
9565
|
+
...regionOption,
|
|
9566
|
+
token: {
|
|
9567
|
+
type: "boolean",
|
|
9568
|
+
default: false
|
|
9569
|
+
},
|
|
9570
|
+
...secretOptions
|
|
9571
|
+
},
|
|
9572
|
+
positionals: {
|
|
9573
|
+
min: 1,
|
|
9574
|
+
max: 1
|
|
9575
|
+
},
|
|
9576
|
+
run: async (invocation) => {
|
|
9577
|
+
const { values } = invocation;
|
|
9578
|
+
const replacesToken = values.token || values["secret-stdin"] || values["secret-file"] !== void 0;
|
|
9579
|
+
if (values.name === void 0 && values.region === void 0 && !replacesToken) throw new UsageError("providers update needs --name, --region or --token.");
|
|
9580
|
+
const run = await runOf(adaptersOf, invocation);
|
|
9581
|
+
const organization = await run.organization();
|
|
9582
|
+
const account = await resolveProviderAccount(run, invocation.positionals[0] ?? "");
|
|
9583
|
+
const change = {
|
|
9584
|
+
revision: account.revision,
|
|
9585
|
+
...values.name === void 0 ? {} : { name: values.name },
|
|
9586
|
+
...values.region === void 0 ? {} : { defaultRegion: values.region }
|
|
9587
|
+
};
|
|
9588
|
+
validated(createProviderAccountBodySchema.pick({
|
|
9589
|
+
name: true,
|
|
9590
|
+
defaultRegion: true
|
|
9591
|
+
}).partial(), change, "a change of a provider account");
|
|
9592
|
+
const body = replacesToken ? {
|
|
9593
|
+
...change,
|
|
9594
|
+
token: await tokenOf(run, account.name, values)
|
|
9595
|
+
} : change;
|
|
9596
|
+
const updated = providerDataOf("update the provider account", await run.api.PATCH("/organizations/{organizationId}/provider-accounts/{accountId}", {
|
|
9597
|
+
params: { path: {
|
|
9598
|
+
organizationId: organization.id,
|
|
9599
|
+
accountId: account.id
|
|
9600
|
+
} },
|
|
9601
|
+
body
|
|
9602
|
+
}));
|
|
9603
|
+
return printed(run, updated, (changed) => `Updated ${changed.name}${replacesToken ? "; its new token has every scope a node needs" : ""}.`);
|
|
9604
|
+
}
|
|
9605
|
+
}),
|
|
9606
|
+
defineCommand({
|
|
9607
|
+
name: "verify",
|
|
9608
|
+
summary: "Check the token of a provider account against DigitalOcean again.",
|
|
9609
|
+
usage: "providers verify <account>",
|
|
9610
|
+
optionsUsage: accountUsage,
|
|
9611
|
+
options: {},
|
|
9612
|
+
positionals: {
|
|
9613
|
+
min: 1,
|
|
9614
|
+
max: 1
|
|
9615
|
+
},
|
|
9616
|
+
run: async (invocation) => {
|
|
9617
|
+
const run = await runOf(adaptersOf, invocation);
|
|
9618
|
+
const organization = await run.organization();
|
|
9619
|
+
const account = await resolveProviderAccount(run, invocation.positionals[0] ?? "");
|
|
9620
|
+
return printed(run, await verifiedAccount(run.api, organization.id, account.id), verificationSentence);
|
|
9621
|
+
}
|
|
9622
|
+
}),
|
|
9623
|
+
defineCommand({
|
|
9624
|
+
name: "catalog",
|
|
9625
|
+
summary: "List the regions of a provider account and the sizes a node can have in one.",
|
|
9626
|
+
usage: "providers catalog <account> [--region <slug>]",
|
|
9627
|
+
optionsUsage: [
|
|
9628
|
+
...accountUsage,
|
|
9629
|
+
["--region <slug>", "the region whose sizes to list (default: the account's)"],
|
|
9630
|
+
["", "only regions with volumes and sizes with at least 2 GB of memory"]
|
|
9631
|
+
],
|
|
9632
|
+
options: regionOption,
|
|
9633
|
+
positionals: {
|
|
9634
|
+
min: 1,
|
|
9635
|
+
max: 1
|
|
9636
|
+
},
|
|
9637
|
+
run: async (invocation) => {
|
|
9638
|
+
const run = await runOf(adaptersOf, invocation);
|
|
9639
|
+
const organization = await run.organization();
|
|
9640
|
+
const account = await resolveProviderAccount(run, invocation.positionals[0] ?? "");
|
|
9641
|
+
const catalog = await providerCatalogOf(run.api, organization.id, account.id);
|
|
9642
|
+
return printed(run, catalogInRegion(catalog, invocation.values.region ?? account.defaultRegion), catalogText);
|
|
9643
|
+
}
|
|
9644
|
+
}),
|
|
9645
|
+
defineCommand({
|
|
9646
|
+
name: "delete",
|
|
9647
|
+
summary: "Delete a provider account no node was created with.",
|
|
9648
|
+
usage: "providers delete <account>",
|
|
9649
|
+
optionsUsage: accountUsage,
|
|
9650
|
+
options: {},
|
|
9651
|
+
positionals: {
|
|
9652
|
+
min: 1,
|
|
9653
|
+
max: 1
|
|
9654
|
+
},
|
|
9655
|
+
run: async (invocation) => {
|
|
9656
|
+
const run = await runOf(adaptersOf, invocation);
|
|
9657
|
+
const organization = await run.organization();
|
|
9658
|
+
const given = invocation.positionals[0] ?? "";
|
|
9659
|
+
const account = await resolveProviderAccount(run, given);
|
|
8813
9660
|
await confirmed({
|
|
8814
9661
|
prompt: run.prompt,
|
|
8815
9662
|
settings: run.settings,
|
|
8816
|
-
question: `Delete the
|
|
9663
|
+
question: `Delete the provider account ${given}? The platform erases its token.`
|
|
8817
9664
|
});
|
|
8818
|
-
await
|
|
8819
|
-
return said(run, `Deleted the
|
|
9665
|
+
await deleteProviderAccount(run.api, organization.id, account.id);
|
|
9666
|
+
return said(run, `Deleted the provider account ${given}.`);
|
|
8820
9667
|
}
|
|
8821
9668
|
})
|
|
8822
9669
|
]
|
|
@@ -8868,6 +9715,7 @@ const kcloudCommands = () => [
|
|
|
8868
9715
|
createCredentialsCommand(createAdapters),
|
|
8869
9716
|
createRepositoriesCommand(createAdapters),
|
|
8870
9717
|
createToolsCommand(createAdapters),
|
|
9718
|
+
createProvidersCommand(createAdapters),
|
|
8871
9719
|
createNodesCommand(createAdapters),
|
|
8872
9720
|
createAgentsCommand(createAdapters),
|
|
8873
9721
|
createNotificationsCommand(createAdapters),
|