From 895a106a4485df214bb827853ed9e7bcb13b38f6 Mon Sep 17 00:00:00 2001 From: Rishi Mondal Date: Sat, 12 Jul 2025 16:06:01 +0530 Subject: [PATCH 1/6] proper folder strcture --- registry/mavrickrishi/.images/avatar.svg | 4 + registry/mavrickrishi/README.md | 13 + .../modules/aws-ami-snapshot/README.md | 185 ++++++++++ .../modules/aws-ami-snapshot/main.test.ts | 31 ++ .../modules/aws-ami-snapshot/main.tf | 232 ++++++++++++ test/test.ts | 336 ------------------ 6 files changed, 465 insertions(+), 336 deletions(-) create mode 100644 registry/mavrickrishi/.images/avatar.svg create mode 100644 registry/mavrickrishi/README.md create mode 100644 registry/mavrickrishi/modules/aws-ami-snapshot/README.md create mode 100644 registry/mavrickrishi/modules/aws-ami-snapshot/main.test.ts create mode 100644 registry/mavrickrishi/modules/aws-ami-snapshot/main.tf delete mode 100644 test/test.ts diff --git a/registry/mavrickrishi/.images/avatar.svg b/registry/mavrickrishi/.images/avatar.svg new file mode 100644 index 00000000..60d7eff6 --- /dev/null +++ b/registry/mavrickrishi/.images/avatar.svg @@ -0,0 +1,4 @@ + + + + diff --git a/registry/mavrickrishi/README.md b/registry/mavrickrishi/README.md new file mode 100644 index 00000000..5e0c2e1f --- /dev/null +++ b/registry/mavrickrishi/README.md @@ -0,0 +1,13 @@ +--- +name: mavrickrishi +description: Modules and templates by mavrickrishi +github_url: https://github.com/MAVRICK-1 +--- + +# mavrickrishi + +This namespace contains modules and templates created by mavrickrishi. + +## Modules + +- **aws-ami-snapshot**: Create and manage AMI snapshots for Coder workspaces with restore capabilities diff --git a/registry/mavrickrishi/modules/aws-ami-snapshot/README.md b/registry/mavrickrishi/modules/aws-ami-snapshot/README.md new file mode 100644 index 00000000..5364f496 --- /dev/null +++ b/registry/mavrickrishi/modules/aws-ami-snapshot/README.md @@ -0,0 +1,185 @@ +--- +display_name: AWS AMI Snapshot +description: Create and manage AMI snapshots for Coder workspaces with restore capabilities +icon: ../../../../../../.icons/aws.svg +maintainer_github: MAVRICK-1 +verified: false +tags: [aws, snapshot, ami, backup, persistence] +--- + +# AWS AMI Snapshot Module + +This module provides AMI-based snapshot functionality for Coder workspaces running on AWS EC2 instances. It enables users to create snapshots when workspaces are stopped and restore from previous snapshots when starting workspaces. + +## Features + +- **Automatic Snapshots**: Create AMI snapshots when workspaces are stopped +- **User Control**: Enable/disable snapshot functionality per workspace +- **Custom Labels**: Add custom labels to snapshots for easy identification +- **Snapshot Selection**: Choose from available snapshots when starting workspaces +- **Automatic Cleanup**: Optional Data Lifecycle Manager integration for automated cleanup +- **Workspace Isolation**: Snapshots are tagged and filtered by workspace and owner + +## Parameters + +The module exposes the following parameters to workspace users: + +- `enable_snapshots`: Enable/disable AMI snapshot creation (default: true) +- `snapshot_label`: Custom label for the snapshot (optional) +- `use_previous_snapshot`: Select a previous snapshot to restore from (default: none) + +## Usage + +### Basic Usage + +```hcl +module "ami_snapshot" { + source = "registry.coder.com/modules/aws-ami-snapshot" + + instance_id = aws_instance.workspace.id + default_ami_id = data.aws_ami.ubuntu.id + template_name = "aws-linux" +} + +resource "aws_instance" "workspace" { + ami = module.ami_snapshot.ami_id + instance_type = "t3.micro" + + # Prevent Terraform from recreating instance when AMI changes + lifecycle { + ignore_changes = [ami] + } +} +``` + +### With Optional Cleanup + +```hcl +module "ami_snapshot" { + source = "registry.coder.com/modules/aws-ami-snapshot" + + instance_id = aws_instance.workspace.id + default_ami_id = data.aws_ami.ubuntu.id + template_name = "aws-linux" + enable_dlm_cleanup = true + dlm_role_arn = aws_iam_role.dlm_lifecycle_role.arn + snapshot_retention_count = 5 + + tags = { + Environment = "development" + Project = "my-project" + } +} +``` + +### IAM Role for DLM (Optional) + +If using automatic cleanup, create an IAM role for Data Lifecycle Manager: + +```hcl +resource "aws_iam_role" "dlm_lifecycle_role" { + name = "dlm-lifecycle-role" + + assume_role_policy = jsonencode({ + Version = "2012-10-17" + Statement = [ + { + Action = "sts:AssumeRole" + Effect = "Allow" + Principal = { + Service = "dlm.amazonaws.com" + } + } + ] + }) +} + +resource "aws_iam_role_policy_attachment" "dlm_lifecycle" { + role = aws_iam_role.dlm_lifecycle_role.name + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSDataLifecycleManagerServiceRole" +} +``` + +## Required IAM Permissions + +Users need the following IAM permissions for full functionality: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "ec2:CreateImage", + "ec2:DescribeImages", + "ec2:DescribeInstances", + "ec2:CreateTags", + "ec2:DescribeTags" + ], + "Resource": "*" + }, + { + "Effect": "Allow", + "Action": [ + "dlm:CreateLifecyclePolicy", + "dlm:GetLifecyclePolicy", + "dlm:UpdateLifecyclePolicy", + "dlm:DeleteLifecyclePolicy" + ], + "Resource": "*", + "Condition": { + "StringEquals": { + "dlm:Target": "INSTANCE" + } + } + } + ] +} +``` + +## How It Works + +1. **Snapshot Creation**: When a workspace transitions to "stop", an AMI snapshot is automatically created (if enabled) +2. **Tagging**: Snapshots are tagged with workspace name, owner, template, and custom labels +3. **Snapshot Retrieval**: Available snapshots are retrieved and presented as options for workspace start +4. **AMI Selection**: The module outputs the appropriate AMI ID (default or selected snapshot) +5. **Cleanup**: Optional DLM policies can automatically clean up old snapshots + +## Variables + +| Name | Description | Type | Default | Required | +| ------------------------ | ------------------------------------------------------------ | ----------- | ------- | -------- | +| instance_id | The EC2 instance ID to create snapshots from | string | n/a | yes | +| default_ami_id | The default AMI ID to use when not restoring from a snapshot | string | n/a | yes | +| template_name | The name of the Coder template using this module | string | n/a | yes | +| tags | Additional tags to apply to snapshots | map(string) | {} | no | +| enable_dlm_cleanup | Enable Data Lifecycle Manager for automated snapshot cleanup | bool | false | no | +| dlm_role_arn | ARN of the IAM role for DLM | string | "" | no | +| snapshot_retention_count | Number of snapshots to retain when using DLM cleanup | number | 7 | no | + +## Outputs + +| Name | Description | +| ------------------- | ----------------------------------------------------- | +| ami_id | The AMI ID to use for the workspace instance | +| is_using_snapshot | Whether the workspace is using a snapshot AMI | +| snapshot_ami_id | The AMI ID of the created snapshot (if any) | +| available_snapshots | List of available snapshot AMI IDs for this workspace | +| snapshot_info | Detailed information about available snapshots | + +## Considerations + +- **Cost**: AMI snapshots incur storage costs. Use cleanup policies to manage costs +- **Time**: AMI creation takes time; workspace stop operations may take longer +- **Permissions**: Ensure proper IAM permissions for AMI creation and management +- **Region**: Snapshots are region-specific and cannot be used across regions +- **Lifecycle**: Use `ignore_changes = [ami]` on EC2 instances to prevent conflicts + +## Examples + +See the updated AWS templates that use this module: + +- `coder/templates/aws-linux` +- `coder/templates/aws-windows` +- `coder/templates/aws-devcontainer` diff --git a/registry/mavrickrishi/modules/aws-ami-snapshot/main.test.ts b/registry/mavrickrishi/modules/aws-ami-snapshot/main.test.ts new file mode 100644 index 00000000..3c062f93 --- /dev/null +++ b/registry/mavrickrishi/modules/aws-ami-snapshot/main.test.ts @@ -0,0 +1,31 @@ +import { describe, expect, it } from "bun:test"; +import { + runTerraformApply, + runTerraformInit, + testRequiredVariables, +} from "~test"; + +describe("aws-ami-snapshot", async () => { + await runTerraformInit(import.meta.dir); + + testRequiredVariables(import.meta.dir, { + instance_id: "i-1234567890abcdef0", + default_ami_id: "ami-12345678", + template_name: "test-template", + }); + + it("supports optional variables", async () => { + await testRequiredVariables(import.meta.dir, { + instance_id: "i-1234567890abcdef0", + default_ami_id: "ami-12345678", + template_name: "test-template", + enable_dlm_cleanup: true, + dlm_role_arn: "arn:aws:iam::123456789012:role/dlm-lifecycle-role", + snapshot_retention_count: 5, + tags: { + Environment: "test", + Project: "coder", + }, + }); + }); +}); \ No newline at end of file diff --git a/registry/mavrickrishi/modules/aws-ami-snapshot/main.tf b/registry/mavrickrishi/modules/aws-ami-snapshot/main.tf new file mode 100644 index 00000000..e76be7fb --- /dev/null +++ b/registry/mavrickrishi/modules/aws-ami-snapshot/main.tf @@ -0,0 +1,232 @@ +terraform { + required_version = ">= 1.0" + + required_providers { + aws = { + source = "hashicorp/aws" + version = "~> 5.0" + } + coder = { + source = "coder/coder" + version = ">= 0.17" + } + } +} + +# Variables +variable "instance_id" { + description = "The EC2 instance ID to create snapshots from" + type = string +} + +variable "default_ami_id" { + description = "The default AMI ID to use when not restoring from a snapshot" + type = string +} + +variable "template_name" { + description = "The name of the Coder template using this module" + type = string +} + +variable "tags" { + description = "Additional tags to apply to snapshots" + type = map(string) + default = {} +} + +variable "enable_dlm_cleanup" { + description = "Enable Data Lifecycle Manager for automated snapshot cleanup" + type = bool + default = false +} + +variable "dlm_role_arn" { + description = "ARN of the IAM role for DLM (required if enable_dlm_cleanup is true)" + type = string + default = "" +} + +variable "snapshot_retention_count" { + description = "Number of snapshots to retain when using DLM cleanup" + type = number + default = 7 +} + +# Parameters for snapshot control +data "coder_parameter" "enable_snapshots" { + name = "enable_snapshots" + display_name = "Enable AMI Snapshots" + description = "Create AMI snapshots when workspace is stopped" + type = "bool" + default = "true" + mutable = true +} + +data "coder_parameter" "snapshot_label" { + name = "snapshot_label" + display_name = "Snapshot Label" + description = "Custom label for this snapshot (optional)" + type = "string" + default = "" + mutable = true +} + +data "coder_parameter" "use_previous_snapshot" { + name = "use_previous_snapshot" + display_name = "Start from Snapshot" + description = "Select a previous snapshot to restore from" + type = "string" + default = "none" + mutable = true + option { + name = "Use default AMI" + value = "none" + description = "Start with a fresh instance" + } + dynamic "option" { + for_each = data.aws_ami_ids.workspace_snapshots.ids + content { + name = "${data.aws_ami.snapshot_info[option.value].name} (${formatdate("YYYY-MM-DD hh:mm", data.aws_ami.snapshot_info[option.value].creation_date)})" + value = option.value + description = data.aws_ami.snapshot_info[option.value].description + } + } +} + +# Get workspace information +data "coder_workspace" "me" {} +data "coder_workspace_owner" "me" {} + +# Retrieve existing snapshots for this workspace +data "aws_ami_ids" "workspace_snapshots" { + owners = ["self"] + + filter { + name = "tag:CoderWorkspace" + values = [data.coder_workspace.me.name] + } + + filter { + name = "tag:CoderOwner" + values = [data.coder_workspace_owner.me.name] + } + + filter { + name = "tag:CoderTemplate" + values = [var.template_name] + } + + filter { + name = "state" + values = ["available"] + } +} + +# Get detailed information about each snapshot +data "aws_ami" "snapshot_info" { + for_each = toset(data.aws_ami_ids.workspace_snapshots.ids) + owners = ["self"] + + filter { + name = "image-id" + values = [each.value] + } +} + +# Determine which AMI to use +locals { + use_snapshot = data.coder_parameter.use_previous_snapshot.value != "none" + ami_id = local.use_snapshot ? data.coder_parameter.use_previous_snapshot.value : var.default_ami_id +} + +# Create AMI snapshot when workspace is stopped +resource "aws_ami_from_instance" "workspace_snapshot" { + count = data.coder_parameter.enable_snapshots.value && data.coder_workspace.me.transition == "stop" ? 1 : 0 + name = "${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}-${formatdate("YYYY-MM-DD-hhmm", timestamp())}" + source_instance_id = var.instance_id + snapshot_without_reboot = true + deprecation_time = timeadd(timestamp(), "168h") # 7 days + + tags = merge(var.tags, { + Name = "${data.coder_workspace_owner.me.name}-${data.coder_workspace.me.name}-snapshot" + CoderWorkspace = data.coder_workspace.me.name + CoderOwner = data.coder_workspace_owner.me.name + CoderTemplate = var.template_name + SnapshotLabel = data.coder_parameter.snapshot_label.value + CreatedAt = timestamp() + SnapshotType = "workspace" + WorkspaceId = data.coder_workspace.me.id + }) + + lifecycle { + ignore_changes = [ + deprecation_time + ] + } +} + +# Optional: Data Lifecycle Manager policy for automated cleanup +resource "aws_dlm_lifecycle_policy" "workspace_snapshots" { + count = var.enable_dlm_cleanup ? 1 : 0 + description = "Lifecycle policy for Coder workspace AMI snapshots" + execution_role_arn = var.dlm_role_arn + state = "ENABLED" + + policy_details { + resource_types = ["INSTANCE"] + target_tags = { + CoderTemplate = var.template_name + SnapshotType = "workspace" + } + + schedule { + name = "Coder workspace snapshot cleanup" + + create_rule { + interval = 24 + interval_unit = "HOURS" + times = ["03:00"] + } + + retain_rule { + count = var.snapshot_retention_count + } + + copy_tags = true + } + } +} + +# Outputs +output "ami_id" { + description = "The AMI ID to use for the workspace instance (either default or selected snapshot)" + value = local.ami_id +} + +output "is_using_snapshot" { + description = "Whether the workspace is using a snapshot AMI" + value = local.use_snapshot +} + +output "snapshot_ami_id" { + description = "The AMI ID of the created snapshot (if any)" + value = data.coder_parameter.enable_snapshots.value && data.coder_workspace.me.transition == "stop" ? aws_ami_from_instance.workspace_snapshot[0].id : null +} + +output "available_snapshots" { + description = "List of available snapshot AMI IDs for this workspace" + value = data.aws_ami_ids.workspace_snapshots.ids +} + +output "snapshot_info" { + description = "Detailed information about available snapshots" + value = { + for ami_id in data.aws_ami_ids.workspace_snapshots.ids : ami_id => { + name = data.aws_ami.snapshot_info[ami_id].name + description = data.aws_ami.snapshot_info[ami_id].description + created_date = data.aws_ami.snapshot_info[ami_id].creation_date + tags = data.aws_ami.snapshot_info[ami_id].tags + } + } +} \ No newline at end of file diff --git a/test/test.ts b/test/test.ts deleted file mode 100644 index bb09a410..00000000 --- a/test/test.ts +++ /dev/null @@ -1,336 +0,0 @@ -import { readableStreamToText, spawn } from "bun"; -import { expect, it } from "bun:test"; -import { readFile, unlink } from "node:fs/promises"; - -export const runContainer = async ( - image: string, - init = "sleep infinity", -): Promise => { - const proc = spawn([ - "docker", - "run", - "--rm", - "-d", - "--label", - "modules-test=true", - "--network", - "host", - "--entrypoint", - "sh", - image, - "-c", - init, - ]); - - const containerID = await readableStreamToText(proc.stdout); - const exitCode = await proc.exited; - if (exitCode !== 0) { - throw new Error(containerID); - } - return containerID.trim(); -}; - -export const removeContainer = async (id: string) => { - const proc = spawn(["docker", "rm", "-f", id], { - stderr: "pipe", - stdout: "pipe", - }); - const exitCode = await proc.exited; - const [stderr, stdout] = await Promise.all([ - readableStreamToText(proc.stderr ?? new ReadableStream()), - readableStreamToText(proc.stdout ?? new ReadableStream()), - ]); - if (exitCode !== 0) { - throw new Error(`${stderr}\n${stdout}`); - } -}; - -export interface scriptOutput { - exitCode: number; - stdout: string[]; - stderr: string[]; -} - -/** - * Finds the only "coder_script" resource in the given state and runs it in a - * container. - */ -export const executeScriptInContainer = async ( - state: TerraformState, - image: string, - shell = "sh", - before?: string, -): Promise => { - const instance = findResourceInstance(state, "coder_script"); - const id = await runContainer(image); - - if (before) { - await execContainer(id, [shell, "-c", before]); - } - - const resp = await execContainer(id, [shell, "-c", instance.script]); - const stdout = resp.stdout.trim().split("\n"); - const stderr = resp.stderr.trim().split("\n"); - return { - exitCode: resp.exitCode, - stdout, - stderr, - }; -}; - -export const execContainer = async ( - id: string, - cmd: string[], - args?: string[], -): Promise<{ - exitCode: number; - stderr: string; - stdout: string; -}> => { - const proc = spawn(["docker", "exec", ...(args ?? []), id, ...cmd], { - stderr: "pipe", - stdout: "pipe", - }); - const [stderr, stdout] = await Promise.all([ - readableStreamToText(proc.stderr), - readableStreamToText(proc.stdout), - ]); - const exitCode = await proc.exited; - return { - exitCode, - stderr, - stdout, - }; -}; - -type JsonValue = - | string - | number - | boolean - | null - | JsonValue[] - | { [key: string]: JsonValue }; - -type TerraformStateResource = { - type: string; - name: string; - provider: string; - - instances: [ - { - attributes: Record; - }, - ]; -}; - -type TerraformOutput = { - type: string; - value: JsonValue; -}; - -export interface TerraformState { - outputs: Record; - resources: [TerraformStateResource, ...TerraformStateResource[]]; -} - -type TerraformVariables = Record; - -export interface CoderScriptAttributes { - script: string; - agent_id: string; - url: string; -} - -export type ResourceInstance = - T extends "coder_script" ? CoderScriptAttributes : Record; - -/** - * finds the first instance of the given resource type in the given state. If - * name is specified, it will only find the instance with the given name. - */ -export const findResourceInstance = ( - state: TerraformState, - type: T, - name?: string, -): ResourceInstance => { - const resource = state.resources.find( - (resource) => - resource.type === type && (name ? resource.name === name : true), - ); - if (!resource) { - throw new Error(`Resource ${type} not found`); - } - if (resource.instances.length !== 1) { - throw new Error( - `Resource ${type} has ${resource.instances.length} instances`, - ); - } - - return resource.instances[0].attributes as ResourceInstance; -}; - -/** - * Creates a test-case for each variable provided and ensures that the apply - * fails without it. - */ -export const testRequiredVariables = ( - dir: string, - vars: Readonly, -) => { - // Ensures that all required variables are provided. - it("required variables", async () => { - await runTerraformApply(dir, vars); - }); - - const varNames = Object.keys(vars); - for (const varName of varNames) { - // Ensures that every variable provided is required! - it(`missing variable: ${varName}`, async () => { - const localVars: TerraformVariables = {}; - for (const otherVarName of varNames) { - if (otherVarName !== varName) { - localVars[otherVarName] = vars[otherVarName]; - } - } - - try { - await runTerraformApply(dir, localVars); - } catch (ex) { - if (!(ex instanceof Error)) { - throw new Error("Unknown error generated"); - } - - expect(ex.message).toContain( - `input variable \"${varName}\" is not set`, - ); - return; - } - throw new Error(`${varName} is not a required variable!`); - }); - } -}; - -/** - * Runs terraform apply in the given directory with the given variables. It is - * fine to run in parallel with other instances of this function, as it uses a - * random state file. - */ -export const runTerraformApply = async ( - dir: string, - vars: Readonly, - customEnv?: Record, -): Promise => { - const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`; - - const childEnv: Record = { - ...process.env, - ...(customEnv ?? {}), - }; - - // This is a fix for when you try to run the tests from a Coder workspace. - // When process.env is destructured into the object, it can sometimes have - // workspace-specific values, which causes the resulting URL to be different - // from what the tests have classically expected. - childEnv.CODER_AGENT_URL = undefined; - childEnv.CODER_WORKSPACE_NAME = undefined; - - for (const [key, value] of Object.entries(vars) as [string, JsonValue][]) { - if (value !== null) { - childEnv[`TF_VAR_${key}`] = String(value); - } - } - - const proc = spawn( - [ - "terraform", - "apply", - "-compact-warnings", - "-input=false", - "-auto-approve", - "-state", - "-no-color", - stateFile, - ], - { - cwd: dir, - env: childEnv, - stderr: "pipe", - stdout: "pipe", - }, - ); - - const text = await readableStreamToText(proc.stderr); - const exitCode = await proc.exited; - if (exitCode !== 0) { - throw new Error(text); - } - - const content = await readFile(stateFile, "utf8"); - await unlink(stateFile); - return JSON.parse(content); -}; - -/** - * Runs terraform init in the given directory. - */ -export const runTerraformInit = async (dir: string) => { - const proc = spawn(["terraform", "init"], { - cwd: dir, - }); - const text = await readableStreamToText(proc.stdout); - const exitCode = await proc.exited; - if (exitCode !== 0) { - throw new Error(text); - } -}; - -export const createJSONResponse = (obj: object, statusCode = 200): Response => { - return new Response(JSON.stringify(obj), { - headers: { - "Content-Type": "application/json", - }, - status: statusCode, - }); -}; - -export const writeCoder = async (id: string, script: string) => { - await writeFileContainer(id, "/usr/bin/coder", script, { - user: "root", - }); - const execResult = await execContainer( - id, - ["chmod", "755", "/usr/bin/coder"], - ["--user", "root"], - ); - expect(execResult.exitCode).toBe(0); -}; - -export const writeFileContainer = async ( - id: string, - path: string, - content: string, - options?: { - user?: string; - }, -) => { - const contentBase64 = Buffer.from(content).toString("base64"); - const proc = await execContainer( - id, - ["sh", "-c", `echo '${contentBase64}' | base64 -d > '${path}'`], - options?.user ? ["--user", options.user] : undefined, - ); - if (proc.exitCode !== 0) { - throw new Error(`Failed to write file: ${proc.stderr}`); - } - expect(proc.exitCode).toBe(0); -}; - -export const readFileContainer = async (id: string, path: string) => { - const proc = await execContainer(id, ["cat", path], ["--user", "root"]); - if (proc.exitCode !== 0) { - console.log(proc.stderr); - console.log(proc.stdout); - } - expect(proc.exitCode).toBe(0); - return proc.stdout; -}; From 993ebf6d26e1aae52babaa36073c6d656c0e25b5 Mon Sep 17 00:00:00 2001 From: Rishi Mondal Date: Sat, 12 Jul 2025 16:19:15 +0530 Subject: [PATCH 2/6] structured --- registry/mavrickrishi/.images/avatar.svg | 4 - registry/mavrickrishi/avatar.jpeg | Bin 0 -> 7821 bytes test/test.ts | 336 +++++++++++++++++++++++ 3 files changed, 336 insertions(+), 4 deletions(-) delete mode 100644 registry/mavrickrishi/.images/avatar.svg create mode 100644 registry/mavrickrishi/avatar.jpeg create mode 100644 test/test.ts diff --git a/registry/mavrickrishi/.images/avatar.svg b/registry/mavrickrishi/.images/avatar.svg deleted file mode 100644 index 60d7eff6..00000000 --- a/registry/mavrickrishi/.images/avatar.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/registry/mavrickrishi/avatar.jpeg b/registry/mavrickrishi/avatar.jpeg new file mode 100644 index 0000000000000000000000000000000000000000..a624cf8fc8d1c168f42f82df3af4b8279df0d871 GIT binary patch literal 7821 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 NUh;}UK=I?;{{wmqIY => { + const proc = spawn([ + "docker", + "run", + "--rm", + "-d", + "--label", + "modules-test=true", + "--network", + "host", + "--entrypoint", + "sh", + image, + "-c", + init, + ]); + + const containerID = await readableStreamToText(proc.stdout); + const exitCode = await proc.exited; + if (exitCode !== 0) { + throw new Error(containerID); + } + return containerID.trim(); +}; + +export const removeContainer = async (id: string) => { + const proc = spawn(["docker", "rm", "-f", id], { + stderr: "pipe", + stdout: "pipe", + }); + const exitCode = await proc.exited; + const [stderr, stdout] = await Promise.all([ + readableStreamToText(proc.stderr ?? new ReadableStream()), + readableStreamToText(proc.stdout ?? new ReadableStream()), + ]); + if (exitCode !== 0) { + throw new Error(`${stderr}\n${stdout}`); + } +}; + +export interface scriptOutput { + exitCode: number; + stdout: string[]; + stderr: string[]; +} + +/** + * Finds the only "coder_script" resource in the given state and runs it in a + * container. + */ +export const executeScriptInContainer = async ( + state: TerraformState, + image: string, + shell = "sh", + before?: string, +): Promise => { + const instance = findResourceInstance(state, "coder_script"); + const id = await runContainer(image); + + if (before) { + await execContainer(id, [shell, "-c", before]); + } + + const resp = await execContainer(id, [shell, "-c", instance.script]); + const stdout = resp.stdout.trim().split("\n"); + const stderr = resp.stderr.trim().split("\n"); + return { + exitCode: resp.exitCode, + stdout, + stderr, + }; +}; + +export const execContainer = async ( + id: string, + cmd: string[], + args?: string[], +): Promise<{ + exitCode: number; + stderr: string; + stdout: string; +}> => { + const proc = spawn(["docker", "exec", ...(args ?? []), id, ...cmd], { + stderr: "pipe", + stdout: "pipe", + }); + const [stderr, stdout] = await Promise.all([ + readableStreamToText(proc.stderr), + readableStreamToText(proc.stdout), + ]); + const exitCode = await proc.exited; + return { + exitCode, + stderr, + stdout, + }; +}; + +type JsonValue = + | string + | number + | boolean + | null + | JsonValue[] + | { [key: string]: JsonValue }; + +type TerraformStateResource = { + type: string; + name: string; + provider: string; + + instances: [ + { + attributes: Record; + }, + ]; +}; + +type TerraformOutput = { + type: string; + value: JsonValue; +}; + +export interface TerraformState { + outputs: Record; + resources: [TerraformStateResource, ...TerraformStateResource[]]; +} + +type TerraformVariables = Record; + +export interface CoderScriptAttributes { + script: string; + agent_id: string; + url: string; +} + +export type ResourceInstance = + T extends "coder_script" ? CoderScriptAttributes : Record; + +/** + * finds the first instance of the given resource type in the given state. If + * name is specified, it will only find the instance with the given name. + */ +export const findResourceInstance = ( + state: TerraformState, + type: T, + name?: string, +): ResourceInstance => { + const resource = state.resources.find( + (resource) => + resource.type === type && (name ? resource.name === name : true), + ); + if (!resource) { + throw new Error(`Resource ${type} not found`); + } + if (resource.instances.length !== 1) { + throw new Error( + `Resource ${type} has ${resource.instances.length} instances`, + ); + } + + return resource.instances[0].attributes as ResourceInstance; +}; + +/** + * Creates a test-case for each variable provided and ensures that the apply + * fails without it. + */ +export const testRequiredVariables = ( + dir: string, + vars: Readonly, +) => { + // Ensures that all required variables are provided. + it("required variables", async () => { + await runTerraformApply(dir, vars); + }); + + const varNames = Object.keys(vars); + for (const varName of varNames) { + // Ensures that every variable provided is required! + it(`missing variable: ${varName}`, async () => { + const localVars: TerraformVariables = {}; + for (const otherVarName of varNames) { + if (otherVarName !== varName) { + localVars[otherVarName] = vars[otherVarName]; + } + } + + try { + await runTerraformApply(dir, localVars); + } catch (ex) { + if (!(ex instanceof Error)) { + throw new Error("Unknown error generated"); + } + + expect(ex.message).toContain( + `input variable \"${varName}\" is not set`, + ); + return; + } + throw new Error(`${varName} is not a required variable!`); + }); + } +}; + +/** + * Runs terraform apply in the given directory with the given variables. It is + * fine to run in parallel with other instances of this function, as it uses a + * random state file. + */ +export const runTerraformApply = async ( + dir: string, + vars: Readonly, + customEnv?: Record, +): Promise => { + const stateFile = `${dir}/${crypto.randomUUID()}.tfstate`; + + const childEnv: Record = { + ...process.env, + ...(customEnv ?? {}), + }; + + // This is a fix for when you try to run the tests from a Coder workspace. + // When process.env is destructured into the object, it can sometimes have + // workspace-specific values, which causes the resulting URL to be different + // from what the tests have classically expected. + childEnv.CODER_AGENT_URL = undefined; + childEnv.CODER_WORKSPACE_NAME = undefined; + + for (const [key, value] of Object.entries(vars) as [string, JsonValue][]) { + if (value !== null) { + childEnv[`TF_VAR_${key}`] = String(value); + } + } + + const proc = spawn( + [ + "terraform", + "apply", + "-compact-warnings", + "-input=false", + "-auto-approve", + "-state", + "-no-color", + stateFile, + ], + { + cwd: dir, + env: childEnv, + stderr: "pipe", + stdout: "pipe", + }, + ); + + const text = await readableStreamToText(proc.stderr); + const exitCode = await proc.exited; + if (exitCode !== 0) { + throw new Error(text); + } + + const content = await readFile(stateFile, "utf8"); + await unlink(stateFile); + return JSON.parse(content); +}; + +/** + * Runs terraform init in the given directory. + */ +export const runTerraformInit = async (dir: string) => { + const proc = spawn(["terraform", "init"], { + cwd: dir, + }); + const text = await readableStreamToText(proc.stdout); + const exitCode = await proc.exited; + if (exitCode !== 0) { + throw new Error(text); + } +}; + +export const createJSONResponse = (obj: object, statusCode = 200): Response => { + return new Response(JSON.stringify(obj), { + headers: { + "Content-Type": "application/json", + }, + status: statusCode, + }); +}; + +export const writeCoder = async (id: string, script: string) => { + await writeFileContainer(id, "/usr/bin/coder", script, { + user: "root", + }); + const execResult = await execContainer( + id, + ["chmod", "755", "/usr/bin/coder"], + ["--user", "root"], + ); + expect(execResult.exitCode).toBe(0); +}; + +export const writeFileContainer = async ( + id: string, + path: string, + content: string, + options?: { + user?: string; + }, +) => { + const contentBase64 = Buffer.from(content).toString("base64"); + const proc = await execContainer( + id, + ["sh", "-c", `echo '${contentBase64}' | base64 -d > '${path}'`], + options?.user ? ["--user", options.user] : undefined, + ); + if (proc.exitCode !== 0) { + throw new Error(`Failed to write file: ${proc.stderr}`); + } + expect(proc.exitCode).toBe(0); +}; + +export const readFileContainer = async (id: string, path: string) => { + const proc = await execContainer(id, ["cat", path], ["--user", "root"]); + if (proc.exitCode !== 0) { + console.log(proc.stderr); + console.log(proc.stdout); + } + expect(proc.exitCode).toBe(0); + return proc.stdout; +}; From 66f94136c83c9084cd6bc0c8d631099609e66922 Mon Sep 17 00:00:00 2001 From: Rishi Mondal Date: Sat, 12 Jul 2025 16:38:05 +0530 Subject: [PATCH 3/6] file strcture --- registry/mavrickrishi/{ => .images}/avatar.jpeg | Bin 1 file changed, 0 insertions(+), 0 deletions(-) rename registry/mavrickrishi/{ => .images}/avatar.jpeg (100%) diff --git a/registry/mavrickrishi/avatar.jpeg b/registry/mavrickrishi/.images/avatar.jpeg similarity index 100% rename from registry/mavrickrishi/avatar.jpeg rename to registry/mavrickrishi/.images/avatar.jpeg From e71b9dd965b38f383c70b34ae17c24b08fa4a472 Mon Sep 17 00:00:00 2001 From: Rishi Mondal Date: Sat, 12 Jul 2025 17:51:28 +0530 Subject: [PATCH 4/6] lint --- registry/mavrickrishi/README.md | 7 ++++--- .../mavrickrishi/modules/aws-ami-snapshot/README.md | 13 ++++++++++++- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/registry/mavrickrishi/README.md b/registry/mavrickrishi/README.md index 5e0c2e1f..abe413d0 100644 --- a/registry/mavrickrishi/README.md +++ b/registry/mavrickrishi/README.md @@ -1,7 +1,8 @@ --- -name: mavrickrishi -description: Modules and templates by mavrickrishi -github_url: https://github.com/MAVRICK-1 +display_name: "mavrickrishi" +description: "Modules and templates by mavrickrishi" +github_url: "https://github.com/MAVRICK-1" +status: "community" --- # mavrickrishi diff --git a/registry/mavrickrishi/modules/aws-ami-snapshot/README.md b/registry/mavrickrishi/modules/aws-ami-snapshot/README.md index 5364f496..9a694307 100644 --- a/registry/mavrickrishi/modules/aws-ami-snapshot/README.md +++ b/registry/mavrickrishi/modules/aws-ami-snapshot/README.md @@ -1,7 +1,7 @@ --- display_name: AWS AMI Snapshot description: Create and manage AMI snapshots for Coder workspaces with restore capabilities -icon: ../../../../../../.icons/aws.svg +icon: ../../../../.icons/aws.svg maintainer_github: MAVRICK-1 verified: false tags: [aws, snapshot, ami, backup, persistence] @@ -11,6 +11,17 @@ tags: [aws, snapshot, ami, backup, persistence] This module provides AMI-based snapshot functionality for Coder workspaces running on AWS EC2 instances. It enables users to create snapshots when workspaces are stopped and restore from previous snapshots when starting workspaces. +```tf +module "ami_snapshot" { + source = "registry.coder.com/mavrickrishi/aws-ami-snapshot/coder" + version = "1.0.0" + + instance_id = aws_instance.workspace.id + default_ami_id = data.aws_ami.ubuntu.id + template_name = "aws-linux" +} +``` + ## Features - **Automatic Snapshots**: Create AMI snapshots when workspaces are stopped From dd94a199417761d8515a04f1effbb20bcd546dca Mon Sep 17 00:00:00 2001 From: Rishi Mondal Date: Sat, 12 Jul 2025 19:33:05 +0530 Subject: [PATCH 5/6] lint error --- .../modules/aws-ami-snapshot/README.md | 4 +- .../modules/aws-ami-snapshot/main.test.ts | 48 +++++++++---------- 2 files changed, 26 insertions(+), 26 deletions(-) diff --git a/registry/mavrickrishi/modules/aws-ami-snapshot/README.md b/registry/mavrickrishi/modules/aws-ami-snapshot/README.md index 9a694307..1aed04a0 100644 --- a/registry/mavrickrishi/modules/aws-ami-snapshot/README.md +++ b/registry/mavrickrishi/modules/aws-ami-snapshot/README.md @@ -13,9 +13,9 @@ This module provides AMI-based snapshot functionality for Coder workspaces runni ```tf module "ami_snapshot" { - source = "registry.coder.com/mavrickrishi/aws-ami-snapshot/coder" + source = "registry.coder.com/mavrickrishi/aws-ami-snapshot/coder" version = "1.0.0" - + instance_id = aws_instance.workspace.id default_ami_id = data.aws_ami.ubuntu.id template_name = "aws-linux" diff --git a/registry/mavrickrishi/modules/aws-ami-snapshot/main.test.ts b/registry/mavrickrishi/modules/aws-ami-snapshot/main.test.ts index 3c062f93..d7898cae 100644 --- a/registry/mavrickrishi/modules/aws-ami-snapshot/main.test.ts +++ b/registry/mavrickrishi/modules/aws-ami-snapshot/main.test.ts @@ -1,31 +1,31 @@ import { describe, expect, it } from "bun:test"; import { - runTerraformApply, - runTerraformInit, - testRequiredVariables, + runTerraformApply, + runTerraformInit, + testRequiredVariables, } from "~test"; describe("aws-ami-snapshot", async () => { - await runTerraformInit(import.meta.dir); + await runTerraformInit(import.meta.dir); - testRequiredVariables(import.meta.dir, { - instance_id: "i-1234567890abcdef0", - default_ami_id: "ami-12345678", - template_name: "test-template", - }); + testRequiredVariables(import.meta.dir, { + instance_id: "i-1234567890abcdef0", + default_ami_id: "ami-12345678", + template_name: "test-template", + }); - it("supports optional variables", async () => { - await testRequiredVariables(import.meta.dir, { - instance_id: "i-1234567890abcdef0", - default_ami_id: "ami-12345678", - template_name: "test-template", - enable_dlm_cleanup: true, - dlm_role_arn: "arn:aws:iam::123456789012:role/dlm-lifecycle-role", - snapshot_retention_count: 5, - tags: { - Environment: "test", - Project: "coder", - }, - }); - }); -}); \ No newline at end of file + it("supports optional variables", async () => { + await testRequiredVariables(import.meta.dir, { + instance_id: "i-1234567890abcdef0", + default_ami_id: "ami-12345678", + template_name: "test-template", + enable_dlm_cleanup: true, + dlm_role_arn: "arn:aws:iam::123456789012:role/dlm-lifecycle-role", + snapshot_retention_count: 5, + tags: { + Environment: "test", + Project: "coder", + }, + }); + }); +}); From 7536a2ab4ccd8b749e7869745c2125b5daae01a9 Mon Sep 17 00:00:00 2001 From: Rishi Mondal Date: Sat, 12 Jul 2025 19:46:11 +0530 Subject: [PATCH 6/6] lint --- registry/mavrickrishi/modules/aws-ami-snapshot/main.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/registry/mavrickrishi/modules/aws-ami-snapshot/main.test.ts b/registry/mavrickrishi/modules/aws-ami-snapshot/main.test.ts index d7898cae..fb8a9702 100644 --- a/registry/mavrickrishi/modules/aws-ami-snapshot/main.test.ts +++ b/registry/mavrickrishi/modules/aws-ami-snapshot/main.test.ts @@ -15,7 +15,7 @@ describe("aws-ami-snapshot", async () => { }); it("supports optional variables", async () => { - await testRequiredVariables(import.meta.dir, { + await runTerraformApply(import.meta.dir, { instance_id: "i-1234567890abcdef0", default_ami_id: "ami-12345678", template_name: "test-template",