Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: Update example deployment code to use custom Neon provisioning #2243

Merged
merged 7 commits into from
Jan 22, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/workflows/deploy_examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ jobs:
echo "linearlite_read_only=$linearlite_read_only" >> $GITHUB_ENV
else
echo "sst outputs file not found. Exiting."
exit 1
exit 123
fi

- name: Deploy NextJs example
Expand All @@ -68,7 +68,7 @@ jobs:
echo "nextjs=$nextjs" >> $GITHUB_ENV
else
echo "sst outputs file not found. Exiting."
exit 1
exit 123
fi

- name: Add comment to PR
Expand Down
158 changes: 119 additions & 39 deletions examples/linearlite-read-only/sst.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,29 +15,38 @@ export default $config({
version: `6.57.0`,
},
neon: `0.6.3`,
command: `1.0.1`,
},
}
},
async run() {
const project = neon.getProjectOutput({ id: process.env.NEON_PROJECT_ID! })
const base = {

const dbName =
$app.stage === `Production`
? `linearlite-read-only-production`
: `linearlite-read-only-${$app.stage}`

const { ownerName, dbName: resultingDbName } = createNeonDb({
projectId: project.id,
branchId: project.defaultBranchId,
}
dbName,
})

const db = new neon.Database(`linearlite-read-only`, {
...base,
name:
$app.stage === `Production`
? `linearlite-read-only-production`
: `linearlite-read-only-${$app.stage}`,
ownerName: `neondb_owner`,
const databaseUri = getNeonConnectionString({
project,
roleName: ownerName,
databaseName: resultingDbName,
pooled: false,
})

const databaseUri = getNeonDbUri(project, db, false)
try {
databaseUri.apply(applyMigrations)
databaseUri.apply(loadData)
databaseUri
.apply(async (dbUri) => {
await applyMigrations(dbUri)
return dbUri
})
.apply(loadData)

const electricInfo = databaseUri.apply((uri) =>
addDatabaseToElectric(uri)
Expand Down Expand Up @@ -94,33 +103,6 @@ function deployLinearLite(
})
}

function getNeonDbUri(
project: $util.Output<neon.GetProjectResult>,
db: neon.Database,
pooled: boolean
) {
const passwordOutput = neon.getBranchRolePasswordOutput({
projectId: project.id,
branchId: project.defaultBranchId,
roleName: db.ownerName,
})
const endpoint = neon.getBranchEndpointsOutput({
projectId: project.id,
branchId: project.defaultBranchId,
})

const databaseHost = pooled
? endpoint.endpoints?.apply((endpoints) =>
endpoints![0].host.replace(
endpoints![0].id,
endpoints![0].id + `-pooler`
)
)
: project.databaseHost

return $interpolate`postgresql://${passwordOutput.roleName}:${passwordOutput.password}@${databaseHost}/${db.name}?sslmode=require`
}

async function addDatabaseToElectric(
uri: string
): Promise<{ id: string; token: string }> {
Expand All @@ -145,3 +127,101 @@ async function addDatabaseToElectric(

return await result.json()
}

function getNeonConnectionString({
project,
roleName,
databaseName,
pooled,
}: {
project: $util.Output<neon.GetProjectResult>
roleName: $util.Input<string>
databaseName: $util.Input<string>
pooled: boolean
}): $util.Output<string> {
const passwordOutput = neon.getBranchRolePasswordOutput({
projectId: project.id,
branchId: project.defaultBranchId,
roleName: roleName,
})

const endpoint = neon.getBranchEndpointsOutput({
projectId: project.id,
branchId: project.defaultBranchId,
})
const databaseHost = pooled
? endpoint.endpoints?.apply((endpoints) =>
endpoints![0].host.replace(
endpoints![0].id,
endpoints![0].id + `-pooler`
)
)
: project.databaseHost
return $interpolate`postgresql://${passwordOutput.roleName}:${passwordOutput.password}@${databaseHost}/${databaseName}?sslmode=require`
}

/**
* Uses the [Neon API](https://neon.tech/docs/manage/databases) along with
* a Pulumi Command resource and `curl` to create and delete Neon databases.
*/
function createNeonDb({
projectId,
branchId,
dbName,
}: {
projectId: $util.Input<string>
branchId: $util.Input<string>
dbName: $util.Input<string>
}): $util.Output<{
dbName: string
ownerName: string
}> {
if (!process.env.NEON_API_KEY) {
throw new Error(`NEON_API_KEY is not set`)
}

const ownerName = `neondb_owner`

const createCommand = `curl -f -v "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/databases" \
-H 'Accept: application/json' \
-H "Authorization: Bearer $NEON_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"database": {
"name": "'$DATABASE_NAME'",
"owner_name": "${ownerName}"
}
}' \
&& echo " SUCCESS" || echo " FAILURE"`

const updateCommand = `echo "Cannot update Neon database with this provisioning method SUCCESS"`

const deleteCommand = `curl -f -v -X 'DELETE' \
"https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/databases/$DATABASE_NAME" \
-H 'Accept: application/json' \
-H "Authorization: Bearer $NEON_API_KEY" \
&& echo " SUCCESS" || echo " FAILURE"`

const result = new command.local.Command(`neon-db-command:${dbName}`, {
create: createCommand,
update: updateCommand,
delete: deleteCommand,
environment: {
NEON_API_KEY: process.env.NEON_API_KEY,
PROJECT_ID: projectId,
BRANCH_ID: branchId,
DATABASE_NAME: dbName,
},
})
return $resolve([result.stdout, dbName]).apply(([stdout, dbName]) => {
if (stdout.endsWith(`SUCCESS`)) {
console.log(`Created Neon database ${dbName}`)
return {
dbName,
ownerName,
}
} else {
throw new Error(`Failed to create Neon database ${dbName}: ${stdout}`)
}
})
}
155 changes: 117 additions & 38 deletions examples/nextjs/sst.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,34 @@ export default $config({
version: `6.57.0`,
},
neon: `0.6.3`,
command: `1.0.1`,
},
}
},
async run() {
const project = neon.getProjectOutput({ id: process.env.NEON_PROJECT_ID! })
const base = {

const dbName =
$app.stage === `Production` ? `nextjs-production` : `nextjs-${$app.stage}`

const { ownerName, dbName: resultingDbName } = createNeonDb({
projectId: project.id,
branchId: project.defaultBranchId,
}

const db = new neon.Database(`nextjs`, {
...base,
name:
$app.stage === `Production`
? `nextjs-production`
: `nextjs-${$app.stage}`,
ownerName: `neondb_owner`,
dbName,
})

const pooledDatabaseUri = getNeonDbUri(project, db, true)
const databaseUri = getNeonDbUri(project, db, false)
const pooledDatabaseUri = getNeonConnectionString({
project,
roleName: ownerName,
databaseName: resultingDbName,
pooled: true,
})
const databaseUri = getNeonConnectionString({
project,
roleName: ownerName,
databaseName: resultingDbName,
pooled: false,
})
try {
pooledDatabaseUri.apply(applyMigrations)

Expand Down Expand Up @@ -83,32 +90,6 @@ function deployNextJsExample(
})
}

function getNeonDbUri(
project: $util.Output<neon.GetProjectResult>,
db: neon.Database,
pooled: boolean
) {
const passwordOutput = neon.getBranchRolePasswordOutput({
projectId: project.id,
branchId: project.defaultBranchId,
roleName: db.ownerName,
})
const endpoint = neon.getBranchEndpointsOutput({
projectId: project.id,
branchId: project.defaultBranchId,
})
const databaseHost = pooled
? endpoint.endpoints?.apply((endpoints) =>
endpoints![0].host.replace(
endpoints![0].id,
endpoints![0].id + `-pooler`
)
)
: project.databaseHost

return $interpolate`postgresql://${passwordOutput.roleName}:${passwordOutput.password}@${databaseHost}/${db.name}?sslmode=require`
}

async function addDatabaseToElectric(
uri: string
): Promise<{ id: string; token: string }> {
Expand All @@ -133,3 +114,101 @@ async function addDatabaseToElectric(

return await result.json()
}

function getNeonConnectionString({
project,
roleName,
databaseName,
pooled,
}: {
project: $util.Output<neon.GetProjectResult>
roleName: $util.Input<string>
databaseName: $util.Input<string>
pooled: boolean
}): $util.Output<string> {
const passwordOutput = neon.getBranchRolePasswordOutput({
projectId: project.id,
branchId: project.defaultBranchId,
roleName: roleName,
})

const endpoint = neon.getBranchEndpointsOutput({
projectId: project.id,
branchId: project.defaultBranchId,
})
const databaseHost = pooled
? endpoint.endpoints?.apply((endpoints) =>
endpoints![0].host.replace(
endpoints![0].id,
endpoints![0].id + `-pooler`
)
)
: project.databaseHost
return $interpolate`postgresql://${passwordOutput.roleName}:${passwordOutput.password}@${databaseHost}/${databaseName}?sslmode=require`
}

/**
* Uses the [Neon API](https://neon.tech/docs/manage/databases) along with
* a Pulumi Command resource and `curl` to create and delete Neon databases.
*/
function createNeonDb({
projectId,
branchId,
dbName,
}: {
projectId: $util.Input<string>
branchId: $util.Input<string>
dbName: $util.Input<string>
}): $util.Output<{
dbName: string
ownerName: string
}> {
if (!process.env.NEON_API_KEY) {
throw new Error(`NEON_API_KEY is not set`)
}

const ownerName = `neondb_owner`

const createCommand = `curl -f -s "https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/databases" \
-H 'Accept: application/json' \
-H "Authorization: Bearer $NEON_API_KEY" \
-H 'Content-Type: application/json' \
-d '{
"database": {
"name": "'$DATABASE_NAME'",
"owner_name": "${ownerName}"
}
}' \
&& echo " SUCCESS" || echo " FAILURE"`

const updateCommand = `echo "Cannot update Neon database with this provisioning method SUCCESS"`

const deleteCommand = `curl -f -s -X 'DELETE' \
"https://console.neon.tech/api/v2/projects/$PROJECT_ID/branches/$BRANCH_ID/databases/$DATABASE_NAME" \
-H 'Accept: application/json' \
-H "Authorization: Bearer $NEON_API_KEY" \
&& echo " SUCCESS" || echo " FAILURE"`

const result = new command.local.Command(`neon-db-command:${dbName}`, {
create: createCommand,
update: updateCommand,
delete: deleteCommand,
environment: {
NEON_API_KEY: process.env.NEON_API_KEY,
PROJECT_ID: projectId,
BRANCH_ID: branchId,
DATABASE_NAME: dbName,
},
})
return $resolve([result.stdout, dbName]).apply(([stdout, dbName]) => {
if (stdout.endsWith(`SUCCESS`)) {
console.log(`Created Neon database ${dbName}`)
return {
dbName,
ownerName,
}
} else {
throw new Error(`Failed to create Neon database ${dbName}: ${stdout}`)
}
})
}
10 changes: 10 additions & 0 deletions examples/todo-app/sst-env.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/* This file is auto-generated by SST. Do not edit. */
/* tslint:disable */
/* eslint-disable */
/* deno-fmt-ignore-file */
import "sst"
export {}
declare module "sst" {
export interface Resource {
}
}
Loading
Loading