generated from freckle/typescript-action-template
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
14 changed files
with
1,687 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
// https://stackoverflow.com/questions/45222819/can-pseudo-lock-objects-be-used-in-the-amazon-s3-api/75347123#75347123 | ||
|
||
import * as S3 from "@aws-sdk/client-s3"; | ||
import { v4 as uuidv4 } from "uuid"; | ||
|
||
import { Duration } from "./duration"; | ||
import { compareObjects } from "./sort-objects"; | ||
import { normalizePrefix } from "./normalize-prefix"; | ||
|
||
export type AcquireLockResult = "acquired" | "not-acquired"; | ||
|
||
export class S3Lock { | ||
bucket: string; | ||
prefix: string; | ||
name: string; | ||
uuid: string; | ||
expires: Duration; | ||
|
||
private key: string; | ||
private keyPrefix: string; | ||
private s3: S3.S3Client; | ||
|
||
constructor( | ||
bucket: string, | ||
prefix: string, | ||
name: string, | ||
expires: Duration, | ||
uuid?: string, | ||
) { | ||
this.bucket = bucket; | ||
this.prefix = normalizePrefix(prefix); | ||
this.name = name; | ||
this.uuid = uuid ? uuid : uuidv4(); | ||
this.expires = expires; | ||
|
||
this.keyPrefix = `${this.prefix}${this.name}.`; | ||
this.key = `${this.keyPrefix}${this.uuid}`; | ||
this.s3 = new S3.S3Client(); | ||
} | ||
|
||
async acquireLock(): Promise<AcquireLockResult> { | ||
await this.createLock(); | ||
|
||
const output = await this.listLocks(); | ||
const oldestKey = this.getOldestKey(output); | ||
|
||
if (oldestKey === this.key) { | ||
return "acquired"; | ||
} | ||
|
||
await this.releaseLock(); | ||
return "not-acquired"; | ||
} | ||
|
||
async releaseLock(): Promise<void> { | ||
await this.s3.send( | ||
new S3.DeleteObjectCommand({ | ||
Bucket: this.bucket, | ||
Key: this.key, | ||
}), | ||
); | ||
} | ||
|
||
private async createLock(): Promise<void> { | ||
await this.s3.send( | ||
new S3.PutObjectCommand({ | ||
Bucket: this.bucket, | ||
Key: this.key, | ||
Expires: this.expires.after(new Date()), | ||
}), | ||
); | ||
} | ||
|
||
private async listLocks(): Promise<S3.ListObjectsV2Output> { | ||
return await this.s3.send( | ||
new S3.ListObjectsV2Command({ | ||
Bucket: this.bucket, | ||
Prefix: this.keyPrefix, | ||
}), | ||
); | ||
} | ||
|
||
private getOldestKey(output: S3.ListObjectsV2Output): string { | ||
if (output.IsTruncated) { | ||
// If we've got > ~1,000 locks here, something is very wrong | ||
throw new Error("Too many lock objects present"); | ||
} | ||
|
||
const contents = output.Contents ?? []; | ||
|
||
if (contents.length === 0) { | ||
// If our own lock didn't get written/returned, something is very wrong | ||
throw new Error("No lock objects found"); | ||
} | ||
|
||
const sorted = contents.sort(compareObjects); | ||
const sortedKey = sorted[0].Key; | ||
|
||
if (!sortedKey) { | ||
// If the thing doesn't have a Key, something is very wrong | ||
throw new Error("Oldest object has no Key"); | ||
} | ||
|
||
return sortedKey; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
import * as core from "@actions/core"; | ||
|
||
import { S3Lock } from "./S3Lock"; | ||
import { getInputs } from "./inputs"; | ||
import { Timer } from "./timer"; | ||
|
||
async function run() { | ||
try { | ||
const { name, s3Bucket, s3Prefix, expires, timeout, timeoutPoll } = | ||
getInputs(); | ||
|
||
const timer = new Timer(timeout); | ||
const s3Lock = new S3Lock(s3Bucket, s3Prefix, name, expires); | ||
|
||
// Used to instantiate the same S3Lock for release | ||
core.saveState("uuid", s3Lock.uuid); | ||
|
||
while (true) { | ||
let result = await s3Lock.acquireLock(); | ||
|
||
if (result === "acquired") { | ||
break; | ||
} | ||
|
||
if (timer.expired()) { | ||
throw new Error("Lock was not acquired within timeout"); | ||
} | ||
|
||
await timer.sleep(timeoutPoll); | ||
} | ||
|
||
core.setOutput("acquired-at", new Date()); | ||
core.info("Lock acquired"); | ||
} catch (error) { | ||
if (error instanceof Error) { | ||
core.setFailed(error.message); | ||
} else if (typeof error === "string") { | ||
core.setFailed(error); | ||
} else { | ||
core.setFailed("Non-Error exception"); | ||
} | ||
} | ||
} | ||
|
||
run(); |
Oops, something went wrong.