@ao-tools/pulumi-ao
Version:
A Pulumi provider for AO processes
260 lines (215 loc) • 7.84 kB
text/typescript
import * as Pulumi from "@pulumi/pulumi"
import * as AoConnect from "@permaweb/aoconnect"
import type { SpawnProcessArgs } from "@permaweb/aoconnect/dist/lib/spawn"
import * as Utils from "../utilities"
export interface ProcessProviderInputs {
name: string
owner: string
code: string | undefined
codeId: string | undefined
moduleId: string
schedulerId: string
authorityId: string
tags: Record<string, string>
customTags: Record<string, string>
environment: Record<string, string>
walletPath: string
gatewayUrl: string
}
/**
* Uses aoconnect to manage AO processes
*/
export class ProcessProvider implements Pulumi.dynamic.ResourceProvider {
/**
* Checks that Resource inputs are valid
* Runs every time
*/
async check(
_olds: ProcessProviderInputs,
news: ProcessProviderInputs
): Promise<Pulumi.dynamic.CheckResult> {
const failures: Pulumi.dynamic.CheckFailure[] = []
if (news.code && news.codeId) {
failures.push({
property: "codeId",
reason: "Only one of 'code' or 'codeId' can be set",
})
failures.push({
property: "code",
reason: "Only one of 'code' or 'codeId' can be set",
})
}
if (!news.code && !news.codeId) {
failures.push({
property: "codeId",
reason: "One of 'code' or 'codeId' must be set",
})
failures.push({
property: "code",
reason: "One of 'code' or 'codeId' must be set",
})
}
if (news.codeId && !Utils.isTxId(news.codeId))
failures.push({
property: "codeId",
reason: "ID invalid: " + news.codeId,
})
if (!Utils.isTxId(news.moduleId))
failures.push({
property: "moduleId",
reason: "ID invalid: " + news.moduleId,
})
if (!Utils.isTxId(news.schedulerId))
failures.push({
property: "schedulerId",
reason: "ID invalid: " + news.schedulerId,
})
if (!Utils.isTxId(news.authorityId))
failures.push({
property: "authorityId",
reason: "ID invalid: " + news.authorityId,
})
return { failures }
}
/**
* Loads the current state of a process from AO
* Called by pulumi refresh
*/
async read(id: string, props?: any): Promise<Pulumi.dynamic.ReadResult> {
const processTx = await Utils.loadProcessTx(props.gatewayUrl, id)
const readProps: Partial<ProcessProviderInputs> = {}
readProps.name = processTx.tags.find((t) => t.name === "Name")?.value ?? ""
readProps.codeId =
processTx.tags.find((t) => t.name === "On-Boot")?.value ?? ""
readProps.moduleId =
processTx.tags.find((t) => t.name === "Module")?.value ?? ""
readProps.schedulerId =
processTx.tags.find((t) => t.name === "Scheduler")?.value ?? ""
readProps.authorityId =
processTx.tags.find((t) => t.name === "Authority")?.value ?? ""
readProps.tags = Utils.tagsArrayToObject(processTx.tags)
return { id, props: readProps }
}
/**
* Checks if a process needs to be updated or replaced
* Called after check()
*/
async diff(
_id: string,
olds: ProcessProviderInputs,
news: ProcessProviderInputs
): Promise<Pulumi.dynamic.DiffResult> {
let diffResult: Utils.Mutable<Pulumi.dynamic.DiffResult> = {
changes: false,
}
// changes that require to create a new process
const replaces: string[] = []
if (olds.name !== news.name) replaces.push("name")
if (olds.moduleId !== news.moduleId) replaces.push("moduleId")
if (olds.schedulerId !== news.schedulerId) replaces.push("schedulerId")
if (olds.authorityId !== news.authorityId) replaces.push("authorityId")
let tagsChanged = false
for (let [name, value] of Object.entries(news.customTags)) {
if (!olds.customTags[name] || olds.customTags[name] !== value) {
replaces.push("customTags")
tagsChanged = true
break
}
}
if (!tagsChanged) {
for (let [name, value] of Object.entries(olds.customTags)) {
if (!news.customTags[name] || news.customTags[name] !== value) {
replaces.push("customTags")
break
}
}
}
if (replaces.length > 0) diffResult.replaces = replaces
// changes that can be done via update
const updates =
olds.codeId !== news.codeId ||
olds.code !== news.code ||
JSON.stringify(olds.environment) !== JSON.stringify(news.environment)
diffResult.changes = diffResult.replaces !== undefined || updates
return diffResult
}
/**
* Spawns a new AO process, and sets the environment variables after creation.
* Called after diff() when a new process is created or needs to be replaced.
*/
async create(
inputs: ProcessProviderInputs
): Promise<Pulumi.dynamic.CreateResult> {
const jwkWallet = Utils.loadWallet(inputs.walletPath)
const spawnTags = [
{ name: "Name", value: inputs.name },
{ name: "On-Boot", value: inputs.codeId ?? "Data" },
{ name: "Authority", value: inputs.authorityId },
]
const customTags = Utils.tagsObjectToArray(inputs.customTags)
const allTags = [...customTags, ...spawnTags]
const spawnOptions: SpawnProcessArgs = {
module: inputs.moduleId,
scheduler: inputs.schedulerId,
signer: AoConnect.createDataItemSigner(jwkWallet),
tags: allTags,
}
if (inputs.code) spawnOptions.data = inputs.code
const processId = await AoConnect.spawn(spawnOptions)
const envTable = Object.entries(inputs.environment)
.map(([name, value]) => `["${name}"]="${value}"`)
.join(", ")
const setEnvironmentCode = `Environment = { ${envTable} }`
await Utils.retry(5, () =>
AoConnect.message({
process: processId,
signer: AoConnect.createDataItemSigner(jwkWallet),
tags: [{ name: "Action", value: "Eval" }],
data: setEnvironmentCode,
})
)
const processTx = await Utils.retry(5, () =>
Utils.loadProcessTx(inputs.gatewayUrl, processId)
)
const outputs = {
owner: processTx.owner.address,
tags: Utils.tagsArrayToObject(processTx.tags),
}
return {
id: processId,
outs: { ...inputs, ...outputs },
}
}
/**
* Sends messages to the AO process to update the environment variables and code.
* Called after diff() when a process needs to be updated.
*/
async update(
id: string,
olds: ProcessProviderInputs,
news: ProcessProviderInputs
): Promise<Pulumi.dynamic.UpdateResult> {
let codeUpdate = ""
if (JSON.stringify(news.environment) !== JSON.stringify(olds.environment)) {
const envTable = Object.entries(news.environment)
.map(([name, value]) => `["${name}"]="${value}"`)
.join(", ")
codeUpdate = `Environment = { ${envTable} }
` // newline for additional code
}
if (news.code && !news.codeId && news.code !== olds.code)
codeUpdate += news.code
if (news.codeId && !news.code && news.codeId !== olds.codeId)
codeUpdate += await Utils.loadCode(news.gatewayUrl, news.codeId)
const jwkWallet = Utils.loadWallet(news.walletPath)
const messageId = await AoConnect.message({
process: id,
signer: AoConnect.createDataItemSigner(jwkWallet),
tags: [{ name: "Action", value: "Eval" }],
data: codeUpdate,
})
const result = await AoConnect.result({ process: id, message: messageId })
if (result.Error) throw new Error(result.Error)
return { outs: { ...news, tags: olds.tags, owner: olds.owner } }
}
}