Skip to content
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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ This server provides your AI with dedicated tools for managing your LocalStack e

| Tool Name | Description | Key Features |
| :-------------------------------------------------------------------------------- | :------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| [`localstack-management`](./src/tools/localstack-management.ts) | Manages LocalStack for AWS container operations and settings | - Execute start, stop, restart, and status checks<br/>- Integrate LocalStack authentication tokens<br/>- Inject custom environment variables<br/>- Verify real-time status and perform health monitoring |
| [`localstack-management`](./src/tools/localstack-management.ts) | Manages LocalStack runtime operations for AWS and Snowflake stacks | - Execute start, stop, restart, and status checks<br/>- Integrate LocalStack authentication tokens<br/>- Inject custom environment variables<br/>- Verify real-time status and perform health monitoring |
| [`localstack-deployer`](./src/tools/localstack-deployer.ts) | Handles infrastructure deployment to LocalStack for AWS environments | - Automatically run CDK and Terraform tooling to deploy infrastructure locally<br/>- Enable parameterized deployments with variable support<br/>- Process and present deployment results<br/>- Requires you to have [`cdklocal`](https://github.com/localstack/aws-cdk-local) or [`tflocal`](https://github.com/localstack/terraform-local) installed in your system path |
| [`localstack-logs-analysis`](./src/tools/localstack-logs-analysis.ts) | Analyzes LocalStack for AWS logs for troubleshooting and insights | - Offer multiple analysis options including summaries, errors, requests, and raw data<br/>- Filter by specific services and operations<br/>- Generate API call metrics and failure breakdowns<br/>- Group errors intelligently and identify patterns |
| [`localstack-iam-policy-analyzer`](./src/tools/localstack-iam-policy-analyzer.ts) | Handles IAM policy management and violation remediation | - Set IAM enforcement levels including `enforced`, `soft`, and `disabled` modes<br/>- Search logs for permission-related violations<br/>- Generate IAM policies automatically from detected access failures<br/>- Requires a valid LocalStack Auth Token |
Expand Down
10 changes: 10 additions & 0 deletions src/core/preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,16 @@ export const requireProFeature = async (feature: ProFeature): Promise<ToolRespon
: null;
};

export const requireAuthToken = (): ToolResponse | null => {
if (!process.env.LOCALSTACK_AUTH_TOKEN) {
return ResponseBuilder.error(
"Auth Token Required",
"LOCALSTACK_AUTH_TOKEN is required for this operation."
);
}
return null;
};

export const runPreflights = async (
checks: Array<Promise<ToolResponse | null>>
): Promise<ToolResponse | null> => {
Expand Down
1 change: 1 addition & 0 deletions src/lib/localstack/license-checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ export enum ProFeature {
CLOUD_PODS = "localstack.platform.plugin/pods",
CHAOS_ENGINEERING = "localstack.platform.plugin/chaos",
EXTENSIONS = "localstack.platform.plugin/extensions",
SNOWFLAKE = "localstack.aws.provider/snowflake:pro",
}

export interface LicenseCheckResult {
Expand Down
51 changes: 51 additions & 0 deletions src/lib/localstack/localstack.utils.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { getLocalStackStatus, getSnowflakeEmulatorStatus } from "./localstack.utils";
import { runCommand } from "../../core/command-runner";

jest.mock("../../core/command-runner", () => ({
runCommand: jest.fn(),
}));

const mockedRunCommand = runCommand as jest.MockedFunction<typeof runCommand>;

describe("localstack.utils", () => {
beforeEach(() => {
mockedRunCommand.mockReset();
});

test("getLocalStackStatus marks instance as running and ready", async () => {
mockedRunCommand.mockResolvedValueOnce({
stdout: "Runtime status: running (Ready)",
stderr: "",
exitCode: 0,
} as any);

const result = await getLocalStackStatus();
expect(result.isRunning).toBe(true);
expect(result.isReady).toBe(true);
});

test("getSnowflakeEmulatorStatus marks emulator healthy on success payload", async () => {
mockedRunCommand.mockResolvedValueOnce({
stdout: '{"success": true}',
stderr: "",
exitCode: 0,
} as any);

const result = await getSnowflakeEmulatorStatus();
expect(result.isRunning).toBe(true);
expect(result.isReady).toBe(true);
expect(result.statusOutput).toContain('"success": true');
});

test("getSnowflakeEmulatorStatus reports unhealthy response", async () => {
mockedRunCommand.mockResolvedValueOnce({
stdout: '{"success": false}',
stderr: "",
exitCode: 0,
} as any);

const result = await getSnowflakeEmulatorStatus();
expect(result.isRunning).toBe(false);
expect(result.isReady).toBe(false);
});
});
153 changes: 153 additions & 0 deletions src/lib/localstack/localstack.utils.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
import { spawn } from "child_process";
import { runCommand } from "../../core/command-runner";
import { ResponseBuilder } from "../../core/response-builder";

export interface LocalStackCliCheckResult {
isAvailable: boolean;
Expand Down Expand Up @@ -44,6 +46,19 @@ export interface LocalStackStatusResult {
isReady?: boolean;
}

export interface SnowflakeStatusResult {
isRunning: boolean;
statusOutput?: string;
errorMessage?: string;
isReady?: boolean;
}

export interface RuntimeStatus {
isRunning: boolean;
isReady?: boolean;
statusOutput?: string;
}

/**
* Get LocalStack status information
* @returns Promise with status details including running state and raw output
Expand All @@ -68,6 +83,144 @@ export async function getLocalStackStatus(): Promise<LocalStackStatusResult> {
}
}

/**
* Get Snowflake emulator status information by checking the Snowflake session endpoint
* @returns Promise with status details including running state and raw output
*/
export async function getSnowflakeEmulatorStatus(): Promise<SnowflakeStatusResult> {
try {
const { stdout, stderr, error, exitCode } = await runCommand("curl", [
"-sS",
"-X",
"POST",
"-H",
"Content-Type: application/json",
"-d",
"{}",
"snowflake.localhost.localstack.cloud:4566/session",
]);

const output = (stdout || "").trim();
const isSuccess = /"success"\s*:\s*true/.test(output);

return {
isRunning: exitCode === 0 && isSuccess,
isReady: exitCode === 0 && isSuccess,
statusOutput: output || stderr.trim(),
...(error
? {
errorMessage: `Failed to reach Snowflake emulator endpoint: ${error.message}`,
}
: {}),
};
} catch (error) {
return {
isRunning: false,
errorMessage: `Failed to get Snowflake emulator status: ${
error instanceof Error ? error.message : String(error)
}`,
};
}
}

/**
* Start a LocalStack runtime flavor and poll until it becomes available.
* Supports custom startup args (e.g. default stack vs Snowflake stack), optional
* environment overrides, and optional post-start validation hooks.
*/
export async function startRuntime({
startArgs,
getStatus,
processLabel,
alreadyRunningMessage,
successTitle,
statusHeading,
timeoutMessage,
envVars,
onReady,
}: {
startArgs: string[];
getStatus: () => Promise<RuntimeStatus>;
processLabel: string;
alreadyRunningMessage: string;
successTitle: string;
statusHeading: string;
timeoutMessage: string;
envVars?: Record<string, string>;
onReady?: () => Promise<ReturnType<typeof ResponseBuilder.error> | null>;
}) {
const statusCheck = await getStatus();
if (statusCheck.isReady || statusCheck.isRunning) {
return ResponseBuilder.markdown(alreadyRunningMessage);
}

const environment = { ...process.env, ...(envVars || {}) } as Record<string, string>;
if (process.env.LOCALSTACK_AUTH_TOKEN) {
environment.LOCALSTACK_AUTH_TOKEN = process.env.LOCALSTACK_AUTH_TOKEN;
}

return new Promise((resolve) => {
const child = spawn("localstack", startArgs, {
env: environment,
stdio: ["ignore", "ignore", "pipe"],
});

let stderr = "";
child.stderr.on("data", (data) => {
stderr += data.toString();
});

let earlyExit = false;
let poll: NodeJS.Timeout;
child.on("error", (err) => {
earlyExit = true;
if (poll) clearInterval(poll);
resolve(ResponseBuilder.markdown(`❌ Failed to start ${processLabel} process: ${err.message}`));
});

child.on("close", (code) => {
if (earlyExit) return;
if (poll) clearInterval(poll);
if (code !== 0) {
resolve(
ResponseBuilder.markdown(
`❌ ${processLabel} process exited unexpectedly with code ${code}.\n\nStderr:\n${stderr}`
)
);
}
});

const pollInterval = 5000;
const maxWaitTime = 120000;
let timeWaited = 0;

poll = setInterval(async () => {
timeWaited += pollInterval;
const status = await getStatus();
if (status.isReady || status.isRunning) {
if (onReady) {
const preflight = await onReady();
if (preflight) {
clearInterval(poll);
resolve(preflight);
return;
}
}

clearInterval(poll);
let resultMessage = `${successTitle}\n\n`;
if (envVars)
resultMessage += `✅ Custom environment variables applied: ${Object.keys(envVars).join(", ")}\n`;
if (status.statusOutput) resultMessage += `\n**${statusHeading}:**\n${status.statusOutput}`;
resolve(ResponseBuilder.markdown(resultMessage));
} else if (timeWaited >= maxWaitTime) {
clearInterval(poll);
resolve(ResponseBuilder.markdown(timeoutMessage));
}
}, pollInterval);
});
}

/**
* Validate LocalStack CLI availability and return early if not available
* This is a helper function for tools that require LocalStack CLI
Expand Down
17 changes: 4 additions & 13 deletions src/tools/localstack-extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
requireLocalStackCli,
requireLocalStackRunning,
requireProFeature,
requireAuthToken,
} from "../core/preflight";
import { ResponseBuilder } from "../core/response-builder";
import { ProFeature } from "../lib/localstack/license-checker";
Expand Down Expand Up @@ -50,9 +51,6 @@ interface MarketplaceExtension {
version?: string;
}

const AUTH_TOKEN_REQUIRED_MESSAGE =
"LOCALSTACK_AUTH_TOKEN is not set in your environment. LocalStack Extensions require a valid Auth Token. Please set it and try again.";

export default async function localstackExtensions({
action,
name,
Expand Down Expand Up @@ -81,13 +79,6 @@ export default async function localstackExtensions({
}
}

function requireAuthTokenForCli() {
if (!process.env.LOCALSTACK_AUTH_TOKEN) {
return ResponseBuilder.error("Auth Token Required", AUTH_TOKEN_REQUIRED_MESSAGE);
}
return null;
}

function cleanOutput(stdout: string, stderr: string) {
return {
stdout: stripAnsiCodes(stdout || "").trim(),
Expand All @@ -100,7 +91,7 @@ function combineOutput(stdout: string, stderr: string): string {
}

async function handleList() {
const authError = requireAuthTokenForCli();
const authError = requireAuthToken();
if (authError) return authError;

const cmd = await runCommand("localstack", ["extensions", "list"], {
Expand Down Expand Up @@ -128,7 +119,7 @@ async function handleList() {
}

async function handleInstall(name?: string, source?: string) {
const authError = requireAuthTokenForCli();
const authError = requireAuthToken();
if (authError) return authError;

const hasName = !!name;
Expand Down Expand Up @@ -198,7 +189,7 @@ async function handleInstall(name?: string, source?: string) {
}

async function handleUninstall(name?: string) {
const authError = requireAuthTokenForCli();
const authError = requireAuthToken();
if (authError) return authError;

if (!name) {
Expand Down
Loading