Skip to content

Commit 6e5bd5e

Browse files
Migrate remaining image commands to /preview/v2/images
HAProxy blocks external /v2/* in production (sfcompute/sfcompute#5494), so any caller hitting /v2/images gets a 404. Migrates the remaining image commands off /v2/images: - `sf images list`: use Stainless SDK `client.vms.images.list({ workspace })` (hits /preview/v2/images automatically). - `sf images upload`: swap raw fetch URLs from /v2/images, /v2/images/{id}/parts, /v2/images/{id}/complete to /preview/v2/images*. - `sf images get` and `sf nodes images show`: use SDK `client.vms.images.get(id)` for the lookup; download URL fetch goes to /preview/v2/images/{id}/download via raw fetch since the SDK has no download method. SDK field rename sha256_hash -> sha256. CLI surface (commands, flags, output) is unchanged. Generated with [Indent](https://indent.com) Co-Authored-By: Indent <noreply@indent.com>
1 parent 002768c commit 6e5bd5e

4 files changed

Lines changed: 143 additions & 142 deletions

File tree

src/lib/images/get.tsx

Lines changed: 36 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import timezone from "dayjs/plugin/timezone";
77
import utc from "dayjs/plugin/utc";
88
import { Box, render, Text } from "ink";
99
import Link from "ink-link";
10-
import { apiClient } from "../../apiClient.ts";
11-
import { logAndQuit } from "../../helpers/errors.ts";
10+
import { getAuthToken, loadConfig } from "../../helpers/config.ts";
1211
import { formatDate } from "../../helpers/format-time.ts";
12+
import { handleNodesError, nodesClient } from "../../nodesClient.ts";
1313
import { Row } from "../Row.tsx";
1414

1515
dayjs.extend(utc);
@@ -24,7 +24,7 @@ function ImageDisplay({
2424
name: string;
2525
id: string;
2626
upload_status: string;
27-
sha256_hash: string | null;
27+
sha256: string | null;
2828
};
2929
download: { url: string; expires_at: number } | null;
3030
}) {
@@ -43,7 +43,7 @@ function ImageDisplay({
4343

4444
<Box paddingX={1} flexDirection="column">
4545
<Row head="Status: " value={formatStatusInk(image.upload_status)} />
46-
{image.sha256_hash && <Row head="SHA256: " value={image.sha256_hash} />}
46+
{image.sha256 && <Row head="SHA256: " value={image.sha256} />}
4747
{download && (
4848
<>
4949
<Row
@@ -102,40 +102,43 @@ const get = new Command("get")
102102
.argument("<id>", "Image ID or name")
103103
.option("--json", "Output JSON")
104104
.action(async (id, opts) => {
105-
const client = await apiClient();
105+
try {
106+
const client = await nodesClient();
107+
const image = await client.vms.images.get(id);
106108

107-
const { data: image, response } = await client.GET("/v2/images/{id}", {
108-
params: { path: { id } },
109-
});
110-
if (!response.ok || !image) {
111-
logAndQuit(
112-
`Failed to get image: ${response.status} ${response.statusText}`,
113-
);
114-
}
109+
// Fetch download URL if image is completed
110+
let download: { url: string; expires_at: number } | null = null;
111+
if (image.upload_status === "completed") {
112+
const config = await loadConfig();
113+
const token = await getAuthToken();
114+
const downloadResponse = await fetch(
115+
`${config.api_url}/preview/v2/images/${encodeURIComponent(id)}/download`,
116+
{
117+
headers: { Authorization: `Bearer ${token}` },
118+
},
119+
);
120+
if (downloadResponse.ok) {
121+
download = (await downloadResponse.json()) as {
122+
url: string;
123+
expires_at: number;
124+
};
125+
}
126+
}
115127

116-
// Fetch download URL if image is completed
117-
let download = null;
118-
if (image.upload_status === "completed") {
119-
const { data: downloadData, response: downloadResponse } =
120-
await client.GET("/v2/images/{id}/download", {
121-
params: { path: { id } },
122-
});
123-
if (downloadResponse.ok && downloadData) {
124-
download = downloadData;
128+
if (opts.json) {
129+
console.log(JSON.stringify({ ...image, download }, null, 2));
130+
return;
125131
}
126-
}
127132

128-
if (opts.json) {
129-
console.log(JSON.stringify({ ...image, download }, null, 2));
130-
return;
133+
render(
134+
<ImageDisplay
135+
image={{ ...image, sha256: image.sha256 ?? null }}
136+
download={download}
137+
/>,
138+
);
139+
} catch (err) {
140+
handleNodesError(err);
131141
}
132-
133-
render(
134-
<ImageDisplay
135-
image={{ ...image, sha256_hash: image.sha256_hash ?? null }}
136-
download={download}
137-
/>,
138-
);
139142
});
140143

141144
export default get;

src/lib/images/list.ts

Lines changed: 73 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,8 @@ import { Command } from "@commander-js/extra-typings";
33
import chalk from "chalk";
44
import Table from "cli-table3";
55
import ora from "ora";
6-
import { apiClient } from "../../apiClient.ts";
7-
import { logAndQuit } from "../../helpers/errors.ts";
86
import { formatDate } from "../../helpers/format-time.ts";
7+
import { handleNodesError, nodesClient } from "../../nodesClient.ts";
98
import { getDefaultWorkspace } from "./utils.ts";
109

1110
const list = new Command("list")
@@ -28,83 +27,79 @@ Examples:\n
2827
`,
2928
)
3029
.action(async (options) => {
31-
const client = await apiClient();
32-
const workspace = await getDefaultWorkspace();
33-
34-
const spinner = ora("Fetching images...").start();
35-
const { data: result, response } = await client.GET("/v2/images", {
36-
params: { query: { workspace } },
37-
});
38-
spinner.stop();
39-
40-
if (!response.ok || !result) {
41-
logAndQuit(
42-
`Failed to list images: ${response.status} ${response.statusText}`,
43-
);
44-
}
45-
46-
if (options.json) {
47-
console.log(JSON.stringify(result, null, 2));
48-
return;
49-
}
50-
51-
const images = result.data;
52-
53-
if (images.length === 0) {
54-
console.log("No images found.");
55-
console.log(chalk.gray("\nUpload your first image:"));
56-
console.log(" sf images upload -f ./my-image.img -n my-image");
57-
return;
58-
}
59-
60-
// Sort images by created_at (newest first)
61-
const sortedImages = [...images].sort((a, b) => {
62-
return (b.created_at || 0) - (a.created_at || 0);
63-
});
64-
const imagesToShow = sortedImages.slice(0, 5);
65-
66-
const table = new Table({
67-
head: [
68-
chalk.cyan("NAME"),
69-
chalk.cyan("ID"),
70-
chalk.cyan("STATUS"),
71-
chalk.cyan("CREATED"),
72-
],
73-
style: {
74-
head: [],
75-
border: ["gray"],
76-
},
77-
});
78-
79-
for (const image of imagesToShow) {
80-
const createdAt = image.created_at
81-
? formatDate(new Date(image.created_at * 1000))
82-
: "Unknown";
83-
84-
const status = formatStatus(image.upload_status);
85-
86-
table.push([image.name, image.id, status, createdAt]);
87-
}
88-
89-
if (images.length > 5) {
90-
table.push([
91-
{
92-
colSpan: 4,
93-
content: chalk.blackBright(
94-
`${images.length - 5} older ${
95-
images.length - 5 === 1 ? "image" : "images"
96-
} not shown. Use sf images list --json to list all images.`,
97-
),
30+
try {
31+
const client = await nodesClient();
32+
const workspace = await getDefaultWorkspace();
33+
34+
const spinner = ora("Fetching images...").start();
35+
const result = await client.vms.images.list({ workspace });
36+
spinner.stop();
37+
38+
if (options.json) {
39+
console.log(JSON.stringify(result, null, 2));
40+
return;
41+
}
42+
43+
const images = result.data;
44+
45+
if (images.length === 0) {
46+
console.log("No images found.");
47+
console.log(chalk.gray("\nUpload your first image:"));
48+
console.log(" sf images upload -f ./my-image.img -n my-image");
49+
return;
50+
}
51+
52+
// Sort images by created_at (newest first)
53+
const sortedImages = [...images].sort((a, b) => {
54+
return (b.created_at || 0) - (a.created_at || 0);
55+
});
56+
const imagesToShow = sortedImages.slice(0, 5);
57+
58+
const table = new Table({
59+
head: [
60+
chalk.cyan("NAME"),
61+
chalk.cyan("ID"),
62+
chalk.cyan("STATUS"),
63+
chalk.cyan("CREATED"),
64+
],
65+
style: {
66+
head: [],
67+
border: ["gray"],
9868
},
99-
]);
100-
}
101-
102-
console.log(table.toString());
103-
104-
console.log(chalk.gray("\nNext steps:"));
105-
const firstImage = sortedImages[0];
106-
if (firstImage) {
107-
console.log(` sf images get ${chalk.cyan(firstImage.id)}`);
69+
});
70+
71+
for (const image of imagesToShow) {
72+
const createdAt = image.created_at
73+
? formatDate(new Date(image.created_at * 1000))
74+
: "Unknown";
75+
76+
const status = formatStatus(image.upload_status);
77+
78+
table.push([image.name, image.id, status, createdAt]);
79+
}
80+
81+
if (images.length > 5) {
82+
table.push([
83+
{
84+
colSpan: 4,
85+
content: chalk.blackBright(
86+
`${images.length - 5} older ${
87+
images.length - 5 === 1 ? "image" : "images"
88+
} not shown. Use sf images list --json to list all images.`,
89+
),
90+
},
91+
]);
92+
}
93+
94+
console.log(table.toString());
95+
96+
console.log(chalk.gray("\nNext steps:"));
97+
const firstImage = sortedImages[0];
98+
if (firstImage) {
99+
console.log(` sf images get ${chalk.cyan(firstImage.id)}`);
100+
}
101+
} catch (err) {
102+
handleNodesError(err);
108103
}
109104
});
110105

src/lib/images/upload.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,8 @@ const upload = new Command("upload")
8787

8888
const workspace = await getDefaultWorkspace();
8989

90-
// Create image via v2 API
91-
const startResponse = await fetch(`${config.api_url}/v2/images`, {
90+
// Create image via preview/v2 API
91+
const startResponse = await fetch(`${config.api_url}/preview/v2/images`, {
9292
method: "POST",
9393
headers: apiHeaders,
9494
body: JSON.stringify({ name, workspace }),
@@ -241,9 +241,9 @@ const upload = new Command("upload")
241241
resetPartProgress(part);
242242
}
243243

244-
// Get presigned URL via v2 API
244+
// Get presigned URL via preview/v2 API
245245
const partResponse = await fetch(
246-
`${config.api_url}/v2/images/${imageId}/parts`,
246+
`${config.api_url}/preview/v2/images/${imageId}/parts`,
247247
{
248248
method: "POST",
249249
headers: apiHeaders,
@@ -366,9 +366,9 @@ const upload = new Command("upload")
366366

367367
const sha256Hash = hash.digest("hex");
368368

369-
// Complete upload via v2 API
369+
// Complete upload via preview/v2 API
370370
const completeResponse = await fetch(
371-
`${config.api_url}/v2/images/${imageId}/complete`,
371+
`${config.api_url}/preview/v2/images/${imageId}/complete`,
372372
{
373373
method: "POST",
374374
headers: apiHeaders,

src/lib/nodes/image/show.tsx

Lines changed: 28 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@ import timezone from "dayjs/plugin/timezone";
77
import utc from "dayjs/plugin/utc";
88
import { Box, render, Text } from "ink";
99
import Link from "ink-link";
10-
import { apiClient } from "../../../apiClient.ts";
11-
import { logAndQuit } from "../../../helpers/errors.ts";
10+
import { getAuthToken, loadConfig } from "../../../helpers/config.ts";
1211
import { formatDate } from "../../../helpers/format-time.ts";
12+
import { handleNodesError, nodesClient } from "../../../nodesClient.ts";
1313
import { Row } from "../../Row.tsx";
1414

1515
dayjs.extend(utc);
@@ -124,34 +124,37 @@ const show = new Command("show")
124124
.argument("<image-id>", "ID of the image")
125125
.option("--json", "Output JSON")
126126
.action(async (imageId, opts) => {
127-
const client = await apiClient();
127+
try {
128+
const client = await nodesClient();
129+
const image = await client.vms.images.get(imageId);
128130

129-
const { data: image, response } = await client.GET("/v2/images/{id}", {
130-
params: { path: { id: imageId } },
131-
});
132-
if (!response.ok || !image) {
133-
logAndQuit(
134-
`Failed to get image: ${response.status} ${response.statusText}`,
135-
);
136-
}
131+
let download: { url: string; expires_at: number } | null = null;
132+
if (image.upload_status === "completed") {
133+
const config = await loadConfig();
134+
const token = await getAuthToken();
135+
const downloadResponse = await fetch(
136+
`${config.api_url}/preview/v2/images/${encodeURIComponent(imageId)}/download`,
137+
{
138+
headers: { Authorization: `Bearer ${token}` },
139+
},
140+
);
141+
if (downloadResponse.ok) {
142+
download = (await downloadResponse.json()) as {
143+
url: string;
144+
expires_at: number;
145+
};
146+
}
147+
}
137148

138-
let download = null;
139-
if (image.upload_status === "completed") {
140-
const { data: downloadData, response: downloadResponse } =
141-
await client.GET("/v2/images/{id}/download", {
142-
params: { path: { id: imageId } },
143-
});
144-
if (downloadResponse.ok && downloadData) {
145-
download = downloadData;
149+
if (opts.json) {
150+
console.log(JSON.stringify({ ...image, download }, null, 2));
151+
return;
146152
}
147-
}
148153

149-
if (opts.json) {
150-
console.log(JSON.stringify({ ...image, download }, null, 2));
151-
return;
154+
render(<ImageDisplay image={image} download={download} />);
155+
} catch (err) {
156+
handleNodesError(err);
152157
}
153-
154-
render(<ImageDisplay image={image} download={download} />);
155158
});
156159

157160
export default show;

0 commit comments

Comments
 (0)