feature: upgrade to Deno 2

#1
This commit is contained in:
Alexander
2024-11-20 18:31:51 +09:00
committed by GitHub
4 changed files with 97 additions and 41 deletions

View File

@@ -7,7 +7,7 @@ name: Docker
on: on:
push: push:
branches: [ "main" ] branches: [ "main", "dev" ]
tags: [ "*" ] tags: [ "*" ]
env: env:

View File

@@ -1,5 +1,5 @@
# Use the official Deno image # Use the official Deno image
FROM denoland/deno:1.46.3 FROM denoland/deno:2.0.6
# Set working directory # Set working directory
WORKDIR /app WORKDIR /app

19
deno.lock generated
View File

@@ -1,14 +1,21 @@
{ {
"version": "3", "version": "4",
"packages": {
"specifiers": { "specifiers": {
"npm:hono": "npm:hono@4.6.10" "jsr:@b-fuze/deno-dom@*": "0.1.48",
"jsr:@libs/xml@*": "6.0.1",
"npm:hono@*": "4.6.10"
},
"jsr": {
"@b-fuze/deno-dom@0.1.48": {
"integrity": "bf5b591aef2e9e9c59adfcbb93a9ecd45bab5b7c8263625beafa5c8f1662e7da"
},
"@libs/xml@6.0.1": {
"integrity": "64af4f93464c77c3e1158fb97c3657779ca554b14f38616b96cde31e22d8a309"
}
}, },
"npm": { "npm": {
"hono@4.6.10": { "hono@4.6.10": {
"integrity": "sha512-IXXNfRAZEahFnWBhUUlqKEGF9upeE6hZoRZszvNkyAz/CYp+iVbxm3viMvStlagRJohjlBRGOQ7f4jfcV0XMGg==", "integrity": "sha512-IXXNfRAZEahFnWBhUUlqKEGF9upeE6hZoRZszvNkyAz/CYp+iVbxm3viMvStlagRJohjlBRGOQ7f4jfcV0XMGg=="
"dependencies": {}
}
} }
}, },
"redirects": { "redirects": {

View File

@@ -2,6 +2,7 @@ import { ListObjectsOptions, ListObjectsResult, StorageObject } from "../types/m
import { S3, type ListObjectsV2Request } from "https://deno.land/x/aws_api@v0.8.1/services/s3/mod.ts"; import { S3, type ListObjectsV2Request } from "https://deno.land/x/aws_api@v0.8.1/services/s3/mod.ts";
import { ApiFactory } from "https://deno.land/x/aws_api@v0.8.1/client/mod.ts"; import { ApiFactory } from "https://deno.land/x/aws_api@v0.8.1/client/mod.ts";
import { getSignedUrl } from "https://deno.land/x/aws_s3_presign@2.2.1/mod.ts"; import { getSignedUrl } from "https://deno.land/x/aws_s3_presign@2.2.1/mod.ts";
import { parse as parseXml } from "jsr:@libs/xml";
interface Credentials { interface Credentials {
awsAccessKeyId: string; awsAccessKeyId: string;
@@ -11,10 +12,14 @@ interface Credentials {
export class StorageService { export class StorageService {
private client: S3; private client: S3;
private credentials: Credentials; private credentials!: Credentials;
constructor(private bucket: string, private region: string) { constructor(private bucket: string, private region: string) {
this.credentials = this.resolveCredentials(); this.initializeClient();
}
private async initializeClient() {
this.credentials = await this.resolveCredentials();
const factory = new ApiFactory({ const factory = new ApiFactory({
region: this.region, region: this.region,
credentials: this.credentials, credentials: this.credentials,
@@ -23,28 +28,35 @@ export class StorageService {
this.client = new S3(factory); this.client = new S3(factory);
} }
private async fetchInstanceProfileCredentials(): Promise<Credentials | null> { // Make sure any method that uses this.client waits for initialization
private async ensureInitialized() {
if (!this.client) {
await this.initializeClient();
}
}
private async fetchInstanceProfileCredentials(): Promise<[Credentials | null, string | null]> {
try { try {
const metadataUrl = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"; const metadataUrl = "http://169.254.169.254/latest/meta-data/iam/security-credentials/";
const roleName = await (await fetch(metadataUrl)).text(); const roleName = await (await fetch(metadataUrl)).text();
const credentials = await (await fetch(`${metadataUrl}${roleName}`)).json(); const credentials = await (await fetch(`${metadataUrl}${roleName}`)).json();
return { return [{
awsAccessKeyId: credentials.AccessKeyId, awsAccessKeyId: credentials.AccessKeyId,
awsSecretKey: credentials.SecretAccessKey, awsSecretKey: credentials.SecretAccessKey,
sessionToken: credentials.Token, sessionToken: credentials.Token,
}; }, null];
} catch { } catch (error) {
return null; return [null, `Instance profile credentials failed: ${error instanceof Error ? error.message : String(error)}`];
} }
} }
private async fetchIRSACredentials(): Promise<Credentials | null> { private async fetchIRSACredentials(): Promise<[Credentials | null, string | null]> {
const roleArn = Deno.env.get("AWS_ROLE_ARN"); const roleArn = Deno.env.get("AWS_ROLE_ARN");
const tokenFile = Deno.env.get("AWS_WEB_IDENTITY_TOKEN_FILE"); const tokenFile = Deno.env.get("AWS_WEB_IDENTITY_TOKEN_FILE");
if (!roleArn || !tokenFile) { if (!roleArn || !tokenFile) {
return null; return [null, "IRSA credentials not configured: missing AWS_ROLE_ARN or AWS_WEB_IDENTITY_TOKEN_FILE"];
} }
try { try {
@@ -52,47 +64,53 @@ export class StorageService {
const sts = new WebIdentityCredentials(roleArn, token); const sts = new WebIdentityCredentials(roleArn, token);
const creds = await sts.getCredentials(); const creds = await sts.getCredentials();
return { return [{
awsAccessKeyId: creds.accessKeyId, awsAccessKeyId: creds.accessKeyId,
awsSecretKey: creds.secretAccessKey, awsSecretKey: creds.secretAccessKey,
sessionToken: creds.sessionToken, sessionToken: creds.sessionToken,
}; }, null];
} catch { } catch (error) {
return null; return [null, `IRSA credentials failed: ${error instanceof Error ? error.message : String(error)}`];
} }
} }
private getStaticCredentials(): Credentials | null { private getStaticCredentials(): [Credentials | null, string | null] {
const accessKeyId = Deno.env.get("AWS_ACCESS_KEY_ID"); const accessKeyId = Deno.env.get("AWS_ACCESS_KEY_ID");
const secretKey = Deno.env.get("AWS_SECRET_ACCESS_KEY"); const secretKey = Deno.env.get("AWS_SECRET_ACCESS_KEY");
if (!accessKeyId || !secretKey) { if (!accessKeyId || !secretKey) {
return null; return [null, "Static credentials not configured: missing AWS_ACCESS_KEY_ID or AWS_SECRET_ACCESS_KEY"];
} }
return { return [{
awsAccessKeyId: accessKeyId, awsAccessKeyId: accessKeyId,
awsSecretKey: secretKey, awsSecretKey: secretKey,
}; }, null];
} }
private async resolveCredentials(): Promise<Credentials> { private async resolveCredentials(): Promise<Credentials> {
const errors: string[] = [];
// Try IRSA first // Try IRSA first
const irsaCreds = await this.fetchIRSACredentials(); const [irsaCreds, irsaError] = await this.fetchIRSACredentials();
if (irsaCreds) return irsaCreds; if (irsaCreds) return irsaCreds;
if (irsaError) errors.push(irsaError);
// Then try static credentials // Then try static credentials
const staticCreds = this.getStaticCredentials(); const [staticCreds, staticError] = this.getStaticCredentials();
if (staticCreds) return staticCreds; if (staticCreds) return staticCreds;
if (staticError) errors.push(staticError);
// Finally try instance profile // Finally try instance profile
const instanceCreds = await this.fetchInstanceProfileCredentials(); const [instanceCreds, instanceError] = await this.fetchInstanceProfileCredentials();
if (instanceCreds) return instanceCreds; if (instanceCreds) return instanceCreds;
if (instanceError) errors.push(instanceError);
throw new Error("No valid AWS credentials found"); throw new Error(`No valid AWS credentials found:\n${errors.join('\n')}`);
} }
async getSignedUrl(key: string, expiresIn = 3600): Promise<string> { async getSignedUrl(key: string, expiresIn = 3600): Promise<string> {
await this.ensureInitialized();
return await getSignedUrl({ return await getSignedUrl({
accessKeyId: this.credentials.awsAccessKeyId, accessKeyId: this.credentials.awsAccessKeyId,
secretAccessKey: this.credentials.awsSecretKey, secretAccessKey: this.credentials.awsSecretKey,
@@ -104,6 +122,7 @@ export class StorageService {
} }
async listObjects(options: ListObjectsOptions): Promise<ListObjectsResult> { async listObjects(options: ListObjectsOptions): Promise<ListObjectsResult> {
await this.ensureInitialized();
try { try {
const params: ListObjectsV2Request = { const params: ListObjectsV2Request = {
Bucket: this.bucket, Bucket: this.bucket,
@@ -169,24 +188,54 @@ class WebIdentityCredentials {
WebIdentityToken: this.token, WebIdentityToken: this.token,
}); });
const response = await fetch(`https://sts.amazonaws.com?${params}`, { const url = `https://sts.amazonaws.com?${params}`;
const response = await fetch(url, {
method: "GET", method: "GET",
}); });
const responseText = await response.text();
if (!response.ok) { if (!response.ok) {
throw new Error(`Failed to assume role: ${await response.text()}`); console.error('IRSA Request Failed:', {
url,
requestHeaders: {
method: "GET",
RoleArn: this.roleArn,
TokenLength: this.token.length,
},
responseStatus: response.status,
responseHeaders: Object.fromEntries(response.headers),
responseBody: responseText
});
throw new Error(`Failed to assume role: ${responseText}`);
} }
const xml = await response.text(); const result = parseXml(responseText);
const result = new DOMParser().parseFromString(xml, "text/xml"); const credentials = result.AssumeRoleWithWebIdentityResponse?.AssumeRoleWithWebIdentityResult?.Credentials;
const credentials = result.querySelector("Credentials"); if (!credentials) {
if (!credentials) throw new Error("No credentials in response"); console.error('IRSA Parsing Failed:', {
parsedXml: JSON.stringify(result, null, 2),
rawResponse: responseText
});
throw new Error("No credentials in response. Full response: " + JSON.stringify(result, null, 2));
}
const { AccessKeyId, SecretAccessKey, SessionToken } = credentials;
if (!AccessKeyId || !SecretAccessKey || !SessionToken) {
console.error('IRSA Missing Fields:', {
hasAccessKeyId: !!AccessKeyId,
hasSecretAccessKey: !!SecretAccessKey,
hasSessionToken: !!SessionToken,
parsedCredentials: credentials
});
throw new Error("Missing required credential fields in response");
}
return { return {
accessKeyId: credentials.querySelector("AccessKeyId")?.textContent ?? "", accessKeyId: AccessKeyId,
secretAccessKey: credentials.querySelector("SecretAccessKey")?.textContent ?? "", secretAccessKey: SecretAccessKey,
sessionToken: credentials.querySelector("SessionToken")?.textContent ?? "", sessionToken: SessionToken,
}; };
} }
} }