chore: import upstream snapshot with attribution
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Migrate Dev DB (push) Has been skipped
CI / Detect Version (push) Has been cancelled
CI / Migrate DB (push) Has been cancelled
CI / Build Dev ECR (./docker/app.Dockerfile, ECR_APP) (push) Has been cancelled
CI / Build Dev ECR (./docker/db.Dockerfile, ECR_MIGRATIONS) (push) Has been cancelled
CI / Build Dev ECR (./docker/pii.Dockerfile, ECR_PII) (push) Has been cancelled
CI / Build Dev ECR (./docker/realtime.Dockerfile, ECR_REALTIME) (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
CI / Build AMD64 (./docker/app.Dockerfile, ECR_APP, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build AMD64 (./docker/db.Dockerfile, ECR_MIGRATIONS, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (./docker/pii.Dockerfile, ECR_PII, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (./docker/realtime.Dockerfile, ECR_REALTIME, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/app.Dockerfile, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/db.Dockerfile, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/pii.Dockerfile, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (./docker/realtime.Dockerfile, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Test and Build (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { CancelUpdateStackCommand, CloudFormationClient } from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { awsCloudformationCancelUpdateStackContract } from '@/lib/api/contracts/tools/aws/cloudformation-cancel-update-stack'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
const logger = createLogger('CloudFormationCancelUpdateStack')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseToolRequest(awsCloudformationCancelUpdateStackContract, request, {
|
||||
errorFormat: 'details',
|
||||
logger,
|
||||
})
|
||||
if (!parsed.success) return parsed.response
|
||||
const validatedData = parsed.data.body
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
logger.info(`Cancelling update for CloudFormation stack "${validatedData.stackName}"`)
|
||||
|
||||
try {
|
||||
const command = new CancelUpdateStackCommand({
|
||||
StackName: validatedData.stackName,
|
||||
})
|
||||
|
||||
await client.send(command)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
message: `Update for stack "${validatedData.stackName}" is being cancelled and rolled back`,
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Failed to cancel CloudFormation stack update')
|
||||
logger.error('CancelUpdateStack failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
import { CloudFormationClient, CreateChangeSetCommand } from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { awsCloudformationCreateChangeSetContract } from '@/lib/api/contracts/tools/aws/cloudformation-create-change-set'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { parseCapabilities, toStackParameters } from '../utils'
|
||||
|
||||
const logger = createLogger('CloudFormationCreateChangeSet')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseToolRequest(awsCloudformationCreateChangeSetContract, request, {
|
||||
errorFormat: 'details',
|
||||
logger,
|
||||
})
|
||||
if (!parsed.success) return parsed.response
|
||||
const validatedData = parsed.data.body
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
logger.info(
|
||||
`Creating change set "${validatedData.changeSetName}" for stack "${validatedData.stackName}"`
|
||||
)
|
||||
|
||||
try {
|
||||
const command = new CreateChangeSetCommand({
|
||||
StackName: validatedData.stackName,
|
||||
ChangeSetName: validatedData.changeSetName,
|
||||
TemplateBody: validatedData.templateBody,
|
||||
UsePreviousTemplate: validatedData.usePreviousTemplate,
|
||||
Parameters: toStackParameters(validatedData.parameters),
|
||||
Capabilities: parseCapabilities(validatedData.capabilities),
|
||||
ChangeSetType: validatedData.changeSetType,
|
||||
Description: validatedData.description,
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
changeSetId: response.Id ?? '',
|
||||
stackId: response.StackId ?? '',
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Failed to create CloudFormation change set')
|
||||
logger.error('CreateChangeSet failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
import { CloudFormationClient, CreateStackCommand } from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { awsCloudformationCreateStackContract } from '@/lib/api/contracts/tools/aws/cloudformation-create-stack'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { parseCapabilities, toStackParameters, toStackTags } from '../utils'
|
||||
|
||||
const logger = createLogger('CloudFormationCreateStack')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseToolRequest(awsCloudformationCreateStackContract, request, {
|
||||
errorFormat: 'details',
|
||||
logger,
|
||||
})
|
||||
if (!parsed.success) return parsed.response
|
||||
const validatedData = parsed.data.body
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
logger.info(`Creating CloudFormation stack "${validatedData.stackName}"`)
|
||||
|
||||
try {
|
||||
const command = new CreateStackCommand({
|
||||
StackName: validatedData.stackName,
|
||||
TemplateBody: validatedData.templateBody,
|
||||
Parameters: toStackParameters(validatedData.parameters),
|
||||
Capabilities: parseCapabilities(validatedData.capabilities),
|
||||
Tags: toStackTags(validatedData.tags),
|
||||
OnFailure: validatedData.onFailure,
|
||||
TimeoutInMinutes: validatedData.timeoutInMinutes,
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
stackId: response.StackId ?? '',
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Failed to create CloudFormation stack')
|
||||
logger.error('CreateStack failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,67 @@
|
||||
import { CloudFormationClient, DeleteStackCommand } from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { awsCloudformationDeleteStackContract } from '@/lib/api/contracts/tools/aws/cloudformation-delete-stack'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
const logger = createLogger('CloudFormationDeleteStack')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseToolRequest(awsCloudformationDeleteStackContract, request, {
|
||||
errorFormat: 'details',
|
||||
logger,
|
||||
})
|
||||
if (!parsed.success) return parsed.response
|
||||
const validatedData = parsed.data.body
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
logger.info(`Deleting CloudFormation stack "${validatedData.stackName}"`)
|
||||
|
||||
try {
|
||||
const retainResources = validatedData.retainResources
|
||||
?.split(',')
|
||||
.map((r) => r.trim())
|
||||
.filter(Boolean)
|
||||
|
||||
const command = new DeleteStackCommand({
|
||||
StackName: validatedData.stackName,
|
||||
...(retainResources && retainResources.length > 0 && { RetainResources: retainResources }),
|
||||
})
|
||||
|
||||
await client.send(command)
|
||||
|
||||
logger.info(
|
||||
`Successfully requested deletion of CloudFormation stack "${validatedData.stackName}"`
|
||||
)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
message: `Deletion of stack "${validatedData.stackName}" has been initiated`,
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Failed to delete CloudFormation stack')
|
||||
logger.error('DeleteStack failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
import { CloudFormationClient, DescribeChangeSetCommand } from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { awsCloudformationDescribeChangeSetContract } from '@/lib/api/contracts/tools/aws/cloudformation-describe-change-set'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
const logger = createLogger('CloudFormationDescribeChangeSet')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseToolRequest(awsCloudformationDescribeChangeSetContract, request, {
|
||||
errorFormat: 'details',
|
||||
logger,
|
||||
})
|
||||
if (!parsed.success) return parsed.response
|
||||
const validatedData = parsed.data.body
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
const command = new DescribeChangeSetCommand({
|
||||
ChangeSetName: validatedData.changeSetName,
|
||||
...(validatedData.stackName && { StackName: validatedData.stackName }),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
const changes = (response.Changes ?? []).map((c) => ({
|
||||
action: c.ResourceChange?.Action,
|
||||
logicalResourceId: c.ResourceChange?.LogicalResourceId,
|
||||
physicalResourceId: c.ResourceChange?.PhysicalResourceId,
|
||||
resourceType: c.ResourceChange?.ResourceType,
|
||||
replacement: c.ResourceChange?.Replacement,
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
changeSetName: response.ChangeSetName,
|
||||
changeSetId: response.ChangeSetId,
|
||||
stackId: response.StackId,
|
||||
stackName: response.StackName,
|
||||
description: response.Description,
|
||||
executionStatus: response.ExecutionStatus,
|
||||
status: response.Status,
|
||||
statusReason: response.StatusReason,
|
||||
creationTime: response.CreationTime?.getTime(),
|
||||
capabilities: response.Capabilities ?? [],
|
||||
changes,
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Failed to describe CloudFormation change set')
|
||||
logger.error('DescribeChangeSet failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,64 @@
|
||||
import {
|
||||
CloudFormationClient,
|
||||
DescribeStackDriftDetectionStatusCommand,
|
||||
} from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { awsCloudformationDescribeStackDriftDetectionStatusContract } from '@/lib/api/contracts/tools/aws/cloudformation-describe-stack-drift-detection-status'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
const logger = createLogger('CloudFormationDescribeStackDriftDetectionStatus')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseToolRequest(
|
||||
awsCloudformationDescribeStackDriftDetectionStatusContract,
|
||||
request,
|
||||
{
|
||||
errorFormat: 'details',
|
||||
logger,
|
||||
}
|
||||
)
|
||||
if (!parsed.success) return parsed.response
|
||||
const validatedData = parsed.data.body
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
const command = new DescribeStackDriftDetectionStatusCommand({
|
||||
StackDriftDetectionId: validatedData.stackDriftDetectionId,
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
stackId: response.StackId ?? '',
|
||||
stackDriftDetectionId: response.StackDriftDetectionId ?? '',
|
||||
stackDriftStatus: response.StackDriftStatus,
|
||||
detectionStatus: response.DetectionStatus ?? 'UNKNOWN',
|
||||
detectionStatusReason: response.DetectionStatusReason,
|
||||
driftedStackResourceCount: response.DriftedStackResourceCount,
|
||||
timestamp: response.Timestamp?.getTime(),
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Failed to describe stack drift detection status')
|
||||
logger.error('DescribeStackDriftDetectionStatus failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,73 @@
|
||||
import {
|
||||
CloudFormationClient,
|
||||
DescribeStackEventsCommand,
|
||||
type StackEvent,
|
||||
} from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { awsCloudformationDescribeStackEventsContract } from '@/lib/api/contracts/tools/aws/cloudformation-describe-stack-events'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
const logger = createLogger('CloudFormationDescribeStackEvents')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseToolRequest(awsCloudformationDescribeStackEventsContract, request, {
|
||||
errorFormat: 'details',
|
||||
logger,
|
||||
})
|
||||
if (!parsed.success) return parsed.response
|
||||
const validatedData = parsed.data.body
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
const limit = validatedData.limit ?? 50
|
||||
|
||||
const allEvents: StackEvent[] = []
|
||||
let nextToken: string | undefined
|
||||
do {
|
||||
const command = new DescribeStackEventsCommand({
|
||||
StackName: validatedData.stackName,
|
||||
...(nextToken && { NextToken: nextToken }),
|
||||
})
|
||||
const response = await client.send(command)
|
||||
allEvents.push(...(response.StackEvents ?? []))
|
||||
nextToken = allEvents.length >= limit ? undefined : response.NextToken
|
||||
} while (nextToken)
|
||||
|
||||
const events = allEvents.slice(0, limit).map((e) => ({
|
||||
stackId: e.StackId ?? '',
|
||||
eventId: e.EventId ?? '',
|
||||
stackName: e.StackName ?? '',
|
||||
logicalResourceId: e.LogicalResourceId,
|
||||
physicalResourceId: e.PhysicalResourceId,
|
||||
resourceType: e.ResourceType,
|
||||
resourceStatus: e.ResourceStatus,
|
||||
resourceStatusReason: e.ResourceStatusReason,
|
||||
timestamp: e.Timestamp?.getTime(),
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { events },
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Failed to describe CloudFormation stack events')
|
||||
logger.error('DescribeStackEvents failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,85 @@
|
||||
import {
|
||||
CloudFormationClient,
|
||||
DescribeStacksCommand,
|
||||
type Stack,
|
||||
} from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { awsCloudformationDescribeStacksContract } from '@/lib/api/contracts/tools/aws/cloudformation-describe-stacks'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
const logger = createLogger('CloudFormationDescribeStacks')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseToolRequest(awsCloudformationDescribeStacksContract, request, {
|
||||
errorFormat: 'details',
|
||||
logger,
|
||||
})
|
||||
if (!parsed.success) return parsed.response
|
||||
const validatedData = parsed.data.body
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
const allStacks: Stack[] = []
|
||||
let nextToken: string | undefined
|
||||
do {
|
||||
const command = new DescribeStacksCommand({
|
||||
...(validatedData.stackName && { StackName: validatedData.stackName }),
|
||||
...(nextToken && { NextToken: nextToken }),
|
||||
})
|
||||
const response = await client.send(command)
|
||||
allStacks.push(...(response.Stacks ?? []))
|
||||
nextToken = response.NextToken
|
||||
} while (nextToken)
|
||||
|
||||
const stacks = allStacks.map((s) => ({
|
||||
stackName: s.StackName ?? '',
|
||||
stackId: s.StackId ?? '',
|
||||
stackStatus: s.StackStatus ?? 'UNKNOWN',
|
||||
stackStatusReason: s.StackStatusReason,
|
||||
creationTime: s.CreationTime?.getTime(),
|
||||
lastUpdatedTime: s.LastUpdatedTime?.getTime(),
|
||||
description: s.Description,
|
||||
enableTerminationProtection: s.EnableTerminationProtection,
|
||||
driftInformation: s.DriftInformation
|
||||
? {
|
||||
stackDriftStatus: s.DriftInformation.StackDriftStatus,
|
||||
lastCheckTimestamp: s.DriftInformation.LastCheckTimestamp?.getTime(),
|
||||
}
|
||||
: null,
|
||||
outputs: (s.Outputs ?? []).map((o) => ({
|
||||
outputKey: o.OutputKey ?? '',
|
||||
outputValue: o.OutputValue ?? '',
|
||||
description: o.Description,
|
||||
})),
|
||||
tags: (s.Tags ?? []).map((t) => ({
|
||||
key: t.Key ?? '',
|
||||
value: t.Value ?? '',
|
||||
})),
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { stacks },
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Failed to describe CloudFormation stacks')
|
||||
logger.error('DescribeStacks failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,55 @@
|
||||
import { CloudFormationClient, DetectStackDriftCommand } from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { awsCloudformationDetectStackDriftContract } from '@/lib/api/contracts/tools/aws/cloudformation-detect-stack-drift'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
const logger = createLogger('CloudFormationDetectStackDrift')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseToolRequest(awsCloudformationDetectStackDriftContract, request, {
|
||||
errorFormat: 'details',
|
||||
logger,
|
||||
})
|
||||
if (!parsed.success) return parsed.response
|
||||
const validatedData = parsed.data.body
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
const command = new DetectStackDriftCommand({
|
||||
StackName: validatedData.stackName,
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
if (!response.StackDriftDetectionId) {
|
||||
throw new Error('No drift detection ID returned')
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
stackDriftDetectionId: response.StackDriftDetectionId,
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Failed to detect CloudFormation stack drift')
|
||||
logger.error('DetectStackDrift failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,58 @@
|
||||
import { CloudFormationClient, ExecuteChangeSetCommand } from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { awsCloudformationExecuteChangeSetContract } from '@/lib/api/contracts/tools/aws/cloudformation-execute-change-set'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
const logger = createLogger('CloudFormationExecuteChangeSet')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseToolRequest(awsCloudformationExecuteChangeSetContract, request, {
|
||||
errorFormat: 'details',
|
||||
logger,
|
||||
})
|
||||
if (!parsed.success) return parsed.response
|
||||
const validatedData = parsed.data.body
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
logger.info(`Executing change set "${validatedData.changeSetName}"`)
|
||||
|
||||
try {
|
||||
const command = new ExecuteChangeSetCommand({
|
||||
ChangeSetName: validatedData.changeSetName,
|
||||
...(validatedData.stackName && { StackName: validatedData.stackName }),
|
||||
})
|
||||
|
||||
await client.send(command)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
message: `Change set "${validatedData.changeSetName}" execution has been initiated`,
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Failed to execute CloudFormation change set')
|
||||
logger.error('ExecuteChangeSet failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,68 @@
|
||||
import { CloudFormationClient, GetTemplateSummaryCommand } from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { awsCloudformationGetTemplateSummaryContract } from '@/lib/api/contracts/tools/aws/cloudformation-get-template-summary'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
const logger = createLogger('CloudFormationGetTemplateSummary')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseToolRequest(awsCloudformationGetTemplateSummaryContract, request, {
|
||||
errorFormat: 'details',
|
||||
logger,
|
||||
})
|
||||
if (!parsed.success) return parsed.response
|
||||
const validatedData = parsed.data.body
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
try {
|
||||
const command = new GetTemplateSummaryCommand({
|
||||
...(validatedData.templateBody && { TemplateBody: validatedData.templateBody }),
|
||||
...(validatedData.stackName && { StackName: validatedData.stackName }),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
description: response.Description,
|
||||
parameters: (response.Parameters ?? []).map((p) => ({
|
||||
parameterKey: p.ParameterKey,
|
||||
defaultValue: p.DefaultValue,
|
||||
parameterType: p.ParameterType,
|
||||
noEcho: p.NoEcho,
|
||||
description: p.Description,
|
||||
})),
|
||||
capabilities: response.Capabilities ?? [],
|
||||
capabilitiesReason: response.CapabilitiesReason,
|
||||
resourceTypes: response.ResourceTypes ?? [],
|
||||
version: response.Version,
|
||||
declaredTransforms: response.DeclaredTransforms ?? [],
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Failed to get CloudFormation template summary')
|
||||
logger.error('GetTemplateSummary failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,53 @@
|
||||
import { CloudFormationClient, GetTemplateCommand } from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { awsCloudformationGetTemplateContract } from '@/lib/api/contracts/tools/aws/cloudformation-get-template'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
const logger = createLogger('CloudFormationGetTemplate')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseToolRequest(awsCloudformationGetTemplateContract, request, {
|
||||
errorFormat: 'details',
|
||||
logger,
|
||||
})
|
||||
if (!parsed.success) return parsed.response
|
||||
const validatedData = parsed.data.body
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
const command = new GetTemplateCommand({
|
||||
StackName: validatedData.stackName,
|
||||
...(validatedData.templateStage && { TemplateStage: validatedData.templateStage }),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
templateBody: response.TemplateBody ?? '',
|
||||
stagesAvailable: response.StagesAvailable ?? [],
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Failed to get CloudFormation template')
|
||||
logger.error('GetTemplate failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,74 @@
|
||||
import {
|
||||
CloudFormationClient,
|
||||
ListStackResourcesCommand,
|
||||
type StackResourceSummary,
|
||||
} from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { awsCloudformationListStackResourcesContract } from '@/lib/api/contracts/tools/aws/cloudformation-list-stack-resources'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
const logger = createLogger('CloudFormationListStackResources')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseToolRequest(awsCloudformationListStackResourcesContract, request, {
|
||||
errorFormat: 'details',
|
||||
logger,
|
||||
})
|
||||
if (!parsed.success) return parsed.response
|
||||
const validatedData = parsed.data.body
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
const allSummaries: StackResourceSummary[] = []
|
||||
let nextToken: string | undefined
|
||||
do {
|
||||
const command = new ListStackResourcesCommand({
|
||||
StackName: validatedData.stackName,
|
||||
...(nextToken && { NextToken: nextToken }),
|
||||
})
|
||||
const response = await client.send(command)
|
||||
allSummaries.push(...(response.StackResourceSummaries ?? []))
|
||||
nextToken = response.NextToken
|
||||
} while (nextToken)
|
||||
|
||||
const resources = allSummaries.map((r) => ({
|
||||
logicalResourceId: r.LogicalResourceId ?? '',
|
||||
physicalResourceId: r.PhysicalResourceId,
|
||||
resourceType: r.ResourceType ?? '',
|
||||
resourceStatus: r.ResourceStatus ?? 'UNKNOWN',
|
||||
resourceStatusReason: r.ResourceStatusReason,
|
||||
lastUpdatedTimestamp: r.LastUpdatedTimestamp?.getTime(),
|
||||
driftInformation: r.DriftInformation
|
||||
? {
|
||||
stackResourceDriftStatus: r.DriftInformation.StackResourceDriftStatus,
|
||||
lastCheckTimestamp: r.DriftInformation.LastCheckTimestamp?.getTime(),
|
||||
}
|
||||
: null,
|
||||
}))
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: { resources },
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Failed to list CloudFormation stack resources')
|
||||
logger.error('ListStackResources failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,63 @@
|
||||
import { CloudFormationClient, UpdateStackCommand } from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { awsCloudformationUpdateStackContract } from '@/lib/api/contracts/tools/aws/cloudformation-update-stack'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
import { parseCapabilities, toStackParameters, toStackTags } from '../utils'
|
||||
|
||||
const logger = createLogger('CloudFormationUpdateStack')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseToolRequest(awsCloudformationUpdateStackContract, request, {
|
||||
errorFormat: 'details',
|
||||
logger,
|
||||
})
|
||||
if (!parsed.success) return parsed.response
|
||||
const validatedData = parsed.data.body
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
logger.info(`Updating CloudFormation stack "${validatedData.stackName}"`)
|
||||
|
||||
try {
|
||||
const command = new UpdateStackCommand({
|
||||
StackName: validatedData.stackName,
|
||||
TemplateBody: validatedData.templateBody,
|
||||
UsePreviousTemplate: validatedData.usePreviousTemplate,
|
||||
Parameters: toStackParameters(validatedData.parameters),
|
||||
Capabilities: parseCapabilities(validatedData.capabilities),
|
||||
Tags: toStackTags(validatedData.tags),
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
stackId: response.StackId ?? '',
|
||||
},
|
||||
})
|
||||
} finally {
|
||||
client.destroy()
|
||||
}
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Failed to update CloudFormation stack')
|
||||
logger.error('UpdateStack failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import type { Capability, Parameter, Tag } from '@aws-sdk/client-cloudformation'
|
||||
|
||||
/**
|
||||
* Parses a comma-separated capabilities string (e.g. "CAPABILITY_IAM,CAPABILITY_NAMED_IAM")
|
||||
* into the array shape the CloudFormation SDK expects.
|
||||
*/
|
||||
export function parseCapabilities(value?: string): Capability[] | undefined {
|
||||
if (!value) return undefined
|
||||
const capabilities = value
|
||||
.split(',')
|
||||
.map((c) => c.trim())
|
||||
.filter(Boolean)
|
||||
return capabilities.length > 0 ? (capabilities as Capability[]) : undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps camelCase stack parameter inputs to the PascalCase `Parameter` shape CloudFormation expects.
|
||||
*/
|
||||
export function toStackParameters(
|
||||
parameters?: { parameterKey: string; parameterValue?: string; usePreviousValue?: boolean }[]
|
||||
): Parameter[] | undefined {
|
||||
if (!parameters || parameters.length === 0) return undefined
|
||||
return parameters.map((p) => ({
|
||||
ParameterKey: p.parameterKey,
|
||||
ParameterValue: p.parameterValue,
|
||||
UsePreviousValue: p.usePreviousValue,
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps camelCase tag inputs to the PascalCase `Tag` shape CloudFormation expects.
|
||||
*/
|
||||
export function toStackTags(tags?: { key: string; value: string }[]): Tag[] | undefined {
|
||||
if (!tags || tags.length === 0) return undefined
|
||||
return tags.map((t) => ({ Key: t.key, Value: t.value }))
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { CloudFormationClient, ValidateTemplateCommand } from '@aws-sdk/client-cloudformation'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { getErrorMessage } from '@sim/utils/errors'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { awsCloudformationValidateTemplateContract } from '@/lib/api/contracts/tools/aws/cloudformation-validate-template'
|
||||
import { parseToolRequest } from '@/lib/api/server'
|
||||
import { checkInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
|
||||
|
||||
const logger = createLogger('CloudFormationValidateTemplate')
|
||||
|
||||
export const POST = withRouteHandler(async (request: NextRequest) => {
|
||||
try {
|
||||
const auth = await checkInternalAuth(request)
|
||||
if (!auth.success || !auth.userId) {
|
||||
return NextResponse.json({ error: auth.error || 'Unauthorized' }, { status: 401 })
|
||||
}
|
||||
|
||||
const parsed = await parseToolRequest(awsCloudformationValidateTemplateContract, request, {
|
||||
errorFormat: 'details',
|
||||
logger,
|
||||
})
|
||||
if (!parsed.success) return parsed.response
|
||||
const validatedData = parsed.data.body
|
||||
|
||||
const client = new CloudFormationClient({
|
||||
region: validatedData.region,
|
||||
credentials: {
|
||||
accessKeyId: validatedData.accessKeyId,
|
||||
secretAccessKey: validatedData.secretAccessKey,
|
||||
},
|
||||
})
|
||||
|
||||
const command = new ValidateTemplateCommand({
|
||||
TemplateBody: validatedData.templateBody,
|
||||
})
|
||||
|
||||
const response = await client.send(command)
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
output: {
|
||||
description: response.Description,
|
||||
parameters: (response.Parameters ?? []).map((p) => ({
|
||||
parameterKey: p.ParameterKey,
|
||||
defaultValue: p.DefaultValue,
|
||||
noEcho: p.NoEcho,
|
||||
description: p.Description,
|
||||
})),
|
||||
capabilities: response.Capabilities ?? [],
|
||||
capabilitiesReason: response.CapabilitiesReason,
|
||||
declaredTransforms: response.DeclaredTransforms ?? [],
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
const errorMessage = getErrorMessage(error, 'Failed to validate CloudFormation template')
|
||||
logger.error('ValidateTemplate failed', { error: errorMessage })
|
||||
return NextResponse.json({ error: errorMessage }, { status: 500 })
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user