Skip to content

Commit 9d9e7d1

Browse files
feat: [ENG-2395] sf nodes create accepts the --any-zone flag for --auto nodes (#231)
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
1 parent becbf88 commit 9d9e7d1

5 files changed

Lines changed: 60 additions & 96 deletions

File tree

deno.lock

Lines changed: 12 additions & 61 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"@commander-js/extra-typings": "^12.1.0",
1111
"@inkjs/ui": "^2.0.0",
1212
"@inquirer/prompts": "^5.1.2",
13-
"@sfcompute/nodes-sdk-alpha": "0.1.0-alpha.22",
13+
"@sfcompute/nodes-sdk-alpha": "0.1.0-alpha.27",
1414
"@types/ms": "^0.7.34",
1515
"async-retry": "^1.3.3",
1616
"axios": "^1.8.4",
@@ -20,7 +20,7 @@
2020
"cli-table3": "0.6.5",
2121
"commander": "^12.1.0",
2222
"date-fns": "^4.1.0",
23-
"dayjs": "^1.11.13",
23+
"dayjs": "^1.11.19",
2424
"dotenv": "^16.4.5",
2525
"ink": "^5.2.0",
2626
"ink-link": "^4.1.0",

src/lib/nodes/create.ts

Lines changed: 39 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import {
2121
pluralizeNodes,
2222
startOrNowOption,
2323
yesOption,
24-
zoneOption,
2524
} from "./utils.ts";
2625
import { handleNodesError, nodesClient } from "../../nodesClient.ts";
2726
import { logAndQuit } from "../../helpers/errors.ts";
@@ -82,7 +81,18 @@ const create = new Command("create")
8281
"[Required: names or --count] Number of nodes to create with auto-generated names",
8382
validateCount,
8483
)
85-
.addOption(zoneOption)
84+
.addOption(
85+
new Option(
86+
"-z, --zone <zone>",
87+
"[Required: zone or --any-zone if --auto is provided] Zone for your nodes",
88+
).conflicts("any-zone"),
89+
)
90+
.addOption(
91+
new Option(
92+
"--any-zone",
93+
"Use any zone that meets requirements",
94+
).conflicts("zone"),
95+
)
8696
.addOption(maxPriceOption)
8797
.addOption(
8898
new Option(
@@ -125,41 +135,42 @@ const create = new Command("create")
125135
.addOption(jsonOption)
126136
.hook("preAction", (command) => {
127137
const names = command.args;
128-
const { count, start, duration, end, auto, reserved } = command
129-
.opts();
138+
const { count, start, duration, end, auto, reserved, anyZone, zone } =
139+
command
140+
.opts();
130141

131142
// Validate arguments
132143
if (names.length === 0 && !count) {
133-
console.error(
144+
command.error(
134145
red("Must specify either node names or use \`--count\` option\n"),
135146
);
136-
command.help();
137-
process.exit(1);
138147
}
139148

140149
if (names.length > 0 && count) {
141150
if (names.length !== count) {
142-
console.error(red(
151+
command.error(red(
143152
`You specified ${names.length} ${
144153
names.length === 1 ? "node name" : "node names"
145154
} but \`--count\` is set to ${count}. The number of names must match the \`count\`.\n`,
146155
));
147-
command.help();
148-
process.exit(1);
149156
}
150157
}
151158

159+
if (auto && !anyZone && !zone) {
160+
command.error(red(
161+
"If --auto is provided, you must specify a zone or use --any-zone\n",
162+
));
163+
}
164+
152165
if (reserved && auto) {
153-
console.error(red("Specify either --reserved or --auto, but not both\n"));
154-
command.help();
155-
process.exit(1);
166+
command.error(red(
167+
"Specify either --reserved or --auto, but not both\n",
168+
));
156169
}
157170

158171
// Validate duration/end like buy command
159172
if (typeof end !== "undefined" && typeof duration !== "undefined") {
160-
console.error(red("Specify either --duration or --end, but not both\n"));
161-
command.help();
162-
process.exit(1);
173+
command.error(red("Specify either --duration or --end, but not both\n"));
163174
}
164175

165176
// Validate that timing flags are only used with reserved nodes
@@ -168,25 +179,21 @@ const create = new Command("create")
168179
(start !== "NOW" || typeof duration !== "undefined" ||
169180
typeof end !== "undefined")
170181
) {
171-
console.error(
182+
command.error(
172183
red(
173184
"Auto-reserved nodes start immediately and cannot have a start time, duration, or end time.\n",
174185
),
175186
);
176-
command.help();
177-
process.exit(1);
178187
}
179188

180189
if (
181190
!auto && typeof duration === "undefined" && typeof end === "undefined"
182191
) {
183-
console.error(
192+
command.error(
184193
red(
185194
"You must specify either --duration or --end to create a reserved node.\n",
186195
),
187196
);
188-
command.help();
189-
process.exit(1);
190197
}
191198
})
192199
.addHelpText(
@@ -250,6 +257,7 @@ async function createNodesAction(
250257
desired_count: count,
251258
max_price_per_node_hour: options.maxPrice * 100,
252259
names: names.length > 0 ? names : undefined,
260+
any_zone: options.anyZone ?? false,
253261
zone: options.zone,
254262
cloud_init_user_data: encodedUserData,
255263
image_id: options.image,
@@ -393,10 +401,13 @@ async function createNodesAction(
393401
confirmationMessage += ` for ~$${
394402
pricePerNodeHour.toFixed(2)
395403
}/node/hr`;
404+
if ("zone" in quote) {
405+
confirmationMessage += ` on ${cyan(quote.zone)}`;
406+
}
396407
} else {
397408
logAndQuit(
398409
red(
399-
"No nodes available matching your requirements. This is likely due to insufficient capacity.",
410+
"No capacity available matching your hardware and pricing requirements. You can view zone capacity at https://sfcompute.com/dashboard/zones.",
400411
),
401412
);
402413
}
@@ -405,6 +416,11 @@ async function createNodesAction(
405416
confirmationMessage += ` for up to $${
406417
options.maxPrice.toFixed(2)
407418
}/node/hr`;
419+
if (options.zone) {
420+
confirmationMessage += ` on ${cyan(options.zone)}`;
421+
} else {
422+
confirmationMessage += ` on any matching zone`;
423+
}
408424
}
409425

410426
// Add node names at the end after a colon

src/lib/nodes/list.tsx

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -311,7 +311,12 @@ function NodeVerboseDisplay({ node }: { node: SFCNodes.Node }) {
311311
<Row head="Type: " value={printNodeType(node.node_type)} />
312312
<Row head="Status: " value={getStatusColor(node.status)} />
313313
<Row head="GPU: " value={node.gpu_type} />
314-
<Row head="Zone: " value={node.zone ?? "Not specified"} />
314+
<Row
315+
head="Zone: "
316+
value={node.zone ?? node.node_type === "autoreserved"
317+
? "Any matching zone"
318+
: "Not specified"}
319+
/>
315320
<Row head="Owner: " value={node.owner} />
316321
</Box>
317322

src/lib/nodes/utils.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,7 @@ export function createNodesTable(
144144
getStatusColor(node.status),
145145
lastVm?.id ?? "",
146146
node.gpu_type,
147-
node.zone || "N/A",
147+
node.zone || (node.node_type === "autoreserved" ? "Any matching" : "N/A"),
148148
startEnd,
149149
maxPrice,
150150
]);
@@ -277,14 +277,6 @@ export const yesOption = new Option(
277277
"Skip confirmation prompt",
278278
);
279279

280-
/**
281-
* Common --zone option for zone selection
282-
*/
283-
export const zoneOption = new Option(
284-
"-z, --zone <zone>",
285-
"[Required] Zone for your nodes",
286-
).makeOptionMandatory();
287-
288280
/**
289281
* Common --max-price option for nodes commands
290282
*/

0 commit comments

Comments
 (0)