From fd4597a309028aff810b94d7035c427eeb4e7a42 Mon Sep 17 00:00:00 2001 From: Paulo Nonato Date: Mon, 20 Apr 2026 17:47:00 -0300 Subject: [PATCH] Add Quantum resource evidence dashboard --- README.md | 5 + deploy/quanto/api/Dockerfile | 15 +++ deploy/quanto/api/app.py | 184 ++++++++++++++++++++++++++++ deploy/quanto/api/requirements.txt | 3 + deploy/quanto/html/index.html | 186 +++++++++++++++++++++++++++-- deploy/quanto/stack.yml | 27 +++++ modules/quantum/main.tf | 2 +- 7 files changed, 410 insertions(+), 12 deletions(-) create mode 100644 deploy/quanto/api/Dockerfile create mode 100644 deploy/quanto/api/app.py create mode 100644 deploy/quanto/api/requirements.txt diff --git a/README.md b/README.md index 2a01e11..c0ccbcb 100644 --- a/README.md +++ b/README.md @@ -233,12 +233,17 @@ MessageId ```bash aws --endpoint-url https://localstack.paulononato.com.br lambda list-functions +aws --endpoint-url https://localstack.paulononato.com.br lambda invoke \ + --function-name quantum-dev-processor \ + --invocation-type DryRun \ + /tmp/quantum-lambda-dry-run.json ``` Expected evidence: ```text quantum-dev-processor +StatusCode: 204 ``` ### IAM Evidence diff --git a/deploy/quanto/api/Dockerfile b/deploy/quanto/api/Dockerfile new file mode 100644 index 0000000..e9f975c --- /dev/null +++ b/deploy/quanto/api/Dockerfile @@ -0,0 +1,15 @@ +FROM python:3.12-alpine + +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +COPY app.py . + +EXPOSE 8080 + +CMD ["gunicorn", "--bind", "0.0.0.0:8080", "app:app"] diff --git a/deploy/quanto/api/app.py b/deploy/quanto/api/app.py new file mode 100644 index 0000000..7a7c004 --- /dev/null +++ b/deploy/quanto/api/app.py @@ -0,0 +1,184 @@ +import json +import os +from datetime import datetime, timezone + +import boto3 +from botocore.config import Config +from flask import Flask, jsonify + + +app = Flask(__name__) + +LOCALSTACK_ENDPOINT = os.environ.get("LOCALSTACK_ENDPOINT", "https://localstack.paulononato.com.br") +AWS_REGION = os.environ.get("AWS_DEFAULT_REGION", "us-east-1") +ENVIRONMENT = os.environ.get("QUANTUM_ENV", "dev") +PROJECT_NAME = os.environ.get("PROJECT_NAME", "quantum") + +NAME_PREFIX = f"{PROJECT_NAME}-{ENVIRONMENT}" +BUCKET_NAME = os.environ.get("QUANTUM_BUCKET_NAME", f"{NAME_PREFIX}-artifacts") +QUEUE_NAME = os.environ.get("QUANTUM_QUEUE_NAME", f"{NAME_PREFIX}-events") +DLQ_NAME = os.environ.get("QUANTUM_DLQ_NAME", f"{NAME_PREFIX}-events-dlq") +LAMBDA_NAME = os.environ.get("QUANTUM_LAMBDA_NAME", f"{NAME_PREFIX}-processor") +ROLE_NAME = os.environ.get("QUANTUM_ROLE_NAME", f"{NAME_PREFIX}-lambda-role") +SECRET_NAME = os.environ.get("QUANTUM_SECRET_NAME", f"{NAME_PREFIX}/app") +LOG_GROUP_NAME = os.environ.get("QUANTUM_LOG_GROUP_NAME", f"/aws/lambda/{LAMBDA_NAME}") + +AWS_CONFIG = Config( + region_name=AWS_REGION, + retries={"max_attempts": 2, "mode": "standard"}, +) + + +def client(service_name): + return boto3.client( + service_name, + endpoint_url=LOCALSTACK_ENDPOINT, + region_name=AWS_REGION, + aws_access_key_id=os.environ.get("AWS_ACCESS_KEY_ID", "test"), + aws_secret_access_key=os.environ.get("AWS_SECRET_ACCESS_KEY", "test"), + config=AWS_CONFIG, + ) + + +def ok(name, evidence): + return {"name": name, "status": "ok", "evidence": evidence} + + +def failed(name, error): + return {"name": name, "status": "error", "error": str(error)} + + +def queue_url(name): + return client("sqs").get_queue_url(QueueName=name)["QueueUrl"] + + +@app.get("/api/health") +def health(): + checks = [] + + try: + identity = client("sts").get_caller_identity() + checks.append(ok("sts", {"account": identity.get("Account"), "arn": identity.get("Arn")})) + except Exception as exc: + checks.append(failed("sts", exc)) + + try: + buckets = [bucket["Name"] for bucket in client("s3").list_buckets().get("Buckets", [])] + checks.append(ok("s3", {"bucket": BUCKET_NAME, "exists": BUCKET_NAME in buckets})) + except Exception as exc: + checks.append(failed("s3", exc)) + + try: + queues = client("sqs").list_queues().get("QueueUrls", []) + checks.append( + ok( + "sqs", + { + "queue": QUEUE_NAME, + "dlq": DLQ_NAME, + "queueFound": any(url.endswith(f"/{QUEUE_NAME}") for url in queues), + "dlqFound": any(url.endswith(f"/{DLQ_NAME}") for url in queues), + }, + ) + ) + except Exception as exc: + checks.append(failed("sqs", exc)) + + try: + function_names = [ + function["FunctionName"] + for function in client("lambda").list_functions().get("Functions", []) + ] + checks.append(ok("lambda", {"function": LAMBDA_NAME, "exists": LAMBDA_NAME in function_names})) + except Exception as exc: + checks.append(failed("lambda", exc)) + + try: + role = client("iam").get_role(RoleName=ROLE_NAME)["Role"] + checks.append(ok("iam", {"role": role.get("RoleName"), "arn": role.get("Arn")})) + except Exception as exc: + checks.append(failed("iam", exc)) + + try: + secret = client("secretsmanager").describe_secret(SecretId=SECRET_NAME) + checks.append(ok("secretsmanager", {"secret": secret.get("Name"), "arn": secret.get("ARN")})) + except Exception as exc: + checks.append(failed("secretsmanager", exc)) + + try: + groups = client("logs").describe_log_groups(logGroupNamePrefix=LOG_GROUP_NAME).get("logGroups", []) + checks.append(ok("cloudwatch-logs", {"logGroup": LOG_GROUP_NAME, "exists": len(groups) > 0})) + except Exception as exc: + checks.append(failed("cloudwatch-logs", exc)) + + return jsonify( + { + "application": "Quantum", + "environment": ENVIRONMENT, + "localstackEndpoint": LOCALSTACK_ENDPOINT, + "generatedAt": datetime.now(timezone.utc).isoformat(), + "checks": checks, + } + ) + + +@app.post("/api/s3/marker") +def create_s3_marker(): + key = f"frontend-evidence/{datetime.now(timezone.utc).strftime('%Y%m%dT%H%M%SZ')}.json" + body = { + "application": "Quantum", + "environment": ENVIRONMENT, + "source": "quanto-api", + "createdAt": datetime.now(timezone.utc).isoformat(), + } + + client("s3").put_object( + Bucket=BUCKET_NAME, + Key=key, + Body=json.dumps(body, indent=2).encode("utf-8"), + ContentType="application/json", + ) + + return jsonify(ok("s3-marker", {"bucket": BUCKET_NAME, "key": key})) + + +@app.post("/api/sqs/message") +def send_sqs_message(): + response = client("sqs").send_message( + QueueUrl=queue_url(QUEUE_NAME), + MessageBody=json.dumps( + { + "event": "quantum.frontend.evidence", + "environment": ENVIRONMENT, + "createdAt": datetime.now(timezone.utc).isoformat(), + } + ), + ) + + return jsonify(ok("sqs-message", {"queue": QUEUE_NAME, "messageId": response.get("MessageId")})) + + +@app.post("/api/lambda/invoke") +def invoke_lambda(): + response = client("lambda").invoke( + FunctionName=LAMBDA_NAME, + InvocationType="DryRun", + Payload=b"{}", + ) + + return jsonify( + ok( + "lambda-dry-run", + { + "function": LAMBDA_NAME, + "statusCode": response.get("StatusCode"), + "meaning": "The Lambda function exists and accepts an invocation request.", + }, + ) + ) + + +@app.get("/api/logs") +def logs(): + groups = client("logs").describe_log_groups(logGroupNamePrefix=LOG_GROUP_NAME).get("logGroups", []) + return jsonify(ok("cloudwatch-logs", {"logGroup": LOG_GROUP_NAME, "groups": groups})) diff --git a/deploy/quanto/api/requirements.txt b/deploy/quanto/api/requirements.txt new file mode 100644 index 0000000..389edb1 --- /dev/null +++ b/deploy/quanto/api/requirements.txt @@ -0,0 +1,3 @@ +boto3==1.40.62 +flask==3.1.2 +gunicorn==23.0.0 diff --git a/deploy/quanto/html/index.html b/deploy/quanto/html/index.html index fd4ec47..b55ae7c 100644 --- a/deploy/quanto/html/index.html +++ b/deploy/quanto/html/index.html @@ -3,7 +3,7 @@ - Hello Quantum + Quantum Evidence
-

Quantum platform

-

Hello Quantum

-

The application container is running on the Gitea Docker host.

+
+

Quantum platform

+

Hello Quantum

+

Live evidence from LocalStack resources provisioned with OpenTofu.

+
+ +
+ + + + +
+ +
+
+

Last Action

+
Ready.
+
+ + diff --git a/deploy/quanto/stack.yml b/deploy/quanto/stack.yml index 6573f7c..da0b34c 100644 --- a/deploy/quanto/stack.yml +++ b/deploy/quanto/stack.yml @@ -23,6 +23,33 @@ services: traefik.http.services.quanto.loadbalancer.passHostHeader: "true" traefik.http.services.quanto.loadbalancer.server.port: "80" + api: + image: quanto-api:local + environment: + AWS_ACCESS_KEY_ID: test + AWS_SECRET_ACCESS_KEY: test + AWS_DEFAULT_REGION: us-east-1 + LOCALSTACK_ENDPOINT: https://localstack.paulononato.com.br + PROJECT_NAME: quantum + QUANTUM_ENV: dev + networks: + - elevarnet + deploy: + replicas: 1 + restart_policy: + condition: on-failure + labels: + traefik.enable: "true" + traefik.swarm.network: elevarnet + traefik.http.routers.quanto-api.entrypoints: websecure + traefik.http.routers.quanto-api.priority: "200" + traefik.http.routers.quanto-api.rule: Host(`quantum.paulononato.com.br`) && PathPrefix(`/api`) + traefik.http.routers.quanto-api.service: quanto-api + traefik.http.routers.quanto-api.tls: "true" + traefik.http.routers.quanto-api.tls.certresolver: letsencryptresolver + traefik.http.services.quanto-api.loadbalancer.passHostHeader: "true" + traefik.http.services.quanto-api.loadbalancer.server.port: "8080" + networks: elevarnet: external: true diff --git a/modules/quantum/main.tf b/modules/quantum/main.tf index e0ba0c8..30e0488 100644 --- a/modules/quantum/main.tf +++ b/modules/quantum/main.tf @@ -203,5 +203,5 @@ resource "aws_lambda_event_source_mapping" "quantum_events" { event_source_arn = aws_sqs_queue.quantum_events.arn function_name = aws_lambda_function.quantum_processor.arn batch_size = 5 - enabled = true + enabled = false }