From 501b4c02e2d1545b38b788fd01eb4737d12d98e1 Mon Sep 17 00:00:00 2001 From: Shyam Kulkarni Date: Sat, 8 Aug 2026 15:34:43 +0530 Subject: [PATCH 1/7] feat(mcp): Add EKS node diagnostics MCP Port the sample-eks-node-diagnostics-mcp project into mcp/aws-eks-node-diagnostics-mcp/, including CDK infrastructure, Lambda sources, diagnostic runbooks, CNI semantics, the packaged skill, and tests. The skill archive (skills/eks-node-diagnostics.zip) is force-added because the repo's root .gitignore (*.zip) would otherwise exclude it. --- mcp/aws-eks-node-diagnostics-mcp/.gitignore | 39 + mcp/aws-eks-node-diagnostics-mcp/LICENSE | 16 + mcp/aws-eks-node-diagnostics-mcp/README.md | 518 + mcp/aws-eks-node-diagnostics-mcp/bin/app.ts | 90 + mcp/aws-eks-node-diagnostics-mcp/cdk.json | 23 + mcp/aws-eks-node-diagnostics-mcp/deploy.sh | 529 + .../docs/ARCHITECTURE.md | 449 + .../get-config.sh | 52 + .../package-lock.json | 5701 +++++++++ mcp/aws-eks-node-diagnostics-mcp/package.json | 45 + .../requirements-dev.txt | 3 + .../scripts/pre-commit-cfn-lint.sh | 57 + .../skills/eks-node-diagnostics.zip | Bin 0 -> 190116 bytes .../sops/cni-semantics/claim-enforcement.md | 101 + .../sops/cni-semantics/cni-config-index.yaml | 674 + .../cni-semantics/guardrail-templates.yaml | 708 + .../cni-semantics/hallucination-patterns.yaml | 323 + .../hallucination-regression-tests.yaml | 442 + .../runbooks/A1-node-not-ready-kubelet-oom.md | 139 + .../A2-node-bootstrap-registration-failure.md | 123 + .../A2-node-not-ready-certificate-expired.md | 119 + .../sops/runbooks/A3-clock-skew.md | 136 + .../runbooks/A4-worker-node-join-failure.md | 337 + .../B1-kubelet-configuration-errors.md | 119 + .../runbooks/B2-eviction-manager-issues.md | 131 + .../sops/runbooks/B3-pleg-issues.md | 138 + .../sops/runbooks/C1-image-pull-failures.md | 153 + .../runbooks/C2-sandbox-creation-failures.md | 148 + .../runbooks/C3-overlayfs-inode-exhaustion.md | 116 + .../D1-vpc-cni-ip-allocation-failures.md | 145 + .../runbooks/D2-kube-proxy-iptables-sync.md | 136 + .../sops/runbooks/D3-conntrack-exhaustion.md | 118 + .../sops/runbooks/D4-mtu-fragmentation.md | 113 + .../sops/runbooks/D5-dns-failures.md | 157 + .../sops/runbooks/D6-ena-throttling.md | 134 + .../D7-network-performance-degradation.md | 215 + .../D8-kube-proxy-service-connectivity.md | 338 + .../runbooks/D9-pod-to-pod-connectivity.md | 387 + .../E1-ebs-csi-attach-mount-timeout.md | 120 + .../sops/runbooks/E2-efs-mount-failures.md | 114 + .../runbooks/F1-insufficient-cpu-memory.md | 155 + .../sops/runbooks/F2-max-pods-limit.md | 133 + .../F3-taints-tolerations-node-selectors.md | 125 + .../G1-disk-pressure-eviction-storms.md | 135 + .../runbooks/G2-oomkill-memory-pressure.md | 128 + .../sops/runbooks/G3-pid-pressure.md | 132 + .../H1-node-role-missing-permissions.md | 145 + .../H2-irsa-pod-identity-confusion.md | 170 + .../sops/runbooks/H3-imds-issues.md | 132 + .../sops/runbooks/I1-version-skew.md | 161 + .../J1-ena-throttling-instance-limits.md | 138 + .../sops/runbooks/J2-ebs-transient-attach.md | 171 + .../sops/runbooks/J3-az-outage-impact.md | 103 + .../runbooks/K1-stuck-terminating-pods.md | 188 + .../sops/runbooks/K2-probe-failures.md | 221 + .../sops/runbooks/K3-crashloopbackoff.md | 235 + .../K4-containerd-runtime-failures.md | 234 + .../runbooks/K5-csi-node-plugin-failures.md | 244 + .../runbooks/Z1-general-troubleshooting.md | 205 + mcp/aws-eks-node-diagnostics-mcp/src/index.ts | 2 + .../src/lambda/ssm-automation-enhanced.py | 10666 ++++++++++++++++ .../ssm-automation-gateway-construct-v2.ts | 2839 ++++ .../src/ssm-automation-gateway-stack-v2.ts | 53 + .../tests/construct-iam.property.test.ts | 328 + .../tests/construct-kms-s3.property.test.ts | 194 + .../tests/test_instance_validation.py | 94 + .../tests/test_region_validation.py | 121 + .../tests/test_tool_validation_wiring.py | 111 + .../tsconfig.json | 24 + 69 files changed, 30993 insertions(+) create mode 100644 mcp/aws-eks-node-diagnostics-mcp/.gitignore create mode 100644 mcp/aws-eks-node-diagnostics-mcp/LICENSE create mode 100644 mcp/aws-eks-node-diagnostics-mcp/README.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/bin/app.ts create mode 100644 mcp/aws-eks-node-diagnostics-mcp/cdk.json create mode 100755 mcp/aws-eks-node-diagnostics-mcp/deploy.sh create mode 100644 mcp/aws-eks-node-diagnostics-mcp/docs/ARCHITECTURE.md create mode 100755 mcp/aws-eks-node-diagnostics-mcp/get-config.sh create mode 100644 mcp/aws-eks-node-diagnostics-mcp/package-lock.json create mode 100644 mcp/aws-eks-node-diagnostics-mcp/package.json create mode 100644 mcp/aws-eks-node-diagnostics-mcp/requirements-dev.txt create mode 100755 mcp/aws-eks-node-diagnostics-mcp/scripts/pre-commit-cfn-lint.sh create mode 100644 mcp/aws-eks-node-diagnostics-mcp/skills/eks-node-diagnostics.zip create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/cni-semantics/claim-enforcement.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/cni-semantics/cni-config-index.yaml create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/cni-semantics/guardrail-templates.yaml create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/cni-semantics/hallucination-patterns.yaml create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/cni-semantics/hallucination-regression-tests.yaml create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A1-node-not-ready-kubelet-oom.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A2-node-bootstrap-registration-failure.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A2-node-not-ready-certificate-expired.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A3-clock-skew.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A4-worker-node-join-failure.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/B1-kubelet-configuration-errors.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/B2-eviction-manager-issues.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/B3-pleg-issues.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/C1-image-pull-failures.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/C2-sandbox-creation-failures.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/C3-overlayfs-inode-exhaustion.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D1-vpc-cni-ip-allocation-failures.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D2-kube-proxy-iptables-sync.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D3-conntrack-exhaustion.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D4-mtu-fragmentation.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D5-dns-failures.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D6-ena-throttling.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D7-network-performance-degradation.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D8-kube-proxy-service-connectivity.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D9-pod-to-pod-connectivity.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/E1-ebs-csi-attach-mount-timeout.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/E2-efs-mount-failures.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/F1-insufficient-cpu-memory.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/F2-max-pods-limit.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/F3-taints-tolerations-node-selectors.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/G1-disk-pressure-eviction-storms.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/G2-oomkill-memory-pressure.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/G3-pid-pressure.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/H1-node-role-missing-permissions.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/H2-irsa-pod-identity-confusion.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/H3-imds-issues.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/I1-version-skew.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/J1-ena-throttling-instance-limits.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/J2-ebs-transient-attach.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/J3-az-outage-impact.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/K1-stuck-terminating-pods.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/K2-probe-failures.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/K3-crashloopbackoff.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/K4-containerd-runtime-failures.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/K5-csi-node-plugin-failures.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/Z1-general-troubleshooting.md create mode 100644 mcp/aws-eks-node-diagnostics-mcp/src/index.ts create mode 100644 mcp/aws-eks-node-diagnostics-mcp/src/lambda/ssm-automation-enhanced.py create mode 100644 mcp/aws-eks-node-diagnostics-mcp/src/ssm-automation-gateway-construct-v2.ts create mode 100644 mcp/aws-eks-node-diagnostics-mcp/src/ssm-automation-gateway-stack-v2.ts create mode 100644 mcp/aws-eks-node-diagnostics-mcp/tests/construct-iam.property.test.ts create mode 100644 mcp/aws-eks-node-diagnostics-mcp/tests/construct-kms-s3.property.test.ts create mode 100644 mcp/aws-eks-node-diagnostics-mcp/tests/test_instance_validation.py create mode 100644 mcp/aws-eks-node-diagnostics-mcp/tests/test_region_validation.py create mode 100644 mcp/aws-eks-node-diagnostics-mcp/tests/test_tool_validation_wiring.py create mode 100644 mcp/aws-eks-node-diagnostics-mcp/tsconfig.json diff --git a/mcp/aws-eks-node-diagnostics-mcp/.gitignore b/mcp/aws-eks-node-diagnostics-mcp/.gitignore new file mode 100644 index 0000000..e6227bd --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/.gitignore @@ -0,0 +1,39 @@ +# Dependencies +node_modules/ + +# Build output +lib/ +*.js +*.d.ts + +# CDK output +cdk.out/ + +# IDE +.idea/ +.vscode/ +*.swp +*.swo + +# OS +.DS_Store +Thumbs.db + +# Logs +*.log +npm-debug.log* + +# Test coverage +coverage/ + +# Python cache +__pycache__/ +*.pyc +.pytest_cache/ + +# Hypothesis test framework cache +.hypothesis/ + +# Deployment-specific outputs (contain account IDs, secrets) +mcp-config.txt +cdk-outputs.json diff --git a/mcp/aws-eks-node-diagnostics-mcp/LICENSE b/mcp/aws-eks-node-diagnostics-mcp/LICENSE new file mode 100644 index 0000000..56a66b6 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/LICENSE @@ -0,0 +1,16 @@ +MIT No Attribution + +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + +Permission is hereby granted, free of charge, to any person obtaining a copy of +this software and associated documentation files (the "Software"), to deal in +the Software without restriction, including without limitation the rights to +use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of +the Software, and to permit persons to whom the Software is furnished to do so. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS +FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR +COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN +CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/mcp/aws-eks-node-diagnostics-mcp/README.md b/mcp/aws-eks-node-diagnostics-mcp/README.md new file mode 100644 index 0000000..1d39f80 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/README.md @@ -0,0 +1,518 @@ +# EKS Node Diagnostics MCP + +> **⚠️ Proof of Concept (POC):** This project is a proof of concept and should be tested in non-production environments first. Validate thoroughly in a staging or development account before using with production workloads. + +MCP Server for AWS DevOps Agent to collect and analyze diagnostic logs from EKS worker nodes using SSM Automation. Covers 20+ log sources including kubelet, containerd, iptables, CNI config, route tables, dmesg, IPAMD, and more — artifacts that live on the node OS and aren't accessible through the Kubernetes API or CloudWatch. + +> **Want to understand the internals?** See [Architecture & Design](docs/ARCHITECTURE.md) for a deep dive into how the components work, data flows, tool design, and security model. + +--- + +## Prerequisites + +### 1. Node.js (v18.x or later) + +**macOS (Homebrew):** +```bash +brew install node +``` + +**Linux (Ubuntu/Debian):** +```bash +curl -fsSL https://deb.nodesource.com/setup_18.x | sudo -E bash - +sudo apt-get install -y nodejs +``` + +### 2. AWS CLI v2 + +**macOS:** +```bash +brew install awscli +``` + +**Linux:** +```bash +curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip" +unzip awscliv2.zip +sudo ./aws/install +``` + +### 3. AWS CDK CLI + +```bash +npm install -g aws-cdk +``` + +### 4. Python 3 + +Most systems have it pre-installed: +```bash +python3 --version +``` + +### 5. AWS Credentials + +You need permissions to create IAM Roles, Lambda Functions, S3 Buckets, KMS Keys, Cognito User Pools, and BedrockAgentCore Gateways. + +```bash +aws configure +# Or use AWS SSO: +aws sso login --profile your-profile +export AWS_PROFILE=your-profile +``` + +### 6. crictl on Worker Nodes (for pod-level tcpdump) + +Required only for `tcpdump_capture` with `podName`/`podNamespace`. EKS-optimized AMIs include it by default. + +--- + +## Deployment + +```bash +# Clone the repository +git clone https://github.com/aws-samples/sample-eks-node-diagnostics-mcp.git +cd sample-eks-node-diagnostics-mcp + +# Make the script executable +chmod +x deploy.sh + +# Deploy (defaults to us-east-1) +./deploy.sh + +# Or deploy to a specific region +AWS_REGION=us-west-2 ./deploy.sh +``` + +`deploy.sh` derives the security scope automatically from your interactive choices — there's no separate "tighten" step. After you pick clusters, the script exports: + +- `ALLOWED_REGIONS` from the regions of the selected clusters. +- `ALLOWED_CLUSTER_NAMES` from the names of the selected clusters. +- `EKS_NODE_ROLE_ARNS` from the selected nodegroup roles. + +These flow straight into the CDK construct, so the deployed IAM policies are tag-scoped and region-scoped without any extra flags. If you skip cluster selection (or no clusters are found), the script falls back to deploy-region-only and prompts before deploying with an unrestricted cluster scope. + +### Interactive Deployment Flow + +The deploy script walks you through three interactive prompts: + +**Step 1 — Region selection:** +``` +Which AWS regions should be scanned for EKS clusters? + + 1) All enabled regions + 2) Current deploy region only (us-east-1) + 3) Enter a specific region + +Select [1/2/3] (default: 1): +``` + +**Step 2 — Cluster selection:** +``` +Found 4 EKS cluster(s): + + 1) prod-cluster (us-east-1) + 2) dev-cluster (us-east-1) + 3) analytics (us-west-2) + 4) eu-cluster (eu-west-1) + + a) All clusters + +Select clusters (comma-separated numbers, or 'a' for all) [default: a]: +``` + +**Step 3 — Node role selection:** +``` +Found 3 unique node role(s): + + 1) arn:aws:iam::123456789012:role/eks-prod-node-role + └─ eks-prod-node-role (prod-cluster / us-east-1) + 2) arn:aws:iam::123456789012:role/eks-dev-node-role + └─ eks-dev-node-role (dev-cluster / us-east-1) + 3) arn:aws:iam::123456789012:role/eks-eu-node-role + └─ eks-eu-node-role (eu-cluster / eu-west-1) + + a) All roles + +Select node roles (comma-separated numbers, or 'a' for all) [default: a]: +``` + +**Fallback — Manual ARN entry:** + +If no EKS clusters or node roles are found, the script prompts you to enter role ARNs manually: +``` +WARNING: No EKS clusters found in the selected region(s). + +Would you like to manually enter node role ARN(s)? [y/N]: y +Enter comma-separated role ARNs (e.g. arn:aws:iam::123456789012:role/MyNodeRole): +> +``` + +### Non-Interactive / CI Mode + +Pre-set the env vars to skip every prompt. Recommended for repeatable deploys: + +```bash +AWS_REGION=us-east-1 \ +ALLOWED_REGIONS=us-east-1 \ +ALLOWED_CLUSTER_NAMES=prod-cluster,staging-cluster \ +EKS_NODE_ROLE_ARNS=arn:aws:iam::123456789012:role/eks-node-role \ +./deploy.sh EksNodeLogMcpStack +``` + +If you genuinely need the wildcard scope (Lambda may target any EKS cluster in the account), opt in explicitly: + +```bash +AWS_REGION=us-east-1 \ +ALLOW_ANY_CLUSTER_NAME=true \ +EKS_NODE_ROLE_ARNS=arn:aws:iam::123456789012:role/eks-node-role \ +./deploy.sh EksNodeLogMcpStack +``` + +Without one of `ALLOWED_CLUSTER_NAMES` or `ALLOW_ANY_CLUSTER_NAME=true`, `cdk synth` fails with a clear error — this is intentional. + +### Maximum Restriction + +For production deploys, layer in the rest of the controls: + +```bash +AWS_REGION=us-west-2 \ +ALLOWED_REGIONS=us-west-2 \ +ALLOWED_CLUSTER_NAMES=prod-cluster \ +ALLOWED_SSM_DOCUMENTS=AWS-RunShellScript \ +EKS_NODE_ROLE_ARNS=arn:aws:iam::123456789012:role/eks-node-role \ +PRESIGNED_URL_EXPIRATION=120 \ +PCAP_PRESIGNED_URL_EXPIRATION=30 \ +PER_CALLER_RATE_LIMIT_PER_MINUTE=30 \ +TOOL_AUTHORIZATION="collect:client-soc;tcpdump_capture:client-emergency" \ +MCP_VPC_ID=vpc-0123456789abcdef0 \ +MCP_VPC_SUBNET_IDS=subnet-aaa,subnet-bbb \ +MAX_PCAP_BYTES=104857600 \ +./deploy.sh +``` + +| Env var | What it restricts | Default | +|---------|-------------------|---------| +| `ALLOWED_REGIONS` | IAM resource ARNs + Lambda region scanning | Stack region | +| `ALLOWED_CLUSTER_NAMES` | `ssm:SendCommand` tag condition on instances | (none — fail-closed) | +| `ALLOW_ANY_CLUSTER_NAME` | Explicit opt-in to any-cluster wildcard | `false` | +| `ALLOWED_SSM_DOCUMENTS` | Which SSM documents can be executed | `AWS-RunShellScript` | +| `EKS_NODE_ROLE_ARNS` | S3 PutObject + KMS Encrypt principals | Account root | +| `PRESIGNED_URL_EXPIRATION` | Log artifact presigned URL lifetime (max 900 s) | 300 s | +| `PCAP_PRESIGNED_URL_EXPIRATION` | Pcap presigned URL lifetime (max 300 s) | 60 s | +| `ENABLED_RESTRICTED_TOOLS` | tcpdump tools availability | Empty (not available) | +| `TOOL_AUTHORIZATION` | Per-tool client-id ACL (`tool:client_a,client_b;…`) | Empty (open) | +| `PER_CALLER_RATE_LIMIT_PER_MINUTE` | Rate limit per caller (`0` disables) | 60 | +| `MCP_VPC_ID` / `MCP_VPC_SUBNET_IDS` | Run Lambda in VPC + create S3/KMS endpoints | None | +| `MAX_PCAP_BYTES` | Pcap upload size cap (warning only) | 200 MiB | + +### What Gets Deployed + +| Resource | Purpose | +|----------|---------| +| S3 Bucket (KMS encrypted) | Stores collected log bundles | +| S3 Bucket (SOPs) | Stores 41 runbooks, auto-deployed via CDK | +| Lambda (SSM Automation) | Handles all 21 MCP tool invocations (19 always-on + 2 restricted tcpdump) | +| Lambda (Unzip) | Auto-extracts uploaded archives | +| Lambda (Findings Indexer) | Pre-indexes errors for fast retrieval | +| SSM Automation Role | Runs log collection on EC2 instances | +| Cognito User Pool | OAuth2 authentication for MCP Gateway | +| BedrockAgentCore Gateway | MCP protocol endpoint | +| KMS Key | Encrypts all data at rest | + +--- + +## Security Model + +All security controls are enforced by default. The construct fails synth unless you make an explicit cluster scope choice — there is no implicit wildcard. + +### Defaults (no extra config) + +| Control | Default | Configurable via | +|---------|---------|------------------| +| **Region restriction** | Stack region only | `ALLOWED_REGIONS` env var | +| **Cluster restriction** | **Fail-closed** — must set `ALLOWED_CLUSTER_NAMES` or `ALLOW_ANY_CLUSTER_NAME=true` | `ALLOWED_CLUSTER_NAMES`, `ALLOW_ANY_CLUSTER_NAME` | +| **SSM document restriction** | `AWS-RunShellScript` only | `ALLOWED_SSM_DOCUMENTS` env var | +| **tcpdump tools** | Removed from routing table | `ENABLED_RESTRICTED_TOOLS` env var | +| **Presigned URL expiry (logs)** | 300 s, max 900 s | `PRESIGNED_URL_EXPIRATION` env var | +| **Presigned URL expiry (pcap)** | 60 s, max 300 s | `PCAP_PRESIGNED_URL_EXPIRATION` env var | +| **Per-tool authorization** | All authenticated callers may invoke any non-restricted tool | `TOOL_AUTHORIZATION` env var | +| **Per-caller rate limit** | 60 invocations / min / caller | `PER_CALLER_RATE_LIMIT_PER_MINUTE` env var (0 disables) | +| **VPC endpoints (S3, KMS, SSM, EC2, Logs, Metrics)** | Off (Lambda runs outside a VPC) | `MCP_VPC_ID` + `MCP_VPC_SUBNET_IDS` | +| **Pcap upload bound** | 200 MiB (warns when exceeded) | `MAX_PCAP_BYTES` env var | +| **Response redaction** | SG/ENI/subnet/VPC IDs, account IDs in ARNs, private IPs (network tools), IAM error bodies, JWT/AKIA tokens, fields named `*password*`/`*secret*`/`*token*`/`*credential*` | Always on | +| **S3 encryption** | SSE-KMS with auto-rotating key | `enableEncryption` CDK prop | +| **S3 public access** | Blocked | Always on | +| **S3 transport** | SSL enforced | Always on | +| **Authentication** | Cognito OAuth2 client credentials | Always on | +| **EKS instance validation** | Tag-based + EKS API cross-reference | Always on | +| **BPF filter validation** | Allowlist-based (not denylist) | Always on | +| **Idempotency writes** | S3 conditional writes (`IfNoneMatch=*`) | Always on | +| **Baseline counter writes** | Optimistic concurrency (`IfMatch=`, retry on `PreconditionFailed`) | Always on | +| **Log auto-deletion** | 1 day | `logRetentionDays` CDK prop | + +### IAM Scoping + +`ssm:SendCommand` is restricted at three levels: + +1. **Resource ARNs** — instance ARNs are scoped to `ALLOWED_REGIONS` (e.g., `arn:aws:ec2:us-west-2:ACCOUNT:instance/*`). Document ARNs are scoped to specific document names (e.g., `document/AWS-RunShellScript`). +2. **Tag conditions** — instances must have the `eks:cluster-name` tag matching `ALLOWED_CLUSTER_NAMES`. With specific names, the condition uses `StringEquals` (exact match). The wildcard form (`StringLike: *`) is only emitted when `ALLOW_ANY_CLUSTER_NAME=true`. +3. **Region conditions** — all SSM, EC2, and EKS actions include `aws:RequestedRegion` conditions. + +If `ALLOWED_CLUSTER_NAMES` is empty **and** `ALLOW_ANY_CLUSTER_NAME` is not `true`, `cdk synth` fails with: + +``` +Error: SsmAutomationGatewayV2: must set either `allowedClusterNames` (preferred) +or `allowAnyClusterName: true` to acknowledge that ssm:SendCommand should be +permitted against every EKS cluster in this account. +``` + +This prevents accidental deploys with an unrestricted instance scope. + +### Per-Tool Authorization & Rate Limiting + +Every invocation extracts the caller's Cognito `client_id` and `sub` from the JWT claims forwarded by the AgentCore Gateway. Two checks then run before dispatch: + +1. **Per-tool ACL** — `TOOL_AUTHORIZATION` is a `;`-delimited list of `tool:client_a,client_b` entries. Tools listed get a non-empty allow-set (only those clients may invoke). Tools listed with an empty set are deny-all. Tools not listed remain open to all authenticated callers. +2. **Token-bucket rate limit** — best-effort, per-caller, in a single warm container. Default 60/min. Returns HTTP 429 with `retryAfterSeconds` when exceeded. Set `PER_CALLER_RATE_LIMIT_PER_MINUTE=0` to disable. + +### tcpdump Tools + +`tcpdump_capture` and `tcpdump_analyze` are **not available by default**. They are completely removed from the Lambda's tool routing table — they don't appear in `available_tools` and cannot be invoked. To enable them, set `ENABLED_RESTRICTED_TOOLS=tcpdump_capture,tcpdump_analyze` before deploying. Even when enabled: + +- Each capture requires `confirmCapture=true`. +- tcpdump will not be auto-installed on nodes (the script bails with manual install instructions). +- Pcap presigned URLs use the shorter `PCAP_PRESIGNED_URL_EXPIRATION` window (default 60 s). +- Captures larger than `MAX_PCAP_BYTES` (default 200 MiB) surface a warning in the response. + +### Response Redaction + +`redact_response` runs on every Lambda response before it returns to the gateway: + +- Resource IDs (`sg-…`, `eni-…`, `subnet-…`, `vpc-…`, `vol-…`, `fs-…`) are masked to `-***`. +- Account IDs in ARNs are replaced with `***`. +- AWS access keys (`AKIA…`, `ASIA…`) and JWT-shaped strings are masked. +- IAM/credential error message bodies (`AccessDenied`, `Unauthorized`, `not authorized to perform`, `ExpiredToken`, etc.) are collapsed to ``. +- For network-related tools (`network_diagnostics`, `tcpdump_*`, `cluster_health`, `storage_diagnostics`), RFC1918 + CGNAT private IPs are masked to ``. +- Fields whose key contains `password`, `secret`, `token`, `apikey`, or `credential` are replaced with ``. `volumeHandle`/`volume_handle` is truncated to 24 chars. + +### VPC Endpoints (optional) + +Setting `MCP_VPC_ID` and `MCP_VPC_SUBNET_IDS` attaches the Lambda to your VPC and provisions a gateway endpoint for S3 plus interface endpoints for KMS, SSM, SSM Messages, EC2, CloudWatch Logs, and CloudWatch Metrics. SDK calls and presigned-URL traffic stay on the AWS network instead of the public internet. + +--- + +## Post-Deployment: EKS Node IAM Setup + +### What's Automatic + +If you selected node roles during the interactive deploy flow (or passed them via `EKS_NODE_ROLE_ARNS`), the CDK stack automatically grants: + +- S3 bucket policy: `s3:PutObject`, `s3:GetBucketPolicyStatus`, `s3:GetBucketAcl` on the logs bucket +- KMS key policy: `kms:GenerateDataKey`, `kms:Encrypt`, `kms:Decrypt` on the encryption key (`kms:Decrypt` is required for S3 multipart uploads of files larger than ~8 MiB) + +No manual S3 or KMS setup is needed for those roles. + +If no node roles were provided during deployment, the stack falls back to an account-scoped policy (any principal in the account can upload). This is less restrictive but still functional. + +### What You May Still Need + +The only thing the CDK stack does not attach is the SSM Agent managed policy. EKS-optimized AMIs include SSM Agent by default, but the IAM role needs the policy: + +```bash +# Only needed if not already attached +aws iam attach-role-policy \ + --role-name \ + --policy-arn arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore +``` + +### Adding Node Roles After Deployment + +If you add new EKS clusters later, re-run the deploy script — it will detect the new node roles and update the S3 bucket and KMS key policies automatically. + +Alternatively, pass the new roles directly: + +```bash +EKS_NODE_ROLE_ARNS="arn:aws:iam::123456789012:role/ExistingRole,arn:aws:iam::123456789012:role/NewRole" ./deploy.sh +``` + +### Checklist Per Cluster + +- [ ] Node role was selected during deployment (or added via re-deploy) +- [ ] Node role has `AmazonSSMManagedInstanceCore` managed policy (for SSM Agent) +- [ ] SSM Agent is running on the nodes (default on EKS-optimized AMIs) +- [ ] `AWSSupport-CollectEKSInstanceLogs` SSM document exists in the target region + +--- + +## Configuration in DevOps Agent + +After deployment, the script outputs all values needed for the MCP Server configuration: + +| Setting | Value | +|---------|-------| +| MCP Server URL | `https://.gateway.bedrock-agentcore..amazonaws.com/mcp` | +| OAuth Client ID | Cognito Client ID from output | +| OAuth Client Secret | Cognito Client Secret from output | +| Token URL | `https://-.auth..amazoncognito.com/oauth2/token` | +| Scope | `ssm-automation-gateway-id/gateway:read` | + +Values are also saved to `mcp-config.txt` for reference. + +--- + +## Agent Skills + +A pre-built [Agent Skill](https://docs.aws.amazon.com/devopsagent/latest/userguide/about-aws-devops-agent-devops-agent-skills.html) is included in `skills/` to teach AWS DevOps Agent how to use this MCP server effectively. The skill loads investigation workflows, anti-hallucination guardrails, and all 41 runbook procedures into the agent's context — complementing the runtime `list_sops`/`get_sop` tools. + +| Skill Zip | Size | Contents | +|-----------|------|----------| +| [`eks-node-diagnostics.zip`](skills/eks-node-diagnostics.zip) | 188K | 21-tool workflow, 41 runbooks, VPC CNI anti-hallucination rules, storage guardrails | + +### Uploading the Skill + +1. Navigate to the **Skills** page in your [Agent Space Operator Web App](https://docs.aws.amazon.com/devopsagent/latest/userguide/about-aws-devops-agent-devops-agent-skills.html) +2. Click **Add skill** → **Upload skill** +3. Upload `skills/eks-node-diagnostics.zip` +4. Set Agent Type to **Generic** (all agent types) +5. Click **Upload** + +For more details, see the [AWS DevOps Agent Skills documentation](https://docs.aws.amazon.com/devopsagent/latest/userguide/about-aws-devops-agent-devops-agent-skills.html). + +> **Skills vs SOPs**: The skill loads at investigation start (upfront methodology and gotchas). SOPs are fetched on-demand via `get_sop` (detailed step-by-step procedures). Use both together for best results. + +--- + +## How It Works + +The server gives MCP-compatible agents the ability to collect full diagnostic bundles from EKS worker nodes, pre-index errors with severity classification, stream multi-GB log files without truncation, correlate events across log sources, run live tcpdump captures, compare nodes, and follow structured runbooks — all through 21 MCP tools (19 always-on + 2 restricted tcpdump tools, opt-in) organized in 5 tiers. + +For a detailed walkthrough of the architecture, data flows, tool design, cross-region mechanics, security model, and anti-hallucination design, see: + +**[Architecture & Design →](docs/ARCHITECTURE.md)** + +### MCP Tools (Quick Reference) + +| Tier | Tools | Purpose | +|------|-------|---------| +| 1 — Core | `collect`, `status`, `validate`, `errors`, `read` | Log collection, findings, streaming | +| 2 — Analysis | `search`, `correlate`, `artifact`, `summarize`, `quick_triage`, `history` | Deep investigation, correlation, summaries | +| 3 — Cluster | `cluster_health`, `compare_nodes`, `batch_collect`, `batch_status`, `network_diagnostics`, `storage_diagnostics` | Multi-node operations | +| 4 — Capture | `tcpdump_capture`, `tcpdump_analyze` | Live packet capture (**disabled by default**) | +| 5 — SOPs | `list_sops`, `get_sop` | 41 structured runbooks | + +> **Note:** Tier 4 tools are removed from the routing table by default. They don't appear in `available_tools` and cannot be invoked unless `ENABLED_RESTRICTED_TOOLS` includes them. See [Security Model](#security-model). + +### Agent Workflow + +``` +collect → status (poll) → validate → errors → search → correlate → read → summarize +``` + +### Runbook Library (41 SOPs) + +| Category | Coverage | +|----------|----------| +| A — Node Lifecycle | OOM/NotReady, certificates, bootstrap, clock skew, join failures | +| B — Kubelet | Config errors, eviction, PLEG | +| C — Container Runtime | Image pull, sandbox creation, OverlayFS/inode | +| D — Networking | VPC CNI, kube-proxy, conntrack, MTU, DNS, ENA, pod-to-pod | +| E — Storage | EBS CSI, EFS mount | +| F — Scheduling | CPU/memory, max pods, taints/tolerations | +| G — Resource Pressure | Disk pressure, OOMKill, PID pressure | +| H — IAM/Security | Node role, IRSA/Pod Identity, IMDS | +| I — Upgrades | Version skew | +| J — Infrastructure | ENA/instance limits, EBS transient, AZ outage | +| K — Workload Issues | Stuck terminating pods, probe failures, CrashLoopBackOff, containerd failures, CSI plugin | +| Z — Catch-All | General troubleshooting | + +--- + +## Usage Examples + +### Basic Investigation +``` +Node i-0abc123def in us-west-2 went NotReady around 3am. Collect its logs +and correlate what happened in the 5 minutes before it went down. +``` + +### Cluster-Wide Triage +``` +We have a 200-node cluster and something is off. Do a dry run batch collection +first — show me which nodes you'd sample. Then collect from the unhealthy ones. +``` + +### Live Packet Capture + +> **Requires:** `ENABLED_RESTRICTED_TOOLS=tcpdump_capture,tcpdump_analyze` set at deploy time. tcpdump must be pre-installed on the node AMI. Each capture requires `confirmCapture=true`. + +``` +Pods on node i-0abc123def can't reach the API server. Run a 2-minute tcpdump +filtered on port 443, then analyze — show me RST counts and retransmissions. +``` + +### Pod-Level Capture + +> **Requires:** Same as above, plus `crictl` on the node (default on EKS-optimized AMIs). + +``` +DNS lookups are timing out. CoreDNS pod coredns-5d78c9869d-abc12 is on node +i-0abc123def in kube-system. Capture UDP port 53 from inside the pod for 60s. +``` + +### SOP-Guided +``` +I don't know what's wrong — just investigate. List the available SOPs, run a +general triage, and follow whichever runbook matches. +``` + +--- + +## CloudFormation Outputs + +| Output | Description | +|--------|-------------| +| `GatewayId` | AgentCore Gateway ID | +| `GatewayUrl` | MCP Server URL | +| `CognitoUserPoolId` | Cognito User Pool ID | +| `CognitoClientId` | OAuth Client ID | +| `OAuthExchangeUrl` | OAuth Token URL | +| `OAuthScope` | OAuth Scope | +| `LogsBucketName` | S3 bucket for logs | +| `SOPBucketName` | S3 bucket for runbooks | +| `SSMAutomationRoleArn` | SSM Automation role ARN | +| `EncryptionKeyArn` | KMS key ARN | + +--- + +## Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| `cdk synth` fails with "must set either `allowedClusterNames` …" | Cluster scope wasn't chosen | Set `ALLOWED_CLUSTER_NAMES=…` (preferred) or `ALLOW_ANY_CLUSTER_NAME=true` and re-run `./deploy.sh` | +| Tool returns 403 "Tool '…' is restricted and not enabled" | Caller hit `tcpdump_capture` / `tcpdump_analyze` without opt-in | Set `ENABLED_RESTRICTED_TOOLS=tcpdump_capture,tcpdump_analyze` and redeploy | +| Tool returns 403 "Caller is not permitted to invoke '…'" | Per-tool ACL doesn't include this client | Add the client to the matching `TOOL_AUTHORIZATION` entry | +| Tool returns 429 "Rate limit exceeded" | Caller exceeded `PER_CALLER_RATE_LIMIT_PER_MINUTE` | Wait the `retryAfterSeconds` in the response, or raise the limit | +| `collect` returns "document not found" | SSM document not in target region | Use a supported region or pass `region` explicitly | +| `collect` fails at `CheckS3BucketPublicStatus` | SSM automation role missing `s3:GetBucketPublicAccessBlock` / `s3:GetAccountPublicAccessBlock` | Already granted by the current construct — redeploy if your stack predates the fix | +| Upload step fails | Node role missing S3/KMS permissions | Pass the node role via `EKS_NODE_ROLE_ARNS` and redeploy | +| `status` returns wrong region | Region metadata not persisted | Pass `region` explicitly | +| Auto-detection times out | Instance in uncommon region | Add the region to `ALLOWED_REGIONS` and pass `region` explicitly | +| `errors` returns empty | Findings indexer hasn't run yet | Wait a few seconds after `validate`, or use `search` | +| Response missing IDs that should be there (e.g. `sg-…`) | Redaction layer is masking them | Expected — `redact_response` masks SG/ENI/subnet/VPC IDs and account IDs by design | + +--- + +## Cleanup + +```bash +cdk destroy +``` + +> The logs and SOP buckets are configured with `removalPolicy: DESTROY` and `autoDeleteObjects: true`, so `cdk destroy` will delete the buckets and all their contents. Download anything you need from `eksnodelogmcpstack-logs-` first. + +--- + +## License + +This project is licensed under the MIT No Attribution (MIT-0) License. See the [LICENSE](LICENSE) file. diff --git a/mcp/aws-eks-node-diagnostics-mcp/bin/app.ts b/mcp/aws-eks-node-diagnostics-mcp/bin/app.ts new file mode 100644 index 0000000..4877f37 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/bin/app.ts @@ -0,0 +1,90 @@ +#!/usr/bin/env node +import 'source-map-support/register'; +import * as cdk from 'aws-cdk-lib'; +import { EksNodeLogMcpStack } from '../src/ssm-automation-gateway-stack-v2'; + +const app = new cdk.App(); + +new EksNodeLogMcpStack(app, 'EksNodeLogMcpStack', { + description: 'EKS Node Log MCP Server - Collect and analyze diagnostic logs from EKS worker nodes', + env: { + account: process.env.CDK_DEFAULT_ACCOUNT, + region: process.env.CDK_DEFAULT_REGION, + }, + gatewayName: 'EksNodeLogMcpGW', + enableEncryption: true, + logRetentionDays: 1, + ssmDefaultHostRoleArn: process.env.SSM_DEFAULT_HOST_ROLE_ARN, + eksNodeRoleArns: process.env.EKS_NODE_ROLE_ARNS + ? process.env.EKS_NODE_ROLE_ARNS.split(',').filter(Boolean) + : undefined, + + // ── Security scoping ── + // Regions: restricts IAM resource ARNs + Lambda auto-detection to these regions only. + // Default: [stack region]. Set via env var or hardcode for customer deployments. + allowedRegions: process.env.ALLOWED_REGIONS + ? process.env.ALLOWED_REGIONS.split(',').filter(Boolean) + : undefined, // defaults to [stack region] + + // Cluster names: restricts ssm:SendCommand to instances tagged with these exact + // eks:cluster-name values. Prevents targeting instances in other clusters. + // SECURITY: production deploys SHOULD set this. To deploy without it (any + // EKS cluster in the account), set ALLOW_ANY_CLUSTER_NAME=true to make the + // wildcard scope explicit. + allowedClusterNames: process.env.ALLOWED_CLUSTER_NAMES + ? process.env.ALLOWED_CLUSTER_NAMES.split(',').filter(Boolean) + : undefined, + allowAnyClusterName: process.env.ALLOW_ANY_CLUSTER_NAME === 'true', + + // SSM documents: restricts which documents can be executed via SendCommand. + // Default: ['AWS-RunShellScript'] only. + allowedSsmDocuments: process.env.ALLOWED_SSM_DOCUMENTS + ? process.env.ALLOWED_SSM_DOCUMENTS.split(',').filter(Boolean) + : undefined, // defaults to ['AWS-RunShellScript'] + + // Presigned URL expiry: max 900s (15 min). Lower = less exposure if URL is intercepted. + presignedUrlExpirationSeconds: process.env.PRESIGNED_URL_EXPIRATION + ? parseInt(process.env.PRESIGNED_URL_EXPIRATION, 10) + : 300, + + // Tighter expiry for raw network captures from tcpdump_*. + pcapPresignedUrlExpirationSeconds: process.env.PCAP_PRESIGNED_URL_EXPIRATION + ? parseInt(process.env.PCAP_PRESIGNED_URL_EXPIRATION, 10) + : 60, + + // Optional VPC + interface endpoints. When set, Lambda runs inside the VPC + // and S3/KMS/SSM/EC2/EKS/Logs traffic stays on private AWS network. + vpcId: process.env.MCP_VPC_ID || undefined, + vpcSubnetIds: process.env.MCP_VPC_SUBNET_IDS + ? process.env.MCP_VPC_SUBNET_IDS.split(',').filter(Boolean) + : undefined, + + // Per-tool authorization map. + // Format: TOOL_AUTHORIZATION="collect:client-a,client-b;tcpdump_capture:client-emergency" + toolAuthorization: process.env.TOOL_AUTHORIZATION + ? Object.fromEntries( + process.env.TOOL_AUTHORIZATION.split(';') + .filter(Boolean) + .map(entry => { + const [tool, clients] = entry.split(':', 2); + return [tool.trim(), (clients ?? '').split(',').map(s => s.trim()).filter(Boolean)]; + }), + ) + : undefined, + + perCallerRateLimitPerMinute: process.env.PER_CALLER_RATE_LIMIT_PER_MINUTE + ? parseInt(process.env.PER_CALLER_RATE_LIMIT_PER_MINUTE, 10) + : 60, + + maxPcapBytes: process.env.MAX_PCAP_BYTES + ? parseInt(process.env.MAX_PCAP_BYTES, 10) + : 209715200, + + // Restricted tools: tcpdump_capture and tcpdump_analyze are removed from the + // tool routing table by default. They do not appear in available_tools and + // cannot be invoked. Only enable if the customer has explicitly approved + // network capture capabilities on their nodes. + enableRestrictedTools: process.env.ENABLED_RESTRICTED_TOOLS + ? process.env.ENABLED_RESTRICTED_TOOLS.split(',').filter(Boolean) + : undefined, // defaults to [] (tcpdump not available) +}); diff --git a/mcp/aws-eks-node-diagnostics-mcp/cdk.json b/mcp/aws-eks-node-diagnostics-mcp/cdk.json new file mode 100644 index 0000000..f6faf96 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/cdk.json @@ -0,0 +1,23 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/app.ts", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "yarn.lock", + "node_modules", + "test" + ] + }, + "context": { + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": ["aws", "aws-cn"], + "@aws-cdk/customresources:installLatestAwsSdkDefault": false + } +} diff --git a/mcp/aws-eks-node-diagnostics-mcp/deploy.sh b/mcp/aws-eks-node-diagnostics-mcp/deploy.sh new file mode 100755 index 0000000..0747e6f --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/deploy.sh @@ -0,0 +1,529 @@ +#!/bin/bash +set -e + +# EKS Node Log MCP - Deploy and Configure Script +# This script deploys the CDK stack and outputs all values needed for DevOps Agent configuration + +STACK_NAME="${1:-EksNodeLogMcpStack}" +REGION="${AWS_REGION:-us-east-1}" + +# Optional: pass EKS node role ARNs directly (comma-separated) +# Usage: ./deploy.sh EksNodeLogMcpStack arn:aws:iam::123456789012:role/MyNodeRole +# Or: EKS_NODE_ROLE_ARNS=arn:aws:iam::123456789012:role/MyNodeRole ./deploy.sh +if [ -n "$2" ]; then + export EKS_NODE_ROLE_ARNS="$2" +fi + +echo "==============================================" +echo "EKS Node Log MCP - Deployment Script" +echo "==============================================" +echo "Stack Name: $STACK_NAME" +echo "Region: $REGION" +echo "" + +# Check prerequisites +command -v npm >/dev/null 2>&1 || { echo "Error: npm is required but not installed."; exit 1; } +command -v aws >/dev/null 2>&1 || { echo "Error: AWS CLI is required but not installed."; exit 1; } +command -v python3 >/dev/null 2>&1 || { echo "Error: python3 is required but not installed."; exit 1; } + +# Install dependencies +echo "Installing dependencies..." +npm install --silent + +# Build TypeScript +echo "Building TypeScript..." +npm run build + +# Bootstrap CDK (if needed) +echo "Bootstrapping CDK (if needed)..." +npx cdk bootstrap --quiet 2>/dev/null || true + +# ======================================================================== +# DETECT / CREATE SSM DEFAULT HOST MANAGEMENT ROLE +# ======================================================================== +echo "" +echo "Setting up SSM Default Host Management role..." + +SSM_ROLE_NAME="AWSSystemsManagerDefaultEC2InstanceManagementRole" +EPOXY_ROLE_NAME="EpoxyAWSSystemsManagerDefaultEC2InstanceManagementRole" + +# Check for existing role (standard or Epoxy-prefixed) +SSM_DEFAULT_HOST_ROLE_ARN=$(aws iam get-role \ + --role-name "$SSM_ROLE_NAME" \ + --query 'Role.Arn' --output text 2>/dev/null || true) + +if [ -z "$SSM_DEFAULT_HOST_ROLE_ARN" ] || [ "$SSM_DEFAULT_HOST_ROLE_ARN" = "None" ]; then + SSM_DEFAULT_HOST_ROLE_ARN=$(aws iam get-role \ + --role-name "$EPOXY_ROLE_NAME" \ + --query 'Role.Arn' --output text 2>/dev/null || true) +fi + +if [ -z "$SSM_DEFAULT_HOST_ROLE_ARN" ] || [ "$SSM_DEFAULT_HOST_ROLE_ARN" = "None" ]; then + echo "SSM Default Host Management role not found. Creating $SSM_ROLE_NAME..." + + # Create the trust policy + TRUST_POLICY=$(cat <<'TRUST' +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { "Service": "ssm.amazonaws.com" }, + "Action": "sts:AssumeRole" + } + ] +} +TRUST +) + + aws iam create-role \ + --role-name "$SSM_ROLE_NAME" \ + --assume-role-policy-document "$TRUST_POLICY" \ + --description "Default EC2 instance management role for SSM" \ + --region "$REGION" >/dev/null + + aws iam attach-role-policy \ + --role-name "$SSM_ROLE_NAME" \ + --policy-arn "arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore" + + SSM_DEFAULT_HOST_ROLE_ARN=$(aws iam get-role \ + --role-name "$SSM_ROLE_NAME" \ + --query 'Role.Arn' --output text) + + echo "Created role: $SSM_DEFAULT_HOST_ROLE_ARN" + echo "Waiting 10s for IAM propagation..." + sleep 10 +else + echo "Found existing role: $SSM_DEFAULT_HOST_ROLE_ARN" +fi + +export SSM_DEFAULT_HOST_ROLE_ARN + +# ======================================================================== +# AUTO-DETECT EKS NODE ROLE ARNS (interactive region & cluster selection) +# ======================================================================== +echo "" +if [ -n "$EKS_NODE_ROLE_ARNS" ]; then + echo "Using provided EKS node role ARNs: $EKS_NODE_ROLE_ARNS" +else + # --- Step 1: Region selection --- + echo "Which AWS regions should be scanned for EKS clusters?" + echo "" + echo " 1) All enabled regions" + echo " 2) Current deploy region only ($REGION)" + echo " 3) Enter a specific region" + echo "" + read -rp "Select [1/2/3] (default: 1): " REGION_CHOICE + REGION_CHOICE="${REGION_CHOICE:-1}" + + case "$REGION_CHOICE" in + 1) + echo "" + echo "Fetching all enabled regions..." + SCAN_REGIONS=$(aws ec2 describe-regions --query 'Regions[].RegionName' --output text 2>/dev/null || echo "$REGION") + ;; + 2) + SCAN_REGIONS="$REGION" + ;; + 3) + read -rp "Enter region (e.g. us-west-2): " CUSTOM_REGION + if [ -z "$CUSTOM_REGION" ]; then + echo "No region entered, falling back to $REGION" + CUSTOM_REGION="$REGION" + fi + SCAN_REGIONS="$CUSTOM_REGION" + ;; + *) + echo "Invalid choice, falling back to all regions." + SCAN_REGIONS=$(aws ec2 describe-regions --query 'Regions[].RegionName' --output text 2>/dev/null || echo "$REGION") + ;; + esac + + # --- Step 2: Discover clusters across selected regions --- + echo "" + echo "Scanning for EKS clusters..." + + # Arrays to hold discovered cluster info + CLUSTER_LIST=() # "region/cluster-name" + CLUSTER_DISPLAY=() # for numbered display + + for SCAN_REGION in $SCAN_REGIONS; do + CLUSTERS=$(aws eks list-clusters --region "$SCAN_REGION" --query 'clusters[*]' --output text 2>/dev/null || true) + if [ -n "$CLUSTERS" ]; then + for CLUSTER in $CLUSTERS; do + CLUSTER_LIST+=("${SCAN_REGION}/${CLUSTER}") + done + fi + done + + if [ ${#CLUSTER_LIST[@]} -eq 0 ]; then + echo "WARNING: No EKS clusters found in the selected region(s)." + echo "" + read -rp "Would you like to manually enter node role ARN(s)? [y/N]: " MANUAL_ENTRY + if [ "$MANUAL_ENTRY" = "y" ] || [ "$MANUAL_ENTRY" = "Y" ]; then + echo "Enter comma-separated role ARNs (e.g. arn:aws:iam::123456789012:role/MyNodeRole):" + read -rp "> " MANUAL_ARNS + MANUAL_ARNS=$(echo "$MANUAL_ARNS" | tr -d ' ') + if [ -n "$MANUAL_ARNS" ]; then + EKS_NODE_ROLE_ARNS="$MANUAL_ARNS" + echo "Using manually provided roles: $EKS_NODE_ROLE_ARNS" + export EKS_NODE_ROLE_ARNS + else + echo "No ARNs entered. The S3 bucket policy will use an account-scoped fallback." + fi + else + echo "Skipping. The S3 bucket policy will use an account-scoped fallback (less restrictive)." + fi + else + # --- Step 3: Display clusters and let user choose --- + echo "" + echo "Found ${#CLUSTER_LIST[@]} EKS cluster(s):" + echo "" + IDX=1 + for ENTRY in "${CLUSTER_LIST[@]}"; do + C_REGION="${ENTRY%%/*}" + C_NAME="${ENTRY#*/}" + echo " ${IDX}) ${C_NAME} (${C_REGION})" + IDX=$((IDX + 1)) + done + echo "" + echo " a) All clusters" + echo "" + read -rp "Select clusters (comma-separated numbers, or 'a' for all) [default: a]: " CLUSTER_CHOICE + CLUSTER_CHOICE="${CLUSTER_CHOICE:-a}" + + SELECTED_CLUSTERS=() + if [ "$CLUSTER_CHOICE" = "a" ] || [ "$CLUSTER_CHOICE" = "A" ]; then + SELECTED_CLUSTERS=("${CLUSTER_LIST[@]}") + else + IFS=',' read -ra PICKS <<< "$CLUSTER_CHOICE" + for PICK in "${PICKS[@]}"; do + PICK=$(echo "$PICK" | tr -d ' ') + if [[ "$PICK" =~ ^[0-9]+$ ]] && [ "$PICK" -ge 1 ] && [ "$PICK" -le ${#CLUSTER_LIST[@]} ]; then + SELECTED_CLUSTERS+=("${CLUSTER_LIST[$((PICK - 1))]}") + else + echo " Skipping invalid selection: $PICK" + fi + done + fi + + if [ ${#SELECTED_CLUSTERS[@]} -eq 0 ]; then + echo "No valid clusters selected. Skipping node role detection." + else + echo "" + echo "Detecting node roles for ${#SELECTED_CLUSTERS[@]} cluster(s)..." + + # --- Step 4: Collect all unique node role ARNs from selected clusters --- + ALL_ROLE_ARNS=() + ROLE_SOURCES=() # "role-arn -> cluster (region)" + + for ENTRY in "${SELECTED_CLUSTERS[@]}"; do + C_REGION="${ENTRY%%/*}" + C_NAME="${ENTRY#*/}" + + NODEGROUPS=$(aws eks list-nodegroups --cluster-name "$C_NAME" --region "$C_REGION" \ + --query 'nodegroups[*]' --output text 2>/dev/null || true) + for NG in $NODEGROUPS; do + ROLE_ARN=$(aws eks describe-nodegroup --cluster-name "$C_NAME" --nodegroup-name "$NG" \ + --region "$C_REGION" --query 'nodegroup.nodeRole' --output text 2>/dev/null || true) + if [ -n "$ROLE_ARN" ] && [ "$ROLE_ARN" != "None" ]; then + # Deduplicate + ALREADY_ADDED=false + for EXISTING in "${ALL_ROLE_ARNS[@]}"; do + if [ "$EXISTING" = "$ROLE_ARN" ]; then + ALREADY_ADDED=true + break + fi + done + if [ "$ALREADY_ADDED" = false ]; then + ALL_ROLE_ARNS+=("$ROLE_ARN") + ROLE_NAME="${ROLE_ARN##*/}" + ROLE_SOURCES+=("${ROLE_NAME} (${C_NAME} / ${C_REGION})") + fi + fi + done + done + + if [ ${#ALL_ROLE_ARNS[@]} -eq 0 ]; then + echo "WARNING: Selected clusters have no managed node groups with detectable roles." + echo "" + read -rp "Would you like to manually enter node role ARN(s)? [y/N]: " MANUAL_ENTRY + if [ "$MANUAL_ENTRY" = "y" ] || [ "$MANUAL_ENTRY" = "Y" ]; then + echo "Enter comma-separated role ARNs (e.g. arn:aws:iam::123456789012:role/MyNodeRole):" + read -rp "> " MANUAL_ARNS + MANUAL_ARNS=$(echo "$MANUAL_ARNS" | tr -d ' ') + if [ -n "$MANUAL_ARNS" ]; then + EKS_NODE_ROLE_ARNS="$MANUAL_ARNS" + echo "Using manually provided roles: $EKS_NODE_ROLE_ARNS" + export EKS_NODE_ROLE_ARNS + else + echo "No ARNs entered. The S3 bucket policy will use an account-scoped fallback." + fi + else + echo "Skipping. The S3 bucket policy will use an account-scoped fallback (less restrictive)." + fi + else + # --- Step 5: Let user choose which node roles to include --- + echo "" + echo "Found ${#ALL_ROLE_ARNS[@]} unique node role(s):" + echo "" + IDX=1 + for i in "${!ALL_ROLE_ARNS[@]}"; do + echo " ${IDX}) ${ALL_ROLE_ARNS[$i]}" + echo " └─ ${ROLE_SOURCES[$i]}" + IDX=$((IDX + 1)) + done + echo "" + echo " a) All roles" + echo "" + read -rp "Select node roles (comma-separated numbers, or 'a' for all) [default: a]: " ROLE_CHOICE + ROLE_CHOICE="${ROLE_CHOICE:-a}" + + SELECTED_ROLES=() + if [ "$ROLE_CHOICE" = "a" ] || [ "$ROLE_CHOICE" = "A" ]; then + SELECTED_ROLES=("${ALL_ROLE_ARNS[@]}") + else + IFS=',' read -ra PICKS <<< "$ROLE_CHOICE" + for PICK in "${PICKS[@]}"; do + PICK=$(echo "$PICK" | tr -d ' ') + if [[ "$PICK" =~ ^[0-9]+$ ]] && [ "$PICK" -ge 1 ] && [ "$PICK" -le ${#ALL_ROLE_ARNS[@]} ]; then + SELECTED_ROLES+=("${ALL_ROLE_ARNS[$((PICK - 1))]}") + else + echo " Skipping invalid selection: $PICK" + fi + done + fi + + # Build the comma-separated string + EKS_NODE_ROLE_ARNS="" + for ROLE in "${SELECTED_ROLES[@]}"; do + if [ -z "$EKS_NODE_ROLE_ARNS" ]; then + EKS_NODE_ROLE_ARNS="$ROLE" + else + EKS_NODE_ROLE_ARNS="$EKS_NODE_ROLE_ARNS,$ROLE" + fi + done + + if [ -n "$EKS_NODE_ROLE_ARNS" ]; then + echo "" + echo "Using EKS node roles: $EKS_NODE_ROLE_ARNS" + export EKS_NODE_ROLE_ARNS + else + echo "No roles selected." + read -rp "Would you like to manually enter node role ARN(s) instead? [y/N]: " MANUAL_ENTRY + if [ "$MANUAL_ENTRY" = "y" ] || [ "$MANUAL_ENTRY" = "Y" ]; then + echo "Enter comma-separated role ARNs (e.g. arn:aws:iam::123456789012:role/MyNodeRole):" + read -rp "> " MANUAL_ARNS + MANUAL_ARNS=$(echo "$MANUAL_ARNS" | tr -d ' ') + if [ -n "$MANUAL_ARNS" ]; then + EKS_NODE_ROLE_ARNS="$MANUAL_ARNS" + echo "Using manually provided roles: $EKS_NODE_ROLE_ARNS" + export EKS_NODE_ROLE_ARNS + else + echo "No ARNs entered. The S3 bucket policy will use an account-scoped fallback." + fi + else + echo "The S3 bucket policy will use an account-scoped fallback." + fi + fi + fi + fi + fi +fi + +# Deploy the stack +echo "" +echo "==============================================" +echo "Security Scoping Configuration" +echo "==============================================" +echo "" + +# --- ALLOWED_REGIONS: derive from selected clusters --- +if [ -z "$ALLOWED_REGIONS" ]; then + if [ "${#SELECTED_CLUSTERS[@]:-0}" -gt 0 ] 2>/dev/null; then + # Extract unique regions from selected clusters (format: "region/cluster-name") + DETECTED_REGIONS=() + for ENTRY in "${SELECTED_CLUSTERS[@]}"; do + C_REGION="${ENTRY%%/*}" + ALREADY=false + for R in "${DETECTED_REGIONS[@]}"; do + if [ "$R" = "$C_REGION" ]; then ALREADY=true; break; fi + done + if [ "$ALREADY" = false ]; then + DETECTED_REGIONS+=("$C_REGION") + fi + done + ALLOWED_REGIONS=$(IFS=','; echo "${DETECTED_REGIONS[*]}") + echo "Allowed regions (from selected clusters): $ALLOWED_REGIONS" + else + ALLOWED_REGIONS="$REGION" + echo "Allowed regions (deploy region only): $ALLOWED_REGIONS" + fi + export ALLOWED_REGIONS +fi + +# --- ALLOWED_CLUSTER_NAMES: derive from selected clusters --- +if [ -z "$ALLOWED_CLUSTER_NAMES" ]; then + if [ "${#SELECTED_CLUSTERS[@]:-0}" -gt 0 ] 2>/dev/null; then + DETECTED_CLUSTERS=() + for ENTRY in "${SELECTED_CLUSTERS[@]}"; do + C_NAME="${ENTRY#*/}" + DETECTED_CLUSTERS+=("$C_NAME") + done + ALLOWED_CLUSTER_NAMES=$(IFS=','; echo "${DETECTED_CLUSTERS[*]}") + echo "Allowed cluster names: $ALLOWED_CLUSTER_NAMES" + else + echo "Allowed cluster names: (any EKS cluster — no clusters were selected)" + fi + export ALLOWED_CLUSTER_NAMES +fi + +echo "" +echo "Deploying CDK stack..." +npx cdk deploy "$STACK_NAME" --require-approval never --outputs-file cdk-outputs.json + +echo "" +echo "==============================================" +echo "Deployment Complete! Retrieving configuration..." +echo "==============================================" + +# Read from cdk-outputs.json using python3 for reliable JSON parsing +if [ ! -f cdk-outputs.json ]; then + echo "Error: cdk-outputs.json not found" + exit 1 +fi + +# Parse values from cdk-outputs.json +GATEWAY_URL=$(python3 -c "import json; d=json.load(open('cdk-outputs.json')); print([v for k,v in d.get('$STACK_NAME',{}).items() if 'GatewayUrl' in k][0])" 2>/dev/null || echo "NOT_FOUND") +CLIENT_ID=$(python3 -c "import json; d=json.load(open('cdk-outputs.json')); print([v for k,v in d.get('$STACK_NAME',{}).items() if 'CognitoClientId' in k][0])" 2>/dev/null || echo "NOT_FOUND") +USER_POOL_ID=$(python3 -c "import json; d=json.load(open('cdk-outputs.json')); print([v for k,v in d.get('$STACK_NAME',{}).items() if 'CognitoUserPoolId' in k][0])" 2>/dev/null || echo "NOT_FOUND") +TOKEN_URL=$(python3 -c "import json; d=json.load(open('cdk-outputs.json')); print([v for k,v in d.get('$STACK_NAME',{}).items() if 'OAuthExchangeUrl' in k][0])" 2>/dev/null || echo "NOT_FOUND") +OAUTH_SCOPE=$(python3 -c "import json; d=json.load(open('cdk-outputs.json')); print([v for k,v in d.get('$STACK_NAME',{}).items() if 'OAuthScope' in k][0])" 2>/dev/null || echo "NOT_FOUND") +LOGS_BUCKET=$(python3 -c "import json; d=json.load(open('cdk-outputs.json')); print([v for k,v in d.get('$STACK_NAME',{}).items() if 'LogsBucketName' in k][0])" 2>/dev/null || echo "NOT_FOUND") + +# Get Cognito Client Secret +echo "Retrieving Cognito Client Secret..." +if [ "$USER_POOL_ID" != "NOT_FOUND" ] && [ "$CLIENT_ID" != "NOT_FOUND" ]; then + CLIENT_SECRET=$(aws cognito-idp describe-user-pool-client \ + --user-pool-id "$USER_POOL_ID" \ + --client-id "$CLIENT_ID" \ + --region "$REGION" \ + --query "UserPoolClient.ClientSecret" \ + --output text 2>/dev/null || echo "NOT_FOUND") +else + CLIENT_SECRET="NOT_FOUND" +fi + +echo "" +echo "==============================================" +echo "DEVOPS AGENT MCP SERVER CONFIGURATION" +echo "==============================================" +echo "" +echo "Copy these values to configure the MCP Server in DevOps Agent Console:" +echo "" +echo "┌─────────────────────────────────────────────────────────────────────┐" +echo "│ MCP Server URL: │" +echo "│ $GATEWAY_URL" +echo "├─────────────────────────────────────────────────────────────────────┤" +echo "│ OAuth Client ID: │" +echo "│ $CLIENT_ID" +echo "├─────────────────────────────────────────────────────────────────────┤" +echo "│ OAuth Client Secret: │" +echo "│ $CLIENT_SECRET" +echo "├─────────────────────────────────────────────────────────────────────┤" +echo "│ Token URL: │" +echo "│ $TOKEN_URL" +echo "├─────────────────────────────────────────────────────────────────────┤" +echo "│ Scope (use only ONE): │" +echo "│ $OAUTH_SCOPE" +echo "└─────────────────────────────────────────────────────────────────────┘" +echo "" +echo "Additional Info:" +echo " Logs Bucket: $LOGS_BUCKET" +echo " Region: $REGION" +echo "" + +# Save configuration to file +CONFIG_FILE="mcp-config.txt" +cat > "$CONFIG_FILE" << EOF +# EKS Node Log MCP - DevOps Agent Configuration +# Generated: $(date) +# Stack: $STACK_NAME +# Region: $REGION + +MCP_SERVER_URL=$GATEWAY_URL +OAUTH_CLIENT_ID=$CLIENT_ID +OAUTH_CLIENT_SECRET=$CLIENT_SECRET +TOKEN_URL=$TOKEN_URL +OAUTH_SCOPE=$OAUTH_SCOPE +LOGS_BUCKET=$LOGS_BUCKET + +# Security Scoping +ALLOWED_REGIONS=${ALLOWED_REGIONS:-$REGION} +ALLOWED_CLUSTER_NAMES=${ALLOWED_CLUSTER_NAMES:-(any EKS cluster)} +ALLOWED_SSM_DOCUMENTS=${ALLOWED_SSM_DOCUMENTS:-AWS-RunShellScript} +PRESIGNED_URL_EXPIRATION=${PRESIGNED_URL_EXPIRATION:-300} +ENABLED_RESTRICTED_TOOLS=${ENABLED_RESTRICTED_TOOLS:-(none — tcpdump disabled)} +EOF + +echo "Configuration saved to: $CONFIG_FILE" +echo "" +echo "==============================================" +echo "SECURITY SCOPING" +echo "==============================================" +echo "" +echo " Allowed Regions: ${ALLOWED_REGIONS:-$REGION}" +echo " Allowed Clusters: ${ALLOWED_CLUSTER_NAMES:-(any EKS cluster)}" +echo " Allowed SSM Documents: ${ALLOWED_SSM_DOCUMENTS:-AWS-RunShellScript}" +echo " Presigned URL Expiry: ${PRESIGNED_URL_EXPIRATION:-300}s (max 900s)" +echo " Restricted Tools: ${ENABLED_RESTRICTED_TOOLS:-(none — tcpdump not available)}" +echo "" +echo " IAM enforces: ssm:SendCommand only on instances tagged with the" +echo " selected cluster names, in the allowed regions, using the allowed" +echo " SSM documents. tcpdump tools are removed from the routing table" +echo " unless explicitly enabled via ENABLED_RESTRICTED_TOOLS." +echo "" +echo "==============================================" +echo "AVAILABLE MCP TOOLS (20)" +echo "==============================================" +echo "" +echo "TIER 1 — CORE OPERATIONS" +echo " collect Start log collection with idempotency" +echo " status Get collection status with progress tracking" +echo " validate Verify all expected files were extracted" +echo " errors Get pre-indexed error findings (fast path)" +echo " read Byte-range streaming for multi-GB files" +echo "" +echo "TIER 2 — ADVANCED ANALYSIS" +echo " search Full-text regex search across all logs" +echo " correlate Cross-file timeline correlation" +echo " artifact Secure presigned URLs for large artifacts" +echo " summarize AI-ready structured incident summary" +echo " quick_triage Rapid node health assessment" +echo " history Audit trail of past collections" +echo "" +echo "TIER 3 — CLUSTER INTELLIGENCE" +echo " cluster_health Cluster-wide node health overview" +echo " compare_nodes Compare healthy vs unhealthy nodes" +echo " batch_collect Batch log collection with sampling" +echo " batch_status Batch collection status" +echo " network_diagnostics Network stack analysis (CNI, iptables, DNS)" +echo " storage_diagnostics Storage/volume/CSI analysis" +echo "" +echo "TIER 4 — LIVE CAPTURE (disabled by default)" +echo " tcpdump_capture Live packet capture via SSM" +echo " tcpdump_analyze Analyze captured pcap files" +echo " NOTE: These tools are removed from the routing table unless" +echo " ENABLED_RESTRICTED_TOOLS includes them." +echo "" +echo "TIER 5 — SOPs" +echo " list_sops List available runbooks" +echo " get_sop Retrieve a specific runbook" +echo "" +echo "==============================================" +echo "EXAMPLE PROMPT FOR DEVOPS AGENT" +echo "==============================================" +echo "" +echo "\"I'm investigating a node issue on i-0123456789abcdef0." +echo " Collect logs, find any critical errors, and give me a summary.\"" +echo "" diff --git a/mcp/aws-eks-node-diagnostics-mcp/docs/ARCHITECTURE.md b/mcp/aws-eks-node-diagnostics-mcp/docs/ARCHITECTURE.md new file mode 100644 index 0000000..6f696a2 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/docs/ARCHITECTURE.md @@ -0,0 +1,449 @@ +# EKS Node Log MCP — Architecture & Design + +This document explains the internal design of the EKS Node Log MCP server: how the components fit together, how data flows from an EKS worker node to an AI agent's context window, and the design decisions behind each layer. + +## Table of Contents + +- [System Overview](#system-overview) +- [Component Deep Dive](#component-deep-dive) + - [MCP Gateway (Bedrock AgentCore)](#mcp-gateway-bedrock-agentcore) + - [Lambda Function (Tool Router)](#lambda-function-tool-router) + - [SSM Automation](#ssm-automation) + - [S3 Log Storage](#s3-log-storage) + - [Findings Indexer](#findings-indexer) + - [Unzip Lambda](#unzip-lambda) + - [Cognito (OAuth2)](#cognito-oauth2) + - [KMS Encryption](#kms-encryption) +- [Data Flow](#data-flow) + - [Log Collection Flow](#log-collection-flow) + - [Analysis Flow](#analysis-flow) + - [Live Packet Capture Flow](#live-packet-capture-flow) +- [Cross-Region Design](#cross-region-design) +- [Tool Architecture](#tool-architecture) + - [Tier 1 — Core Operations](#tier-1--core-operations) + - [Tier 2 — Advanced Analysis](#tier-2--advanced-analysis) + - [Tier 3 — Cluster-Level Intelligence](#tier-3--cluster-level-intelligence) + - [Tier 4 — Live Packet Capture](#tier-4--live-packet-capture) + - [Tier 5 — SOP Management](#tier-5--sop-management) +- [Time-Bounded Analysis](#time-bounded-analysis) +- [Anti-Hallucination Design](#anti-hallucination-design) +- [SOP Runbook System](#sop-runbook-system) +- [Security Model](#security-model) +- [CDK Construct Design](#cdk-construct-design) +- [Deploy Script Design](#deploy-script-design) + +--- + +## System Overview + +The server bridges the gap between AI agents (DevOps Agent or any MCP-compatible client) and the OS-level diagnostic data on EKS worker nodes. The Kubernetes API and CloudWatch don't expose iptables rules, CNI config, route tables, dmesg, IPAMD logs, or conntrack state — but these are exactly what's needed to diagnose ~40-50% of EKS production issues. + +``` +┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐ +│ MCP Client │────▶│ MCP Gateway │────▶│ Lambda │ +│ (DevOps Agent) │◀────│ (AgentCore) │◀────│ (19 tools) │ +└─────────────────┘ └──────────────────┘ └────────┬────────┘ + │ OAuth2 │ + ▼ │ + ┌──────────┐ ┌───────────┼───────────┐ + │ Cognito │ │ │ │ + │ User Pool│ ▼ ▼ ▼ + └──────────┘ ┌──────────┐ ┌──────────┐ ┌──────────┐ + │ SSM │ │ S3 │ │ S3 │ + │ Automati-│ │ Logs │ │ SOPs │ + │ on │ │ (KMS) │ │ Bucket │ + └────┬─────┘ └──────────┘ └──────────┘ + │ + ┌─────────┼─────────┐ + ▼ ▼ ▼ + ┌────────┐┌────────┐┌────────┐ + │EKS Node││EKS Node││EKS Node│ + │Region A││Region B││Region C│ + └────────┘└────────┘└────────┘ +``` + +The design is intentionally hub-and-spoke: one central deployment (Lambda + S3 + Gateway) serves nodes across all AWS regions. Logs always flow back to the central S3 bucket regardless of which region the node lives in. + +--- + +## Component Deep Dive + +### MCP Gateway (Bedrock AgentCore) + +The entry point for all MCP tool calls. AgentCore provides: +- MCP protocol handling (JSON-RPC over HTTP) +- OAuth2 token validation via Cognito +- Request routing to the Lambda function +- Tool schema advertisement to MCP clients + +The gateway is configured with tool definitions that include parameter schemas, descriptions, and required fields. Tool names are kept short (e.g., `collect`, `errors`, `read`) to stay under the 64-character limit when prefixed with the MCP server name. + +### Lambda Function (Tool Router) + +A single Python Lambda (~9700 lines) that implements all 19 MCP tools. It acts as a router: + +1. Receives the tool name and arguments from the gateway +2. Dispatches to the appropriate handler function +3. Returns structured JSON responses + +Key design decisions: +- **Single Lambda**: All tools share the same function to avoid cold start multiplication and simplify IAM. The function has cross-region permissions for EC2, SSM, and S3. +- **Regional clients**: `get_regional_client()` creates boto3 clients for any target region on demand, cached per invocation. +- **Auto-detection**: `detect_instance_region()` tries the default region first, then scans 16 common regions with a 20-second timeout. Explicit `region` parameter always wins. + +### SSM Automation + +Log collection uses the AWS-managed `AWSSupport-CollectEKSInstanceLogs` SSM document. This document: +- Runs on the target EC2 instance via SSM Agent +- Collects 20+ log sources (kubelet, containerd, CNI, iptables, routes, dmesg, sysctl, ENI metadata, IPAMD, etc.) +- Packages everything into a tar.gz archive +- Uploads directly to the central S3 bucket + +The Lambda calls `ssm:StartAutomationExecution` in the target region, passing the S3 bucket and KMS key as parameters. The SSM Automation role is created by CDK with a trust policy for `ssm.amazonaws.com`. + +### S3 Log Storage + +Two S3 buckets: + +1. **Logs bucket** (KMS-encrypted): Stores collected log bundles, findings indexes, baselines, execution metadata, and tcpdump captures. Structure: + ``` + eks-logs/{instance-id}/ + ├── {timestamp}.tar.gz # Raw bundle from SSM + ├── {timestamp}/ # Extracted files (by Unzip Lambda) + │ ├── var/log/kubelet.log + │ ├── var/log/containers/... + │ ├── iptables-rules.txt + │ └── manifest.json + ├── findings-v2.json # Pre-indexed errors + └── baselines/{cluster}/ # Baseline noise profiles + + execution-regions/ # Region metadata for cross-region routing + idempotency-tokens/ # Dedup mappings + tcpdump/{instance-id}/ # Packet captures + ``` + +2. **SOPs bucket**: Stores 41 runbook markdown files, auto-deployed via CDK `BucketDeployment` from `sops/runbooks/`. + +### Findings Indexer + +A separate Lambda triggered by S3 `ObjectCreated` events on `manifest.json` files. When a log bundle is extracted: + +1. Scans all extracted log files for error patterns +2. Assigns severity levels (CRITICAL, HIGH, WARNING, INFO) +3. Assigns stable finding IDs (F-001, F-002, ...) +4. Writes a `findings-v2.json` index to S3 + +This pre-indexing means the `errors` tool returns results instantly without re-scanning files. The agent gets structured findings with IDs it can reference in follow-up calls. + +### Unzip Lambda + +Triggered by S3 `ObjectCreated` events on `.tar.gz` files. It: +1. Downloads the archive +2. Extracts all files to the same S3 prefix +3. Generates a `manifest.json` listing all files with sizes +4. The manifest creation triggers the Findings Indexer + +### Cognito (OAuth2) + +Provides machine-to-machine authentication: +- A User Pool with a resource server defining the `gateway:read` scope +- A client credentials grant flow +- The MCP Gateway validates tokens on every request + +### KMS Encryption + +A customer-managed KMS key encrypts: +- All S3 objects at rest (SSE-KMS) +- The key policy grants the Lambda role, SSM Automation role, and EKS node roles encrypt/decrypt permissions +- Node roles need `kms:GenerateDataKey` and `kms:Encrypt` to upload logs + +--- + +## Data Flow + +### Log Collection Flow + +``` +Agent calls collect(instanceId, region?) + │ + ▼ +Lambda resolves region (explicit > auto-detect > default) + │ + ▼ +Lambda calls SSM StartAutomationExecution in target region + - Passes: instanceId, S3 bucket, KMS key ARN + - Stores: execution-region mapping in S3 + - Returns: executionId + task envelope (async) + │ + ▼ +SSM Agent on the node runs the collection document + - Collects 20+ log sources + - Packages into tar.gz + - Uploads to central S3 bucket (cross-region write) + │ + ▼ +S3 ObjectCreated triggers Unzip Lambda + - Extracts archive + - Writes manifest.json + │ + ▼ +manifest.json triggers Findings Indexer Lambda + - Scans all files for errors + - Writes findings-v2.json + │ + ▼ +Agent polls status(executionId) until complete +Agent calls errors(instanceId) → gets pre-indexed findings +``` + +### Analysis Flow + +Once logs are in S3, all analysis is local to the central region: + +- `errors` → reads `findings-v2.json` (or live-scans if index missing) +- `search` → regex search across extracted files with byte-range reads +- `correlate` → builds cross-file timeline, temporal clusters, root cause chains +- `read` → line-aligned byte-range streaming for specific files +- `summarize` → grounded summary using finding IDs from `errors`/`search` +- `network_diagnostics` → structured analysis of iptables, CNI, routes, DNS, ENI, IPAMD +- `compare_nodes` → diffs findings across multiple nodes + +### Live Packet Capture Flow + +``` +Agent calls tcpdump_capture(instanceId, filter?, podName?, podNamespace?) + │ + ▼ +Lambda calls SSM SendCommand (RunShellScript) in target region + - If podName provided: resolves to container PID via crictl + - Runs tcpdump in the appropriate network namespace + - Captures for durationSeconds (default 120s) + - Uploads pcap + decoded text + stats to S3 + - Returns: commandId (async) + │ + ▼ +Agent polls tcpdump_capture(commandId, instanceId) until complete + │ + ▼ +Agent calls tcpdump_analyze(instanceId, commandId) + - Reads decoded packet text from S3 + - Runs 15 analysis modules: + DNS analysis, TCP RST patterns, kube-proxy DNAT, VPC CNI SNAT, + TCP keepalives, ICMP analysis, CoreDNS transients, SYN flood, + TCP window zero, retransmissions, connection refused, traffic bursts, + top talkers, MTU fragmentation, conntrack pressure + - Returns structured results with anomaly flags +``` + +--- + +## Cross-Region Design + +The stack deploys to one region but operates across all regions: + +``` + ┌─────────────────────────────────────────────┐ + │ Central Region (us-east-1) │ + │ │ + │ MCP Gateway → Lambda → S3 Bucket (KMS) │ + │ │ │ + │ │ SSM StartAutomation │ + └──────────────────┼──────────────────────────┘ + │ + ┌────────────────────────┼────────────────────────┐ + │ │ │ + ▼ ▼ ▼ + ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ + │ us-west-2 │ │ eu-west-1 │ │ ap-southeast-1 │ + │ EKS Node │ │ EKS Node │ │ EKS Node │ + │ ↓ SSM Agent │ │ ↓ SSM Agent │ │ ↓ SSM Agent │ + │ ↓ Upload to S3 │ │ ↓ Upload to S3 │ │ ↓ Upload to S3 │ + └─────────────────┘ └─────────────────┘ └─────────────────┘ +``` + +Key mechanisms: +- **Region resolution priority**: explicit parameter > auto-detect via `ec2:DescribeInstances` > Lambda's own region +- **Region metadata persistence**: After `collect`, the Lambda stores `execution-regions/{executionId}` in S3 so `status`/`validate` auto-route to the correct region +- **Cross-region S3 writes**: The node's IAM role writes directly to the central bucket — S3 is global, so this works regardless of the node's region +- **Lambda IAM**: CDK grants `ec2:DescribeInstances`, `ssm:*` across `*` resources so the Lambda can operate in any region + +--- + +## Tool Architecture + +### Tier 1 — Core Operations + +| Tool | Purpose | Key Design | +|------|---------|------------| +| `collect` | Start log collection | Idempotency via token mapping in S3. Cross-region SSM dispatch. Returns async task envelope. | +| `status` | Poll execution progress | Reads region metadata from S3. Parses SSM failure reasons. Estimates progress percentage. | +| `validate` | Verify bundle completeness | Reads `manifest.json`. Reports file count, total size, missing expected files. | +| `errors` | Pre-indexed findings | Reads `findings-v2.json` first (fast path). Falls back to live scan. Supports severity filter, pagination, baseline subtraction. | +| `read` | Byte-range streaming | Line-aligned reads. Supports `startByte/endByte` or `startLine/lineCount`. No truncation — handles multi-GB files. | + +### Tier 2 — Advanced Analysis + +| Tool | Purpose | Key Design | +|------|---------|------------| +| `search` | Regex search across logs | Searches all extracted files. Returns matches with finding IDs (S-NNN), line numbers, context lines. | +| `correlate` | Cross-file timeline | Extracts timestamps from all files. Builds temporal clusters. Generates root cause chains with confidence scores. Reports data gaps. | +| `artifact` | Presigned URLs | 15-minute expiration. For downloading large files outside MCP. | +| `summarize` | Incident summary | Grounded in finding IDs — agent must pass IDs from `errors`/`search`. Unresolved IDs are flagged. Prevents hallucinated evidence. | +| `history` | Audit trail | Lists past collections by instance. Supports cross-region. | + +### Tier 3 — Cluster-Level Intelligence + +| Tool | Purpose | Key Design | +|------|---------|------------| +| `cluster_health` | Cluster overview | Enumerates nodes via EKS + EC2 APIs. Checks SSM Agent status. Reports unhealthy nodes. | +| `compare_nodes` | Diff findings | Collects findings from 2+ nodes. Separates common vs unique errors. Generates comparison insight. | +| `batch_collect` | Smart batch collection | Statistical sampling for large clusters. Dry-run mode. Prioritizes unhealthy nodes. | +| `batch_status` | Batch polling | Polls multiple executions. Reports overall completion percentage. | +| `network_diagnostics` | Networking analysis | Structured parsing of iptables, CNI config, routes, DNS, ENI metadata, IPAMD logs. Issues severity assessment. | + +### Tier 4 — Live Packet Capture + +| Tool | Purpose | Key Design | +|------|---------|------------| +| `tcpdump_capture` | Run tcpdump via SSM | Async via SSM RunCommand. Supports pod-level capture (resolves PID via crictl). Uploads pcap + decoded text to S3. | +| `tcpdump_analyze` | Analyze capture | 15 analysis modules covering DNS, RST, DNAT, SNAT, keepalives, ICMP, SYN floods, retransmissions, top talkers, MTU, conntrack. | + +### Tier 5 — SOP Management + +| Tool | Purpose | Key Design | +|------|---------|------------| +| `list_sops` | Browse runbooks | Lists all 41 SOPs with title, description, severity, trigger patterns. | +| `get_sop` | Retrieve full SOP | Returns complete 3-phase procedure. Agent follows it step by step. | + +--- + +## Time-Bounded Analysis + +All analysis tools enforce time windows to prevent historical noise from polluting active investigations. + +Resolution order: +1. Explicit `start_time` + `end_time` → used as-is +2. `incident_time` → window = incident_time ± 5 minutes +3. Nothing provided → last 10 minutes from current UTC +4. Maximum window: 24 hours (safety cap) + +Every response includes `window_start_utc`, `window_end_utc`, `resolution_reason`, and counts of findings excluded outside the window. + +--- + +## Anti-Hallucination Design + +The server is designed to prevent AI agents from fabricating evidence: + +1. **Finding IDs**: Every error gets a stable ID (F-001, S-001). The `summarize` tool requires the agent to pass specific finding IDs. Any ID that doesn't resolve to a real finding is flagged in the response. + +2. **Baseline subtraction**: Known cluster noise is annotated (not removed). The agent sees `"is_baseline": true` but the finding is still present — the user retains full visibility. + +3. **Confidence scores**: `correlate` reports confidence levels and data quality gaps. If timestamps are missing or log coverage is incomplete, the response says so. + +4. **Grounded summaries**: `summarize` only includes evidence from findings the agent explicitly references. No inferred or assumed evidence. + +--- + +## SOP Runbook System + +41 runbooks covering the most common EKS node-level failure categories. Each follows a consistent structure: + +``` +Phase 1 — Triage (MUST) + → Check pod/node state via EKS MCP tools first + → Collect logs, get pre-indexed findings + +Phase 2 — Enrich (SHOULD) + → Deep search, correlate, domain-specific diagnostics + +Phase 3 — Report (MUST) + → Grounded incident summary with finding IDs + → Root cause, evidence chain, remediation steps +``` + +SOPs are stored as markdown in `sops/runbooks/`, deployed to S3 via CDK `BucketDeployment`, and retrieved at runtime by the agent using `list_sops` and `get_sop`. + +--- + +## Security Model + +| Layer | Mechanism | +|-------|-----------| +| Authentication | Cognito OAuth2 client credentials grant | +| Encryption at rest | KMS customer-managed key for all S3 objects | +| Encryption in transit | HTTPS enforced on S3 (deny HTTP policy) | +| Public access | S3 Block Public Access enabled | +| IAM | Least-privilege: Lambda role scoped to required actions, node roles scoped to PutObject on the logs bucket | +| Presigned URLs | 15-minute expiration for artifact downloads | +| Idempotency | Token-based dedup prevents duplicate SSM executions | +| Audit | CloudWatch logs for all Lambda invocations | + +--- + +## CDK Construct Design + +The infrastructure is a single CDK construct (`SsmAutomationGatewayV2Construct`) that provisions everything: + +- S3 buckets (logs + SOPs) with KMS encryption and lifecycle policies +- Lambda functions (main handler, unzip, findings indexer, client secret retriever) +- SSM Automation role with trust policy +- Cognito User Pool, resource server, and client +- Bedrock AgentCore Gateway with tool schema definitions +- IAM policies with cross-region permissions +- S3 event notifications wiring (tar.gz → unzip, manifest.json → indexer) +- BucketDeployment for SOP runbooks + +The construct accepts optional props for customization (gateway name, log retention, encryption toggle, node role ARNs) but works with zero configuration. + +--- + +## Deploy Script Design + +The `deploy.sh` script handles the full deployment lifecycle with interactive prompts: + +### Interactive Flow + +``` +Step 1: Region Selection + ├── 1) All enabled regions + ├── 2) Current deploy region only + └── 3) Enter a specific region + +Step 2: Cluster Selection + ├── Lists all discovered EKS clusters with region + ├── a) All clusters + └── 1,2,5) Comma-separated picks + +Step 3: Node Role Selection + ├── Lists all unique node role ARNs with source cluster + ├── a) All roles + └── 1,3) Comma-separated picks + +Fallback: Manual ARN Entry + └── If no clusters/roles found, prompts for manual ARN input +``` + +### What It Does + +1. Installs npm dependencies and builds TypeScript +2. Bootstraps CDK if needed +3. Detects or creates the SSM Default Host Management role +4. Interactively discovers EKS clusters and node roles (or accepts manual input) +5. Deploys the CDK stack +6. Retrieves all configuration values (Gateway URL, Cognito credentials, etc.) +7. Saves configuration to `mcp-config.txt` + +### Automation Mode + +Skip all interactive prompts by providing role ARNs directly: + +```bash +EKS_NODE_ROLE_ARNS="arn:aws:iam::123456789012:role/MyNodeRole" ./deploy.sh +``` + +Or as a positional argument: + +```bash +./deploy.sh EksNodeLogMcpStack arn:aws:iam::123456789012:role/MyNodeRole +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/get-config.sh b/mcp/aws-eks-node-diagnostics-mcp/get-config.sh new file mode 100755 index 0000000..00d51a6 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/get-config.sh @@ -0,0 +1,52 @@ +#!/bin/bash +# EKS Node Log MCP - Get Configuration Values +# Run this after deployment to retrieve all configuration values + +STACK_NAME="${1:-SsmAutomationGatewayStack}" +REGION="${AWS_REGION:-us-east-1}" + +if [ ! -f cdk-outputs.json ]; then + echo "Error: cdk-outputs.json not found. Run ./deploy.sh first." + exit 1 +fi + +# Parse values from cdk-outputs.json +GATEWAY_URL=$(python3 -c "import json; d=json.load(open('cdk-outputs.json')); print([v for k,v in d.get('$STACK_NAME',{}).items() if 'GatewayUrl' in k][0])" 2>/dev/null) +CLIENT_ID=$(python3 -c "import json; d=json.load(open('cdk-outputs.json')); print([v for k,v in d.get('$STACK_NAME',{}).items() if 'CognitoClientId' in k][0])" 2>/dev/null) +USER_POOL_ID=$(python3 -c "import json; d=json.load(open('cdk-outputs.json')); print([v for k,v in d.get('$STACK_NAME',{}).items() if 'CognitoUserPoolId' in k][0])" 2>/dev/null) +TOKEN_URL=$(python3 -c "import json; d=json.load(open('cdk-outputs.json')); print([v for k,v in d.get('$STACK_NAME',{}).items() if 'OAuthExchangeUrl' in k][0])" 2>/dev/null) +OAUTH_SCOPE=$(python3 -c "import json; d=json.load(open('cdk-outputs.json')); print([v for k,v in d.get('$STACK_NAME',{}).items() if 'OAuthScope' in k][0])" 2>/dev/null) +LOGS_BUCKET=$(python3 -c "import json; d=json.load(open('cdk-outputs.json')); print([v for k,v in d.get('$STACK_NAME',{}).items() if 'LogsBucketName' in k][0])" 2>/dev/null) + +# Get Cognito Client Secret +CLIENT_SECRET=$(aws cognito-idp describe-user-pool-client \ + --user-pool-id "$USER_POOL_ID" \ + --client-id "$CLIENT_ID" \ + --region "$REGION" \ + --query "UserPoolClient.ClientSecret" \ + --output text 2>/dev/null) + +echo "" +echo "==============================================" +echo "DEVOPS AGENT MCP SERVER CONFIGURATION" +echo "==============================================" +echo "" +echo "MCP Server URL:" +echo " $GATEWAY_URL" +echo "" +echo "OAuth Client ID:" +echo " $CLIENT_ID" +echo "" +echo "OAuth Client Secret:" +echo " $CLIENT_SECRET" +echo "" +echo "Token URL:" +echo " $TOKEN_URL" +echo "" +echo "Scope (use only ONE):" +echo " $OAUTH_SCOPE" +echo "" +echo "Logs Bucket:" +echo " $LOGS_BUCKET" +echo "" +echo "==============================================" diff --git a/mcp/aws-eks-node-diagnostics-mcp/package-lock.json b/mcp/aws-eks-node-diagnostics-mcp/package-lock.json new file mode 100644 index 0000000..74b7cf9 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/package-lock.json @@ -0,0 +1,5701 @@ +{ + "name": "eks-node-log-mcp", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "eks-node-log-mcp", + "version": "1.0.0", + "license": "MIT-0", + "dependencies": { + "aws-cdk-lib": "^2.170.0", + "constructs": "^10.0.0" + }, + "devDependencies": { + "@types/jest": "^30.0.0", + "@types/node": "^20.0.0", + "aws-cdk": "^2.170.0", + "fast-check": "^4.5.3", + "jest": "^29.7.0", + "ts-jest": "^29.4.6", + "typescript": "^5.0.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.263", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.263.tgz", + "integrity": "sha512-X9JvcJhYcb7PHs8R7m4zMablO5C9PGb/hYfLnxds9h/rKJu6l7MiXE/SabCibuehxPnuO/vk+sVVJiUWrccarQ==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", + "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "48.20.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-48.20.0.tgz", + "integrity": "sha512-+eeiav9LY4wbF/EFuCt/vfvi/Zoxo8bf94PW5clbMraChEliq83w4TbRVy0jB9jE0v1ooFTtIjSQkowSPkfISg==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.2" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.2", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-bigint": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", + "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.28.6.tgz", + "integrity": "sha512-jiLC0ma9XkQT3TKJ9uYvlakm66Pamywo+qwL+oL8HJOvc6TWdZXVfhqJr8CCzbSGUAbDOzlGHJC1U+vRfLQDvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz", + "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.28.6.tgz", + "integrity": "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/console/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/console/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/core/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/core/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/environment/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.2.0.tgz", + "integrity": "sha512-1JnRfhqpD8HGpOmQp180Fo9Zt69zNtC+9lR+kT7NVL05tNXIi+QC8Csz7lfidMoVLPD3FnOtcmp0CEFnxExGEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect/node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/expect/node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/expect/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/fake-timers/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/fake-timers/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/globals/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/reporters/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/@jest/reporters/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/reporters/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/transform/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/graceful-fs": { + "version": "4.1.9", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.9.tgz", + "integrity": "sha512-olP3sd1qOEe5dXTSaFvQG+02VdRXcdytWLAZsAq1PecU8uqQAhkrnbli7DagjtXKW/Bl7YJbUsa8MPcuc8LHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", + "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-coverage": "*" + } + }, + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/node": { + "version": "20.19.32", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.32.tgz", + "integrity": "sha512-Ez8QE4DMfhjjTsES9K2dwfV258qBui7qxUsoaixZDiTzbde4U12e1pXGNu/ECsUIOi5/zoCxAQxIhQnaUQ2VvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/yargs-parser": "*" + } + }, + "node_modules/@types/yargs-parser": { + "version": "21.0.3", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", + "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/aws-cdk": { + "version": "2.1105.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1105.0.tgz", + "integrity": "sha512-1RY2UZJv31XYobEGFHQEb7c2HXNzDbHuHqdnfdYyygvZW4Nrm8MJCW42lqItQCn+wF52Ixc7r2VR5eR4YGtVhA==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.237.1", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.237.1.tgz", + "integrity": "sha512-RH8mWHLBtc14stkeUF0gFLaWdS5iS2AHUHnoT5B5LLZfEkYP9G43LjJs0AMmpdlQ5/ZQVvCZ+VB83Q9vLxILRw==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "2.2.263", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^48.20.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.3", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^3.1.2", + "punycode": "^2.3.1", + "semver": "^7.7.3", + "table": "^6.9.0", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "constructs": "^10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.17.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-styles": { + "version": "4.3.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.12", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/concat-map": { + "version": "0.0.1", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.1.0", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.2.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.7.3", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "inBundle": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/strip-ansi": { + "version": "6.0.1", + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-istanbul/node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-import-attributes": "^7.24.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5" + }, + "peerDependencies": { + "@babel/core": "^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.19", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.19.tgz", + "integrity": "sha512-ipDqC8FrAl/76p2SSWKSI+H9tFwm7vYqXQrItCuiVPt26Km0jS+NzSsBWAaBusvSbQcfJG+JitdMm+wZAgTYqg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bs-logger": { + "version": "0.2.6", + "resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz", + "integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-json-stable-stringify": "2.x" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "node-int64": "^0.4.0" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001770", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001770.tgz", + "integrity": "sha512-x/2CLQ1jHENRbHg5PSId2sXq1CIO1CISvwWAj027ltMVG2UNgW+w9oH2+HzgEIRFembL8bUlXtfbBHR1fCg2xw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } + }, + "node_modules/collect-v8-coverage": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.3.tgz", + "integrity": "sha512-1L5aqIkwPfiodaMgQunkF1zRhNqifHBmtbbbxcr6yVxxBnliw4TDOW6NxpO8DJLgJ16OT+Y4ztZqP6p/FtXnAw==", + "dev": true, + "license": "MIT" + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/constructs": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.5.tgz", + "integrity": "sha512-fOoP70YLevMZr5avJHx2DU3LNYmC6wM8OwdrNewMZou1kZnPGOeVzBrRjZNgFDHUlulYUjkpFRSpTE3D+n+ZSg==", + "license": "Apache-2.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" + }, + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/create-jest/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.7.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.7.1.tgz", + "integrity": "sha512-9JmrhGZpOlEgOLdQgSm0zxFaYoQon408V1v49aqTWuXENVlnCuY9JBZcXZiCsZQWDjTm5Qf/nIvAy77mXDAjEg==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.286", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.286.tgz", + "integrity": "sha512-9tfDXhJ4RKFNerfjdCcZfufu49vg620741MNs26a9+bhLThdB+plgMeou98CAaHu/WATj2iHOOHTp1hWtABj2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha512-Zk/eNKV2zbjpKzrsQ+n1G6poVbErQxJ0LBOJXaKZ1EViLzH+hrLu9cdXI4zw9dBQJslwBEpbQ2P1oS7nDxs6jQ==", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.2.0.tgz", + "integrity": "sha512-u/feCi0GPsI+988gU2FLcsHyAHTU0MX1Wg68NhAnN7z/+C5wqG+CY8J53N9ioe8RXgaoz0nBR/TYMf3AycUuPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.2.0", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.2.0", + "jest-message-util": "30.2.0", + "jest-mock": "30.2.0", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-check": { + "version": "4.5.3", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.5.3.tgz", + "integrity": "sha512-IE9csY7lnhxBnA8g/WI5eg/hygA6MGWJMSNfFRrBlXUciADEhS1EDB0SIsMSvzubzIlOBbVITSsypCsW717poA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^7.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "license": "ISC", + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.2.tgz", + "integrity": "sha512-O8dpsF+r0WV/8MNRKfnmrtCWhuKjxrq2w+jpzBL5UZKTi2LeVWnWOmWRxFlesJONmc+wLAGvKQZEOanko0LFTg==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-config/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.2.0.tgz", + "integrity": "sha512-dQHFo3Pt4/NLlG5z4PxZ/3yZTZ1C7s9hveiOj+GCN+uT109NC2QgsoVZsVOAvbJ3RgKkvyLGXZV9+piDpWbm6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-environment-node/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-haste-map/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-leak-detector/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-leak-detector/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.2.0.tgz", + "integrity": "sha512-dQ94Nq4dbzmUWkQ0ANAWS9tBRfqCrn0bV9AMYdOi/MHW726xn7eQmMeRTpX2ViC00bpNaWXq+7o4lIQ3AX13Hg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.2.0", + "pretty-format": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.2.0.tgz", + "integrity": "sha512-y4DKFLZ2y6DxTWD4cDe07RglV88ZiNEdlRfGtqahfbIjfsw1nMCPx49Uev4IA/hWn3sDKyAnSPwoYSsAEdcimw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.2.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.2.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-mock": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.2.0.tgz", + "integrity": "sha512-JNNNl2rj4b5ICpmAcq+WbLH83XswjPbjH4T7yvGzfAGCPh1rw+xVNbtk+FnRslvt9lkCcdn9i1oAoKUuFsOxRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "jest-util": "30.2.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.2.0.tgz", + "integrity": "sha512-QKNsM0o3Xe6ISQU869e+DhG+4CK/48aHYdJZGlFQVTjnbvgpcKyxpzk29fGiO7i/J8VENZ+d2iGnSsvmuHywlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.2.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/@jest/types": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.2.0.tgz", + "integrity": "sha512-H9xg1/sfVvyfU7o3zMfBEjQ1gcsdeTMgqHoYdN79tuLqfTtuu7WckRA1R5whDwOzxaZAeMKTYWqP+WCAi0CHsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-util/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pretty-format": { + "version": "30.2.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.2.0.tgz", + "integrity": "sha512-9uBdv/B4EefsuAL+pWqueZyZS2Ba+LxfFeQ9DN14HU4bN8bhaxKdkpjpB6fs9+pSjIBu+FXQHImEg8j/Lw0+vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/pretty-format/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/test-exclude": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", + "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", + "dev": true, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-jest": { + "version": "29.4.6", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.6.tgz", + "integrity": "sha512-fSpWtOO/1AjSNQguk43hb/JCo16oJDnMJf3CdEGNkqsEX3t0KX96xvyX1D7PfLCpVoKu4MfVrqUkFyblYoY4lA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.3", + "type-fest": "^4.41.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "ts-jest": "cli.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || ^18.0.0 || >=20.0.0" + }, + "peerDependencies": { + "@babel/core": ">=7.0.0-beta.0 <8", + "@jest/transform": "^29.0.0 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.0.0", + "typescript": ">=4.3 <6" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "@jest/transform": { + "optional": true + }, + "@jest/types": { + "optional": true + }, + "babel-jest": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/type-detect": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", + "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/v8-to-istanbul": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.3.0.tgz", + "integrity": "sha512-kiGUalWN+rgBJ/1OHZsBtU4rXZOfj/7rKQxULKlIzwzQSvMJUUNgPwJEEh7gU6xEVxC0ahoOBvN2YI8GH6FNgA==", + "dev": true, + "license": "ISC", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", + "@types/istanbul-lib-coverage": "^2.0.1", + "convert-source-map": "^2.0.0" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/mcp/aws-eks-node-diagnostics-mcp/package.json b/mcp/aws-eks-node-diagnostics-mcp/package.json new file mode 100644 index 0000000..caf1eaf --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/package.json @@ -0,0 +1,45 @@ +{ + "name": "eks-node-log-mcp", + "version": "1.0.0", + "description": "MCP Server for DevOps Agent to collect EKS node logs via SSM Automation", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest && python3 -m pytest tests/ -v", + "lint:cfn": "npx cdk synth --quiet -o cdk.out && cfn-lint cdk.out/EksNodeLogMcpStack.template.json", + "cdk": "cdk", + "synth": "cdk synth", + "deploy": "cdk deploy", + "destroy": "cdk destroy" + }, + "keywords": [ + "aws", + "cdk", + "ssm", + "automation", + "mcp", + "devops-agent", + "eks", + "troubleshooting" + ], + "engines": { + "node": ">=18.0.0" + }, + "author": "AWS", + "license": "MIT-0", + "devDependencies": { + "@types/jest": "^30.0.0", + "@types/node": "^20.0.0", + "aws-cdk": "^2.170.0", + "fast-check": "^4.5.3", + "jest": "^29.7.0", + "ts-jest": "^29.4.6", + "typescript": "^5.0.0" + }, + "dependencies": { + "aws-cdk-lib": "^2.170.0", + "constructs": "^10.0.0" + } +} diff --git a/mcp/aws-eks-node-diagnostics-mcp/requirements-dev.txt b/mcp/aws-eks-node-diagnostics-mcp/requirements-dev.txt new file mode 100644 index 0000000..1167a06 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/requirements-dev.txt @@ -0,0 +1,3 @@ +pytest>=7.0 +hypothesis>=6.0 +cfn-lint>=1.0 diff --git a/mcp/aws-eks-node-diagnostics-mcp/scripts/pre-commit-cfn-lint.sh b/mcp/aws-eks-node-diagnostics-mcp/scripts/pre-commit-cfn-lint.sh new file mode 100755 index 0000000..450cd64 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/scripts/pre-commit-cfn-lint.sh @@ -0,0 +1,57 @@ +#!/bin/bash +# Pre-commit hook: synthesize CDK and validate with cfn-lint +# Install: cp scripts/pre-commit-cfn-lint.sh .git/hooks/pre-commit && chmod +x .git/hooks/pre-commit +# Or run manually: bash scripts/pre-commit-cfn-lint.sh + +set -e + +echo "Running cfn-lint on synthesized CloudFormation template..." + +# Synth to a temp directory to avoid polluting cdk.out +TMPDIR=$(mktemp -d) +trap "rm -rf $TMPDIR" EXIT + +npx cdk synth --quiet -o "$TMPDIR" 2>/dev/null + +TEMPLATE="$TMPDIR/EksNodeLogMcpStack.template.json" +if [ ! -f "$TEMPLATE" ]; then + echo "ERROR: Template not found after cdk synth" + exit 1 +fi + +# Find cfn-lint — check PATH first, then common pip install locations +CFN_LINT=$(command -v cfn-lint 2>/dev/null || \ + python3 -c "import shutil; print(shutil.which('cfn-lint') or '')" 2>/dev/null || true) + +if [ -z "$CFN_LINT" ]; then + # Try user pip bin directories + for candidate in \ + "$HOME/Library/Python/3.9/bin/cfn-lint" \ + "$HOME/Library/Python/3.11/bin/cfn-lint" \ + "$HOME/Library/Python/3.12/bin/cfn-lint" \ + "$HOME/.local/bin/cfn-lint"; do + if [ -x "$candidate" ]; then + CFN_LINT="$candidate" + break + fi + done +fi + +if [ -z "$CFN_LINT" ]; then + echo "ERROR: cfn-lint not found. Install with: pip3 install cfn-lint" + exit 1 +fi + +# Run cfn-lint — capture exit code without triggering set -e +RESULT=0 +"$CFN_LINT" "$TEMPLATE" || RESULT=$? + +if [ $RESULT -eq 0 ]; then + echo "cfn-lint: PASSED (no issues)" +elif [ $RESULT -eq 4 ]; then + echo "cfn-lint: PASSED (warnings only — see above)" + exit 0 +else + echo "cfn-lint: FAILED — fix errors before committing" + exit 1 +fi diff --git a/mcp/aws-eks-node-diagnostics-mcp/skills/eks-node-diagnostics.zip b/mcp/aws-eks-node-diagnostics-mcp/skills/eks-node-diagnostics.zip new file mode 100644 index 0000000000000000000000000000000000000000..041822d3c47722863ac249c6615f81619b299290 GIT binary patch literal 190116 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

B+u+URFo{3AOItSruGX!%(R=-+|11H}LI z2J8IeZ#)e+baU#fGHL@VQ)HirS0)!;~&UE>gML(F~`#*qj6Ju10*eAk!IH$c{{_&jp|bN-Il+;gV)@$J!7-7 z0<1DwOx$?k3#81nrA;o5_|78?6M_=HNqwf1e>^z|p22P-(?Z>r=f|b*i-6c+mt&v4 zz$^aw$cnUb=ra1e`&%}nh~p(8)-XOWDK33uepMidl6{yoys%jA$nf_3xKf5yhd0Jv zPeUV2G>()lUwX)GL{;X)K|s+mP(%U10BTRVQ3-GY1r=Cd`e@Ky+>UG!k{ZZ*@JMPs zNyTreFqS|iF$y#Sf5idSF2)ipK=6Fjkv$V+;BSF!Pi39a@(1c2=dcA)$v$TmUmZ_f z&o0)kmyFTcOL4F@?7OX}IIgjMrOVu@6a%K#*W>s7(B+6HEb6LbKg6bDF$lx2icJz% z`~VE1R86t^FME8OIG1_b4#tjW)VqsYb0ERmIfQXN20L8wVdi+%9z7UgM>!NVAXV;( zzRSi;2!?-200629lm}2HKut7Z*+~cjTH*f6-y|_q8}2rn#h2ED0nwII86y8lenWxp zz&wdLvR@n|(%cly(#M?e<=HEE0w+qf$y~w;oV{VyTva3n%M(EqBn&|76;MTes`Z;9 zezP@$0mmA&ocy64PkykAE^%t@dQ^kjbj~@Q_MCdz5`YnPl@CE65&>WiaK*(?@0+W5 z8X9D4H$gI&e9JmO+1Zoo2byT=P|Z@An2zPq(J3;iVd}+aO5?}1G&MvMBgm-SBP@>ACq>7?dlb{S0zyTc$u_EE1wqR=l-IE;tX;8zTq6WbLL z#LbdZ8kG&f!6Y%42;}WI%tO;FTSy*XQHqUH!5|2xXZ>f(iSC)M-t8g93U-S?5(L?D zN&DPI%=xiQ;xM!j%_!?dhx5GOFSYWx{mGPOBj;Lr@BICWgXLuFn#A?l4v~%UanyOv zm13j#QNwyxJeCYMhnr6bI77?C- zYrCPC;XU&sYICq#VC0;hF2fzrb-eVrN}*Vh&<1(US<0X>S~&_7hY&qEzV<*yTIG}) z#@)Jvme0!3_}e<#R12YBYYxFo_*a}wBtdNM36lU4sM$cillywcrFw-JfFWvxY$Py` zM)s!a4!VeD#@2x~up}ALgz;6$J)&k9CjoFh%&?ah5OtXbTgZ;+6I2(~oYnnbeRz5$ z`+)-~(uR6lq$}vgaj^i5a}rf0S~Pj!-+Ot(=ZQN_Q(pYr zTw1t}AfmJmTDJNlRF^o|I_b@9aj&cr@dVzY@e$gs_tas_69CB_do#d~ zHqTc&3B}dN?gMZD=bLk+RF6L4_kn5d8e|Hzc5l$-=?Enfa)SH;2ESL6HN@>%`G065 zGffpKrM*Wl5s8~_5YVE^CJUML5}rI`kYJqTfbtPg*6EW{WJ(Mk6$t{jW@KK626?E2 zKZJ~;-jR17D>NNeW;!WKCAK-h4J+f&=RDOx)?2K1RHGCa6+rd+%m53p1Oa~eBB7;b zV*9ly{+Dg2YZ+i}{;&l$!Nvi(owlBOd;=e}qHB=B0$oprUgF1!N-dx(PC%nNt2F1} z3UyqE!I;=ARcPfU;prigP)zhh!y9XLhh$6xy6MNTQ&g9Nn=%b7V2V@r3=G`pu1+-* zr~(DhtWjA*JbpcV4_z^rkP12sah&CtEriSlc6_-xI)#)3e=8f;K}Psq2RsLT`MTQO ze?y#AZp1NpP{^JOckLzQiypl=u%+ta8ku`^|9YI8>^040@_eTTORJ0sh){_MhImva-*dLvb!pFbVjn#j!`tX5`(PU z;mdAZm>_##3@yNc%p%oeJVVKa1sfIfa&Va7`2+Oq+by{`F^78|)4SF$Flsw{7e5J= ziRl^4⪚^lcrPwWo>|B4Oz(XkY}z`bagKU@Z0yi5|P{u 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 or CNI-E4- | + +### IMDS-Only Mode +| Claim | Repo File | Function | Gate | +|-------|-----------|----------|------| +| IMDS-only skips EC2 API | `pkg/awsutils/awsutils.go` | DescribeAllENIs | ENABLE_IMDS_ONLY_MODE=true | +| IMDS-only disables ENI provisioning | `pkg/ipamd/ipamd.go` | disableENIProvisioning() | enableImdsOnlyMode() returns true | + +### Removed/Deprecated Env Vars +| Env Var | Status | Replacement | +|---------|--------|-------------| +| ENABLE_NFTABLES | REMOVED from codebase | Auto-detection from kubelet (v1.13.1+) | + +## Anti-Hallucination Rules + +1. **NEVER claim SGP affects unannotated pods** — The PodVlanId gate in rpc_handler.go proves this is impossible. +2. **NEVER claim CNI manages KUBE-SERVICES** — The string "KUBE-SERVICES" does not appear in the CNI codebase. +3. **NEVER claim externalSNAT breaks service routing** — Service masquerade is kube-proxy's KUBE-POSTROUTING, independent of CNI SNAT. +4. **NEVER claim prefix delegation changes routing** — PD only changes IP allocation strategy. +5. **NEVER claim IPv6 mode has SNAT rules** — updateHostIptablesRules() returns nil for v6. +6. **NEVER claim CNI config is cluster-wide** — All config is node-local via os.Getenv. +7. **NEVER confuse NETWORK_POLICY_ENFORCING_MODE with POD_SECURITY_GROUP_ENFORCING_MODE** — They are completely different subsystems (eBPF network policy vs SGP veth naming). +8. **NEVER claim DISABLE_NETWORK_RESOURCE_PROVISIONING breaks networking** — It only disables ENI provisioning, not existing networking. +9. **NEVER claim WARM_IP_TARGET limits max pods** — It controls warm pool pre-allocation, not pod capacity. +10. **NEVER confuse AWS_VPC_ENI_MTU with POD_MTU** — ENI MTU is host-level, POD_MTU is pod veth-level. +11. **NEVER flag empty main route table as broken on multi-ENI nodes** — Per-ENI policy routing replaces the main table. +12. **NEVER flag missing KUBE-SVC chains as broken in IPVS mode** — IPVS uses kernel hash tables, not iptables chains. +13. **NEVER blame IPAMD for transient "IP not in datastore" during cooldown** — 30s cooldown after pod deletion is normal. +14. **NEVER blame VPC CNI or kube-proxy for conntrack table exhaustion** — It's a kernel resource limit, fix via nf_conntrack_max. +15. **NEVER claim nm-cloud-setup is compatible with VPC CNI** — It overwrites per-ENI ip rules, breaking pod networking. +16. **NEVER restart kube-proxy during API server outages** — Static stability keeps existing rules working. +17. **NEVER claim ENABLE_IMDS_ONLY_MODE breaks networking** — It only changes ENI discovery from EC2 API to IMDS. Existing pod networking is unaffected. +18. **NEVER confuse ENABLE_V4_EGRESS with ENABLE_V6_EGRESS** — V4 egress is for IPv6 clusters (enables IPv4 outbound). V6 egress is for IPv4 clusters (enables IPv6 outbound). The naming is counterintuitive. +19. **NEVER reference ENABLE_NFTABLES as a current env var** — It has been REMOVED from the VPC CNI codebase. Auto-detection replaced it in v1.13.1+. diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/cni-semantics/cni-config-index.yaml b/mcp/aws-eks-node-diagnostics-mcp/sops/cni-semantics/cni-config-index.yaml new file mode 100644 index 0000000..d83bb4d --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/cni-semantics/cni-config-index.yaml @@ -0,0 +1,674 @@ +# ============================================================================= +# VPC CNI Semantics Index +# Source of truth: amazon-vpc-cni-k8s (github.com/aws/amazon-vpc-cni-k8s) +# Generated from repo scan — DO NOT edit manually without re-verifying against repo +# ============================================================================= + +# ───────────────────────────────────────────────────────────────────────────── +# SECTION 1: IPAMD Environment Variables (pkg/ipamd/ipamd.go) +# These are read by the aws-k8s-agent binary (IPAMD daemon) +# ───────────────────────────────────────────────────────────────────────────── + +- key_name: WARM_IP_TARGET + source_file_path: pkg/ipamd/ipamd.go + parse_location: "getWarmIPTarget() — os.LookupEnv" + default_value: "0 (disabled — WARM_ENI_TARGET takes precedence)" + semantic_summary: > + Number of free IPs to keep available in the warm pool. When set, overrides + WARM_ENI_TARGET. IPAMD allocates/deallocates IPs to maintain this target. + With prefix delegation, maintains warmth at /28 prefix granularity. + preconditions: + - "Must be a positive integer" + - "Overrides WARM_ENI_TARGET when set" + observable_signals: + - "ipamd logs: 'Warm IP target' messages" + - "aws-node metrics: awscni_warm_ip_target" + non_effects: + - "Does NOT affect pod networking or routing" + - "Does NOT affect iptables rules" + - "Does NOT affect security groups" + +- key_name: MINIMUM_IP_TARGET + source_file_path: pkg/ipamd/ipamd.go + parse_location: "getMinimumIPTarget() — os.LookupEnv" + default_value: "0 (disabled)" + semantic_summary: > + Floor for total IPs on node. IPAMD allocates max(WARM_IP_TARGET, MINIMUM_IP_TARGET - assigned). + Ensures a minimum number of IPs are always available regardless of current pod count. + preconditions: + - "Must be a positive integer" + observable_signals: + - "ipamd logs: 'Minimum IP target' messages" + non_effects: + - "Does NOT affect pod networking or routing" + +- key_name: WARM_ENI_TARGET + source_file_path: pkg/ipamd/ipamd.go + parse_location: "getWarmENITarget() — os.LookupEnv" + default_value: "1" + semantic_summary: > + Number of full ENIs worth of IPs to keep available. Default 1 means one + entire ENI's worth of IPs are pre-allocated. Overridden by WARM_IP_TARGET if set. + preconditions: + - "Must be a non-negative integer" + - "Ignored if WARM_IP_TARGET is set" + observable_signals: + - "ipamd logs: 'Warm ENI target' messages" + - "EC2 console: number of ENIs attached to instance" + non_effects: + - "Does NOT affect pod networking or routing" + +- key_name: MAX_ENI + source_file_path: pkg/ipamd/ipamd.go + parse_location: "getMaxENI() — os.LookupEnv" + default_value: "-1 (use instance type limit)" + semantic_summary: > + Maximum number of ENIs IPAMD will attach. -1 means use the instance type's + maximum. Capped by instance type limit regardless of setting. + preconditions: + - "Must be >= 1 or -1" + observable_signals: + - "ipamd logs: 'Max ENI' messages" + non_effects: + - "Does NOT affect routing or iptables" + +- key_name: ENABLE_PREFIX_DELEGATION + source_file_path: pkg/ipamd/ipamd.go + parse_location: "usePrefixDelegation() — utils.GetBoolAsStringEnvVar" + default_value: "false" + semantic_summary: > + Allocates /28 IPv4 prefixes (16 IPs each) instead of individual secondary IPs. + Dramatically increases max pods per node. Requires Nitro instances. + With PD enabled, warm pool targets are converted to prefix granularity. + preconditions: + - "Nitro instance type required" + - "WARM_PREFIX_TARGET or WARM_IP_TARGET or MINIMUM_IP_TARGET must be set > 0" + - "Required for IPv6 mode" + observable_signals: + - "ipamd logs: 'Prefix delegation enabled'" + - "EC2 console: /28 prefixes on ENIs instead of individual IPs" + - "ip addr show: pod IPs from /28 ranges" + non_effects: + - "Does NOT change iptables rules" + - "Does NOT affect SNAT behavior" + - "Does NOT affect security groups" + +- key_name: WARM_PREFIX_TARGET + source_file_path: pkg/ipamd/ipamd.go + parse_location: "getWarmPrefixTarget() — os.LookupEnv" + default_value: "0" + semantic_summary: > + Number of /28 prefixes to keep in warm pool. Only applies when + ENABLE_PREFIX_DELEGATION=true. Overridden by WARM_IP_TARGET if both set. + preconditions: + - "ENABLE_PREFIX_DELEGATION must be true" + observable_signals: + - "ipamd logs: 'Warm prefix target'" + non_effects: + - "Does NOT affect routing or iptables" + +- key_name: ENABLE_POD_ENI + source_file_path: pkg/ipamd/ipamd.go + parse_location: "tryEnableSecurityGroupsForPods() — utils.GetBoolAsStringEnvVar" + default_value: "false" + semantic_summary: > + Enables Security Groups for Pods (SGP). Attaches a trunk ENI to the node, + allowing branch ENIs to be created for pods with SecurityGroupPolicy CRD. + ONLY pods with vpc.amazonaws.com/pod-eni annotation get branch ENIs. + Unannotated pods are COMPLETELY UNAFFECTED. + preconditions: + - "Instance type must support trunk ENI (Nitro instances)" + - "SecurityGroupPolicy CRD must be installed" + - "VPC Resource Controller must be running in the cluster" + - "DISABLE_TCP_EARLY_DEMUX should be true for kubelet probes to work" + observable_signals: + - "ipamd logs: 'Trunk ENI attached'" + - "EC2 console: trunk ENI visible on instance" + - "Pod annotations: vpc.amazonaws.com/pod-eni present on SGP pods" + non_effects: + - "Does NOT affect pods without vpc.amazonaws.com/pod-eni annotation" + - "Does NOT create any default-deny behavior" + - "Does NOT change iptables rules for normal pods" + - "Does NOT affect kube-proxy or service routing" + +- key_name: AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG + source_file_path: pkg/ipamd/ipamd.go + parse_location: "nodeInit() — utils.GetBoolAsStringEnvVar" + default_value: "false" + semantic_summary: > + Enables custom networking via ENIConfig CRD. Secondary ENIs use subnet and + security groups from ENIConfig instead of primary ENI's. Pods get IPs from + the ENIConfig-specified subnet. + preconditions: + - "ENIConfig CRD must be created for each AZ/node" + - "Node must have label or annotation matching ENIConfig name" + observable_signals: + - "ipamd logs: 'Custom networking enabled'" + - "Pod IPs from different subnet than node primary IP" + non_effects: + - "Does NOT affect iptables SNAT rules (those use VPC CIDRs)" + - "Does NOT affect kube-proxy" + +- key_name: ENABLE_SUBNET_DISCOVERY + source_file_path: pkg/ipamd/ipamd.go + parse_location: "utils.GetBoolAsStringEnvVar" + default_value: "true" + semantic_summary: > + Enables enhanced subnet selection when creating ENIs. IPAMD discovers + subnets with available IPs instead of using only the primary ENI's subnet. + preconditions: [] + observable_signals: + - "ipamd logs: subnet discovery messages" + non_effects: + - "Does NOT affect routing or iptables" + +- key_name: DISABLE_NETWORK_RESOURCE_PROVISIONING + source_file_path: pkg/ipamd/ipamd.go + parse_location: "utils.GetBoolAsStringEnvVar" + default_value: "false" + semantic_summary: > + Disables ENI provisioning during pod initialization. Used when an external + controller manages ENI lifecycle. + preconditions: [] + observable_signals: + - "ipamd logs: 'Network resource provisioning disabled'" + non_effects: + - "Does NOT affect existing pod networking" + +- key_name: DISABLE_LEAKED_ENI_CLEANUP + source_file_path: pkg/ipamd/ipamd.go + parse_location: "utils.GetBoolAsStringEnvVar" + default_value: "false" + semantic_summary: > + Disables the background task that cleans up leaked ENIs (ENIs attached + to the instance but not tracked by IPAMD). + preconditions: [] + observable_signals: + - "ipamd logs: 'Leaked ENI cleanup disabled'" + non_effects: + - "Does NOT affect pod networking" + +- key_name: AWS_MANAGE_ENIS_NON_SCHEDULABLE + source_file_path: pkg/ipamd/ipamd.go + parse_location: "utils.GetBoolAsStringEnvVar" + default_value: "false" + semantic_summary: > + Controls whether IPAMD allocates/deallocates ENIs on non-schedulable + (cordoned) nodes. Default false means ENIs are not managed on cordoned nodes. + preconditions: [] + observable_signals: + - "ipamd logs: ENI management messages on cordoned nodes" + non_effects: + - "Does NOT affect pod networking on schedulable nodes" + +- key_name: ANNOTATE_POD_IP + source_file_path: pkg/ipamd/ipamd.go + parse_location: "utils.GetBoolAsStringEnvVar" + default_value: "false" + semantic_summary: > + When true, IPAMD annotates pods with their allocated IP address + (vpc.amazonaws.com/pod-ips). Helps with IP tracking and debugging. + preconditions: [] + observable_signals: + - "Pod annotations: vpc.amazonaws.com/pod-ips" + non_effects: + - "Does NOT affect networking behavior" + +- key_name: NETWORK_POLICY_ENFORCING_MODE + source_file_path: pkg/ipamd/ipamd.go + parse_location: "os.LookupEnv" + default_value: '""' + semantic_summary: > + Controls network policy enforcement mode. Passed to the CNI plugin + to coordinate with the network policy agent. + preconditions: + - "Network policy agent must be running (separate container in aws-node)" + observable_signals: + - "network-policy-agent logs" + - "/var/log/aws-routed-eni/network-policy-agent.log" + non_effects: + - "Does NOT affect pods without NetworkPolicy applied" + - "Does NOT affect SNAT or service routing" + +- key_name: ENABLE_IPv4 + source_file_path: pkg/ipamd/ipamd.go + parse_location: "utils.GetBoolAsStringEnvVar" + default_value: "true" + semantic_summary: "Enables IPv4 mode. Default and most common mode." + preconditions: [] + observable_signals: + - "Pod IPs are IPv4 addresses" + non_effects: [] + +- key_name: ENABLE_IPv6 + source_file_path: pkg/ipamd/ipamd.go + parse_location: "utils.GetBoolAsStringEnvVar" + default_value: "false" + semantic_summary: > + Enables IPv6 mode. Requires prefix delegation. No SNAT rules in IPv6 mode. + Mutually exclusive with IPv4 in current implementation. + preconditions: + - "ENABLE_PREFIX_DELEGATION must be true" + - "VPC must have IPv6 CIDR" + observable_signals: + - "Pod IPs are IPv6 addresses" + - "No AWS-SNAT-CHAIN rules in iptables" + non_effects: + - "No SNAT rules created" + - "No connmark rules created" + +- key_name: MANAGE_UNTAGGED_ENI + source_file_path: pkg/ipamd/ipamd.go + parse_location: "utils.GetBoolAsStringEnvVar" + default_value: "true" + semantic_summary: > + Controls whether IPAMD manages ENIs that don't have the + node.k8s.amazonaws.com/instance_id tag. + preconditions: [] + observable_signals: + - "ipamd logs: untagged ENI management messages" + non_effects: + - "Does NOT affect pod networking" + +- key_name: ENABLE_MULTI_NIC + source_file_path: pkg/ipamd/ipamd.go + parse_location: "utils.GetBoolAsStringEnvVar" + default_value: "false" + semantic_summary: > + Enables multi-NIC support. Configures ENIs on network cards > 0 + for pods requiring multi-NIC attachments. + preconditions: + - "Instance type must support multiple network cards" + observable_signals: + - "Multiple network cards visible in EC2 console" + non_effects: + - "Does NOT affect single-NIC pod networking" + + + +# ───────────────────────────────────────────────────────────────────────────── +# SECTION 2: Network Utils Environment Variables (pkg/networkutils/network.go) +# These control iptables rules, SNAT, connmark, MTU, and veth configuration +# ───────────────────────────────────────────────────────────────────────────── + +- key_name: AWS_VPC_K8S_CNI_EXTERNALSNAT + source_file_path: pkg/networkutils/network.go + parse_location: "useExternalSNAT() — os.Getenv" + default_value: "false" + semantic_summary: > + When true, disables the CNI's built-in SNAT rules (AWS-SNAT-CHAIN-0) and + off-VPC ip rules. Used when an external NAT gateway handles SNAT for + secondary ENI IPs. Does NOT affect kube-proxy or service routing. + preconditions: + - "External NAT gateway must be configured in VPC" + - "Route tables must route 0.0.0.0/0 to NAT gateway" + observable_signals: + - "iptables -t nat -L: AWS-SNAT-CHAIN-0 absent or empty" + - "ipamd logs: 'External SNAT enabled'" + non_effects: + - "Does NOT affect kube-proxy KUBE-SERVICES chain" + - "Does NOT affect pod-to-pod traffic within VPC" + - "Does NOT affect security groups" + - "Does NOT affect service routing (DNAT)" + +- key_name: AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS + source_file_path: pkg/networkutils/network.go + parse_location: "parseCIDRString(envExcludeSNATCIDRs)" + default_value: '""' + semantic_summary: > + Comma-separated list of IPv4 CIDRs to exclude from SNAT. Adds iptables + rules in AWS-SNAT-CHAIN-0 to skip SNAT for traffic to these CIDRs. + Used for VPN, DirectConnect, or peered VPC traffic. + preconditions: + - "AWS_VPC_K8S_CNI_EXTERNALSNAT must be false (otherwise no SNAT chain exists)" + observable_signals: + - "iptables -t nat -L AWS-SNAT-CHAIN-0: exclusion rules for specified CIDRs" + non_effects: + - "Does NOT affect kube-proxy rules" + - "Does NOT affect pod-to-pod traffic" + +- key_name: AWS_EXTERNAL_SERVICE_CIDRS + source_file_path: pkg/networkutils/network.go + parse_location: "parseCIDRString(envExternalServiceCIDRs)" + default_value: '""' + semantic_summary: > + Comma-separated list of IPv4 CIDRs that require routing lookup in the + main routing table. An ip rule is created for each CIDR at priority 1535. + preconditions: [] + observable_signals: + - "ip rule list: rules at priority 1535 for specified CIDRs" + non_effects: + - "Does NOT affect iptables rules" + +- key_name: AWS_VPC_K8S_CNI_RANDOMIZESNAT + source_file_path: pkg/networkutils/network.go + parse_location: "typeOfSNAT() — os.Getenv" + default_value: '"prng"' + semantic_summary: > + Controls SNAT port randomization. "hashrandom" adds --random flag, + "prng" adds --random-fully flag, anything else uses sequential. + Only affects the MASQUERADE rule in AWS-SNAT-CHAIN-0. + preconditions: + - "AWS_VPC_K8S_CNI_EXTERNALSNAT must be false" + observable_signals: + - "iptables -t nat -L AWS-SNAT-CHAIN-0: --random or --random-fully on MASQUERADE" + non_effects: + - "Does NOT affect kube-proxy SNAT" + - "Does NOT affect service routing" + +- key_name: AWS_VPC_CNI_NODE_PORT_SUPPORT + source_file_path: pkg/networkutils/network.go + parse_location: "nodePortSupportEnabled() — os.Getenv" + default_value: "true" + semantic_summary: > + Enables NodePort support by loosening RPF check and adding connmark + rules. Required for NodePort traffic to reach pods on secondary ENIs. + preconditions: [] + observable_signals: + - "sysctl net.ipv4.conf.eth0.rp_filter = 2 (loose mode)" + - "iptables -t nat -L AWS-CONNMARK-CHAIN-0: connmark rules present" + non_effects: + - "Does NOT affect ClusterIP service routing (that's kube-proxy)" + +- key_name: AWS_VPC_K8S_CNI_CONNMARK + source_file_path: pkg/networkutils/network.go + parse_location: "getConnmark() — os.Getenv" + default_value: "0x80" + semantic_summary: > + Connection mark used to mark traffic from primary ENI so return traffic + is forced out the same interface. Needed for NodePort DNAT + source-based + routing to work together. Mark space: CNI=0x80, kube-proxy=0x0000c000, + Calico=0xffff0000. + preconditions: [] + observable_signals: + - "iptables -t nat -L AWS-CONNMARK-CHAIN-0: CONNMARK rules with 0x80" + - "conntrack -L: entries with mark 0x80" + non_effects: + - "Does NOT affect kube-proxy's mark (0x0000c000)" + - "Does NOT affect Calico marks" + +- key_name: AWS_VPC_ENI_MTU + source_file_path: pkg/networkutils/network.go + parse_location: "GetEthernetMTU() — os.Getenv" + default_value: "9001" + semantic_summary: > + MTU for ENI interfaces. Range 576-9001 for IPv4, 1280-9001 for IPv6. + Applied to all ENIs attached by IPAMD. + preconditions: [] + observable_signals: + - "ip link show eth0: mtu value" + non_effects: + - "Does NOT affect pod veth MTU (that's POD_MTU)" + +- key_name: AWS_VPC_K8S_CNI_VETHPREFIX + source_file_path: pkg/networkutils/network.go + parse_location: "getVethPrefixName() — os.Getenv" + default_value: '"eni"' + semantic_summary: > + Prefix for host-side veth device names. Default "eni" creates devices + like "eni1234abcd". In SGP strict mode, branch ENI pods use "vlan" + prefix instead (hardcoded in pkg/sgpp/utils.go). + preconditions: [] + observable_signals: + - "ip link show: veth devices with specified prefix" + non_effects: + - "Does NOT affect routing or iptables rules" + +- key_name: ENABLE_V6_EGRESS + source_file_path: pkg/networkutils/network.go + parse_location: "ipV6EgressEnabled() — os.Getenv" + default_value: "false" + semantic_summary: > + Enables IPv6 egress support on IPv4 clusters. Configures egress-v6 + plugin for IPv6 outbound traffic. + preconditions: + - "Cluster must be IPv4 mode" + - "VPC must have IPv6 CIDR" + observable_signals: + - "egress-v6 plugin configuration in 10-aws.conflist" + non_effects: + - "Does NOT affect IPv4 SNAT rules" + +- key_name: ENABLE_V4_EGRESS + source_file_path: cmd/aws-vpc-cni/main.go + parse_location: "envEnIPv4Egress — utils.GetBoolAsStringEnvVar" + default_value: "true" + semantic_summary: > + Enables IPv4 egress support on IPv6 clusters. Configures egress-v4 + plugin for IPv4 outbound traffic from IPv6 pods. Default true means + IPv4 egress is enabled by default on IPv6 clusters. + preconditions: + - "Cluster must be IPv6 mode" + observable_signals: + - "egress-v4 plugin configuration in 10-aws.conflist" + - "169.254.172.0/22 IPAM subnet for egress-v4" + non_effects: + - "Does NOT affect IPv6 pod networking" + - "Does NOT affect IPv4 clusters (only relevant for IPv6 clusters)" + + +# ───────────────────────────────────────────────────────────────────────────── +# SECTION 3: CNI Plugin Environment Variables (cmd/aws-vpc-cni/main.go) +# These are read by the entrypoint script that generates 10-aws.conflist +# and are passed to the routed-eni-cni-plugin binary via the conflist +# ───────────────────────────────────────────────────────────────────────────── + +- key_name: POD_SECURITY_GROUP_ENFORCING_MODE + source_file_path: cmd/aws-vpc-cni/main.go + parse_location: "envPodSGEnforcingMode — written into 10-aws.conflist" + default_value: '"strict"' + semantic_summary: > + Controls how SGP pods' veth interfaces are named and whether they opt out + of SNAT/Calico. "strict" uses "vlan" prefix (opts out of SNAT + Calico). + "standard" uses normal "eni" prefix (SNAT + Calico apply). + CRITICAL: This ONLY affects pods with vpc.amazonaws.com/pod-eni annotation. + Unannotated pods are COMPLETELY UNAFFECTED by this setting. + preconditions: + - "ENABLE_POD_ENI must be true" + - "Pod must have vpc.amazonaws.com/pod-eni annotation" + - "Pod must have SecurityGroupPolicy CRD matching it" + observable_signals: + - "SGP pod veth: 'vlan' prefix (strict) or 'eni' prefix (standard)" + - "10-aws.conflist: podSGEnforcingMode field" + non_effects: + - "Does NOT create default-deny for unannotated pods" + - "Does NOT affect iptables rules for normal pods" + - "Does NOT affect kube-proxy or service routing" + - "Does NOT affect SNAT for normal pods" + - "Does NOT block any traffic for pods without pod-eni annotation" + repo_proof: + gate_code: "pkg/ipamd/rpc_handler.go — PodVlanId != 0 gate" + veth_logic: "pkg/sgpp/utils.go — BuildHostVethNamePrefix()" + cni_fork: "cmd/routed-eni-cni-plugin/cni.go — if r.PodVlanId != 0" + +- key_name: POD_MTU + source_file_path: cmd/aws-vpc-cni/main.go + parse_location: "envPodMTU — written into 10-aws.conflist" + default_value: "0 (uses ENI MTU)" + semantic_summary: > + MTU for pod veth interfaces. If 0, uses the ENI MTU value. + Written into 10-aws.conflist and applied by the CNI plugin. + preconditions: [] + observable_signals: + - "ip link show inside pod: mtu value on eth0" + non_effects: + - "Does NOT affect ENI MTU (that's AWS_VPC_ENI_MTU)" + +- key_name: ENABLE_BANDWIDTH_PLUGIN + source_file_path: cmd/aws-vpc-cni/main.go + parse_location: "envEnBandwidthPlugin — controls bandwidth plugin in conflist" + default_value: "false" + semantic_summary: > + Enables the bandwidth CNI plugin in the conflist chain. Allows + kubernetes.io/ingress-bandwidth and kubernetes.io/egress-bandwidth + annotations on pods for traffic shaping. + preconditions: [] + observable_signals: + - "10-aws.conflist: bandwidth plugin entry present" + non_effects: + - "Does NOT affect pods without bandwidth annotations" + +- key_name: IP_COOLDOWN_PERIOD + source_file_path: cmd/aws-vpc-cni/main.go + parse_location: "envIPCooldownPeriod — written into 10-aws.conflist" + default_value: "30" + semantic_summary: > + Seconds to wait before recycling a released IP address. Prevents + IP reuse conflicts during pod churn. + preconditions: [] + observable_signals: + - "10-aws.conflist: ipCooldownPeriod field" + non_effects: + - "Does NOT affect routing or iptables" + +- key_name: DISABLE_POD_V6 + source_file_path: cmd/aws-vpc-cni/main.go + parse_location: "envDisablePodV6 — written into 10-aws.conflist" + default_value: "false" + semantic_summary: > + Disables IPv6 inside pod network namespace. Sets + net.ipv6.conf.all.disable_ipv6=1 via sysctl plugin. + preconditions: [] + observable_signals: + - "Inside pod: sysctl net.ipv6.conf.all.disable_ipv6 = 1" + non_effects: + - "Does NOT affect host IPv6 settings" + +- key_name: ENABLE_MULTI_NIC + source_file_path: cmd/aws-vpc-cni/main.go + parse_location: "envEnableMultiNICSupport — written into 10-aws.conflist" + default_value: "false" + semantic_summary: > + Enables multi-NIC support in the CNI plugin. Allows pods to request + multiple network interfaces across different network cards. + preconditions: + - "Instance type must support multiple network cards" + observable_signals: + - "10-aws.conflist: enableMultiNICSupport field" + non_effects: + - "Does NOT affect single-NIC pods" + + +# ───────────────────────────────────────────────────────────────────────────── +# SECTION 4: Init Container Environment Variables (cmd/aws-vpc-cni-init/main.go) +# These are read by the aws-vpc-cni-init container during node bootstrap +# ───────────────────────────────────────────────────────────────────────────── + +- key_name: ENABLE_IMDS_ONLY_MODE + source_file_path: utils/constants.go + parse_location: "utils.EnvEnableImdsOnlyMode — utils.GetBoolAsStringEnvVar" + default_value: "false" + semantic_summary: > + When true, IPAMD skips EC2 API calls and uses only IMDS metadata to + discover ENI information. Also implicitly disables ENI provisioning + and leaked ENI cleanup. Used in environments where EC2 API access is + restricted or unavailable. + preconditions: + - "IMDS must be accessible from the node" + observable_signals: + - "ipamd logs: 'ENABLE_IMDS_ONLY_MODE is enabled, skipping EC2 API call'" + - "No EC2 DescribeNetworkInterfaces calls from IPAMD" + non_effects: + - "Does NOT disable pod networking" + - "Does NOT affect iptables rules" + - "Does NOT affect kube-proxy" + +- key_name: AWS_VPC_K8S_CNI_BACKING_STORE + source_file_path: pkg/ipamd/ipamd.go + parse_location: "dsBackingStorePath() — os.Getenv" + default_value: "/var/run/aws-node/ipam.json" + semantic_summary: > + Path where IPAMD persists its IP-to-container allocation state. + Used for crash recovery — on restart, IPAMD reads this file to + reconstruct its datastore without losing track of assigned IPs. + preconditions: [] + observable_signals: + - "File exists at specified path with JSON content" + non_effects: + - "Does NOT affect networking behavior" + - "Does NOT affect iptables rules" + +- key_name: MY_NODE_NAME + source_file_path: pkg/ipamd/ipamd.go + parse_location: "os.Getenv(envNodeName)" + default_value: '""' + semantic_summary: > + Kubernetes node name, set via Downward API in the aws-node DaemonSet. + Used by IPAMD to identify itself when querying the K8s API for node + status (e.g., checking if node is schedulable). + preconditions: + - "Must be set via Downward API (fieldRef: spec.nodeName)" + observable_signals: + - "aws-node pod env: MY_NODE_NAME=" + non_effects: + - "Does NOT affect networking behavior" + +- key_name: DISABLE_TCP_EARLY_DEMUX + source_file_path: cmd/aws-vpc-cni-init/main.go + parse_location: "configureSystemParams() — procSys.Set('net/ipv4/tcp_early_demux')" + default_value: "false" + semantic_summary: > + When true, sets net/ipv4/tcp_early_demux to 0. Required for SGP pods + in strict mode so kubelet health probes can reach pods on branch ENIs. + Without this, TCP early demux routes probe packets to the wrong interface. + preconditions: + - "Kernel must support tcp_early_demux sysctl" + - "Should be true when ENABLE_POD_ENI=true and using strict mode" + observable_signals: + - "sysctl net.ipv4.tcp_early_demux = 0" + - "init container logs: 'Updated net/ipv4/tcp_early_demux to 0'" + non_effects: + - "Does NOT affect pod networking directly" + - "Does NOT affect iptables rules" + - "Does NOT affect SNAT" + - "Minor performance impact on TCP connection setup for all pods on node" + + +# ───────────────────────────────────────────────────────────────────────────── +# SECTION 5: iptables Chain Ownership Map +# Defines which component owns which iptables chains +# ───────────────────────────────────────────────────────────────────────────── + +- key_name: "__IPTABLES_CHAIN_OWNERSHIP_MAP__" + source_file_path: pkg/networkutils/network.go + parse_location: "buildIptablesSNATRules() + buildIptablesConnmarkRules()" + semantic_summary: "Reference map of iptables chain ownership across components" + cni_owned_chains: + - chain: AWS-SNAT-CHAIN-0 + table: nat + hook: POSTROUTING + purpose: "SNAT for pod traffic leaving VPC" + created_by: "pkg/networkutils/network.go — buildIptablesSNATRules()" + skipped_when: "AWS_VPC_K8S_CNI_EXTERNALSNAT=true OR ENABLE_IPv6=true" + - chain: AWS-CONNMARK-CHAIN-0 + table: nat + hook: PREROUTING + purpose: "Mark traffic from primary ENI for NodePort return routing" + created_by: "pkg/networkutils/network.go — buildIptablesConnmarkRules()" + skipped_when: "ENABLE_IPv6=true" + mark_value: "0x80 (configurable via AWS_VPC_K8S_CNI_CONNMARK)" + kube_proxy_owned_chains: + - chain: KUBE-SERVICES + table: nat + hook: "PREROUTING + OUTPUT" + purpose: "Service ClusterIP DNAT — routes service VIPs to pod endpoints" + owner: "kube-proxy" + cni_involvement: "NONE — CNI never touches this chain" + - chain: "KUBE-SVC-*" + table: nat + purpose: "Per-service endpoint selection chains" + owner: "kube-proxy" + - chain: "KUBE-SEP-*" + table: nat + purpose: "Per-endpoint DNAT rules" + owner: "kube-proxy" + - chain: KUBE-POSTROUTING + table: nat + hook: POSTROUTING + purpose: "Masquerade for service traffic" + owner: "kube-proxy" + mark_value: "0x0000c000" + mark_space: + cni: "0x80" + kube_proxy: "0x0000c000" + calico: "0xffff0000" diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/cni-semantics/guardrail-templates.yaml b/mcp/aws-eks-node-diagnostics-mcp/sops/cni-semantics/guardrail-templates.yaml new file mode 100644 index 0000000..3bb5cdc --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/cni-semantics/guardrail-templates.yaml @@ -0,0 +1,708 @@ +# ============================================================================= +# VPC CNI Guardrail Templates +# Source of truth: amazon-vpc-cni-k8s (github.com/aws/amazon-vpc-cni-k8s) +# Each guardrail prevents a specific class of hallucination/misdiagnosis +# ============================================================================= + +guardrails: + + # ───────────────────────────────────────────────────────────────────────── + # G1: Security Groups for Pods (SGP) / pod-eni + # ───────────────────────────────────────────────────────────────────────── + - id: G1-sgp-pod-eni + category: security_groups_for_pods + common_mistaken_interpretation: > + "podSGEnforcingMode: strict creates a default-deny posture for all pods, + blocking traffic to pods that don't have explicit security group rules." + repo_verified_correct_interpretation: > + podSGEnforcingMode ONLY affects pods with vpc.amazonaws.com/pod-eni + annotation (assigned by VPC Resource Controller when a SecurityGroupPolicy + CRD matches the pod). The PodVlanId != 0 gate in rpc_handler.go is the + sole branch point — pods without this annotation get VlanId=0 and go + through SetupPodNetwork(), completely bypassing all SGP logic. Strict + mode only changes the veth prefix from "eni" to "vlan" for annotated + pods, which opts them out of SNAT and Calico management. + required_evidence_to_blame: + - "Pod MUST have vpc.amazonaws.com/pod-eni annotation" + - "SecurityGroupPolicy CRD MUST exist matching the pod" + - "ENABLE_POD_ENI MUST be true on the node" + - "Trunk ENI MUST be attached to the node" + explicit_disqualifiers: + - "If pod lacks vpc.amazonaws.com/pod-eni annotation → SGP is NOT involved" + - "If ENABLE_POD_ENI is false → SGP is completely disabled" + - "If kube-proxy KUBE-SERVICES chain is empty → that's a kube-proxy issue, not SGP" + - "If service ClusterIP is unreachable → check kube-proxy first, not SGP" + repo_citation: + gate: "pkg/ipamd/rpc_handler.go lines 95-180 — PodVlanId != 0 check" + veth_logic: "pkg/sgpp/utils.go — BuildHostVethNamePrefix()" + constants: "pkg/sgpp/constants.go — DefaultEnforcingMode = strict" + cni_fork: "cmd/routed-eni-cni-plugin/cni.go line 270 — if r.PodVlanId != 0" + + # ───────────────────────────────────────────────────────────────────────── + # G2: SNAT / externalSNAT + # ───────────────────────────────────────────────────────────────────────── + - id: G2-snat-external-snat + category: snat_configuration + common_mistaken_interpretation: > + "AWS_VPC_K8S_CNI_EXTERNALSNAT=true disables all NAT on the node, + breaking service routing and pod-to-external connectivity." + repo_verified_correct_interpretation: > + externalSNAT only controls the CNI's AWS-SNAT-CHAIN-0 in the nat + POSTROUTING hook. When true, the CNI does not create SNAT rules for + pod traffic leaving the VPC. It does NOT affect kube-proxy's + KUBE-POSTROUTING chain (service masquerade) or KUBE-SERVICES chain + (ClusterIP DNAT). Pod-to-pod within VPC is unaffected. An external + NAT gateway must handle outbound SNAT instead. + required_evidence_to_blame: + - "Pod traffic to external (non-VPC) destinations is failing" + - "iptables -t nat -L shows AWS-SNAT-CHAIN-0 is empty or absent" + - "No NAT gateway in the VPC route table for 0.0.0.0/0" + explicit_disqualifiers: + - "If service ClusterIP routing is broken → that's kube-proxy, not SNAT" + - "If pod-to-pod within VPC fails → SNAT is not involved" + - "If ENABLE_IPv6=true → no SNAT rules exist regardless of this setting" + repo_citation: + env_def: "pkg/networkutils/network.go — envExternalSNAT constant" + skip_logic: "pkg/networkutils/network.go — useExternalSNAT() function" + chain_build: "pkg/networkutils/network.go — buildIptablesSNATRules()" + + # ───────────────────────────────────────────────────────────────────────── + # G3: Custom Networking (ENIConfig) + # ───────────────────────────────────────────────────────────────────────── + - id: G3-custom-networking + category: custom_networking + common_mistaken_interpretation: > + "AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG=true changes the node's primary + ENI subnet and affects all traffic routing on the node." + repo_verified_correct_interpretation: > + Custom networking only affects SECONDARY ENIs. The primary ENI retains + its original subnet and security groups. Pods get IPs from the + ENIConfig-specified subnet, but the node's primary IP and host + networking are unchanged. SNAT rules still use VPC CIDRs. + required_evidence_to_blame: + - "Pod IPs are from unexpected subnet" + - "ENIConfig CRD exists for the node's AZ" + - "AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG=true in aws-node env" + explicit_disqualifiers: + - "If node primary IP is affected → not custom networking" + - "If iptables rules are wrong → check SNAT config, not custom networking" + - "If ENIConfig CRD doesn't exist → custom networking has no effect" + repo_citation: + env_def: "pkg/ipamd/ipamd.go — envCustomNetworkCfg" + init_logic: "pkg/ipamd/ipamd.go — nodeInit() reads ENIConfig" + + # ───────────────────────────────────────────────────────────────────────── + # G4: Prefix Delegation / IPAMD Warm Pool + # ───────────────────────────────────────────────────────────────────────── + - id: G4-prefix-delegation + category: prefix_delegation + common_mistaken_interpretation: > + "ENABLE_PREFIX_DELEGATION=true changes how pods route traffic or + affects iptables rules and security groups." + repo_verified_correct_interpretation: > + Prefix delegation only changes IP allocation strategy — /28 prefixes + (16 IPs each) instead of individual secondary IPs. This increases + max pods per node but does NOT change routing, iptables, SNAT, or + security groups. The warm pool targets (WARM_IP_TARGET, + WARM_PREFIX_TARGET, MINIMUM_IP_TARGET) control pre-allocation only. + required_evidence_to_blame: + - "IP allocation failures (insufficient IPs)" + - "Max pods limit reached unexpectedly" + - "ENI/prefix attachment errors in ipamd logs" + explicit_disqualifiers: + - "If routing or iptables is broken → prefix delegation is not involved" + - "If SNAT is wrong → check SNAT config, not PD" + - "If service routing fails → check kube-proxy, not PD" + repo_citation: + env_def: "pkg/ipamd/ipamd.go — usePrefixDelegation()" + allocation: "pkg/ipamd/ipamd.go — tryAllocateENI() and assignPodIPv4AddressUnsafe()" + + # ───────────────────────────────────────────────────────────────────────── + # G5: Node-Local vs Pod-Level Routing + # ───────────────────────────────────────────────────────────────────────── + - id: G5-node-vs-pod-routing + category: routing_scope + common_mistaken_interpretation: > + "VPC CNI configuration changes affect cluster-wide routing behavior + and can break cross-node pod communication." + repo_verified_correct_interpretation: > + VPC CNI configuration is strictly node-local. Each node runs its own + IPAMD and applies its own iptables rules. Pod-to-pod across nodes + uses VPC routing (the VPC route table has routes for each node's + secondary IPs pointing to the node's ENI). Cross-node issues are + VPC route table or security group problems, not CNI config problems. + The CNI only manages: (1) ip rules for per-ENI route tables, + (2) AWS-SNAT-CHAIN-0 and AWS-CONNMARK-CHAIN-0 in iptables, + (3) veth pairs between host and pod namespaces. + required_evidence_to_blame: + - "Issue is isolated to pods on a SINGLE node" + - "iptables or ip rules on that specific node are wrong" + - "ipamd logs on that node show errors" + explicit_disqualifiers: + - "If issue affects pods across multiple nodes → VPC routing, not CNI" + - "If issue is cross-node pod-to-pod → check VPC route tables and SGs" + - "If issue is service routing → check kube-proxy on each node" + repo_citation: + ip_rules: "pkg/networkutils/network.go — SetupENINetwork()" + host_setup: "pkg/networkutils/network.go — SetupHostNetwork()" + + # ───────────────────────────────────────────────────────────────────────── + # G6: Service Routing vs Pod Routing (kube-proxy ownership) + # ───────────────────────────────────────────────────────────────────────── + - id: G6-service-vs-pod-routing + category: kube_proxy_ownership + common_mistaken_interpretation: > + "VPC CNI manages service ClusterIP routing and DNAT. If a service + is unreachable, the CNI configuration must be wrong." + repo_verified_correct_interpretation: > + Service routing (ClusterIP DNAT, NodePort DNAT, LoadBalancer) is + 100% owned by kube-proxy via KUBE-SERVICES, KUBE-SVC-*, KUBE-SEP-* + chains. The CNI has ZERO involvement in service routing. The CNI + only handles: pod IP assignment, per-ENI routing tables, SNAT for + off-VPC traffic, and connmark for NodePort return path. If + KUBE-SERVICES chain is empty, kube-proxy is broken — not the CNI. + required_evidence_to_blame: + - "Pod-to-pod direct IP connectivity fails (not via service)" + - "SNAT is wrong for external traffic" + - "ip rules or per-ENI route tables are misconfigured" + explicit_disqualifiers: + - "If service ClusterIP unreachable → kube-proxy issue" + - "If KUBE-SERVICES chain empty → kube-proxy not running/syncing" + - "If endpoints exist but DNAT fails → kube-proxy iptables issue" + - "If NodePort works but ClusterIP doesn't → kube-proxy issue" + repo_citation: + cni_chains: "pkg/networkutils/network.go — only creates AWS-SNAT-CHAIN-0 and AWS-CONNMARK-CHAIN-0" + no_kube_svc: "grep -r 'KUBE-SERVICES' amazon-vpc-cni-k8s/ returns ZERO results" + + # ───────────────────────────────────────────────────────────────────────── + # G7: iptables Chain Ownership (CNI vs kube-proxy) + # ───────────────────────────────────────────────────────────────────────── + - id: G7-chain-ownership + category: iptables_chain_ownership + common_mistaken_interpretation: > + "The VPC CNI manages all iptables rules on the node. If any iptables + chain is broken, the CNI must be reconfigured or restarted." + repo_verified_correct_interpretation: > + The CNI owns EXACTLY two custom chains: AWS-SNAT-CHAIN-0 (nat/POSTROUTING) + and AWS-CONNMARK-CHAIN-0 (nat/PREROUTING). Everything else in iptables + is owned by other components: + - kube-proxy: KUBE-SERVICES, KUBE-SVC-*, KUBE-SEP-*, KUBE-POSTROUTING, KUBE-MARK-MASQ + - Calico: cali-*, felix-* + - Network Policy Agent: AWSCNI-NTWK-POLICY-* + Mark space is partitioned: CNI=0x80, kube-proxy=0x0000c000, Calico=0xffff0000. + required_evidence_to_blame: + - "AWS-SNAT-CHAIN-0 or AWS-CONNMARK-CHAIN-0 rules are wrong" + - "Issue is specifically with SNAT or connmark behavior" + explicit_disqualifiers: + - "If KUBE-* chains are broken → kube-proxy issue" + - "If cali-* chains are broken → Calico issue" + - "If AWSCNI-NTWK-POLICY-* chains are wrong → network policy agent issue" + repo_citation: + snat_chain: "pkg/networkutils/network.go — buildIptablesSNATRules() creates AWS-SNAT-CHAIN-0" + connmark_chain: "pkg/networkutils/network.go — buildIptablesConnmarkRules() creates AWS-CONNMARK-CHAIN-0" + mark_comment: "pkg/networkutils/network.go line 121 — 'kube-proxy uses 0x0000c000'" + + # ───────────────────────────────────────────────────────────────────────── + # G8: IPv6 Mode — No SNAT/Connmark + # ───────────────────────────────────────────────────────────────────────── + - id: G8-ipv6-no-snat + category: ipv6_mode + common_mistaken_interpretation: > + "IPv6 mode has the same SNAT and connmark rules as IPv4, just with + ip6tables instead of iptables." + repo_verified_correct_interpretation: > + In IPv6 mode, the CNI creates NO SNAT rules and NO connmark rules. + The updateHostIptablesRules() function returns immediately when + v6Enabled=true. Traffic enters and exits from the ENI it came from. + This is a fundamental architectural difference from IPv4 mode. + required_evidence_to_blame: + - "N/A — there are no SNAT/connmark rules to blame in IPv6 mode" + explicit_disqualifiers: + - "If ENABLE_IPv6=true → AWS-SNAT-CHAIN-0 and AWS-CONNMARK-CHAIN-0 should NOT exist" + - "If someone reports missing SNAT rules in IPv6 mode → that's expected behavior" + repo_citation: + early_return: "pkg/networkutils/network.go line 441 — 'if v6Enabled { return nil }'" + + # ───────────────────────────────────────────────────────────────────────── + # G9: Network Policy Enforcing Mode (NOT the same as SGP enforcing mode) + # ───────────────────────────────────────────────────────────────────────── + - id: G9-network-policy-enforcing-mode + category: network_policy + common_mistaken_interpretation: > + "NETWORK_POLICY_ENFORCING_MODE=strict is the same as + POD_SECURITY_GROUP_ENFORCING_MODE=strict — they both block traffic." + repo_verified_correct_interpretation: > + These are TWO COMPLETELY DIFFERENT settings. NETWORK_POLICY_ENFORCING_MODE + controls Kubernetes NetworkPolicy enforcement via the aws-network-policy-agent + (eBPF-based, separate binary). "strict" means default-deny until a + NetworkPolicy explicitly allows traffic. POD_SECURITY_GROUP_ENFORCING_MODE + controls veth naming for SGP-annotated pods only. They share the word + "enforcing" but have zero overlap in code paths. + required_evidence_to_blame: + - "NETWORK_POLICY_ENFORCING_MODE=strict is set" + - "Pod has NO NetworkPolicy allowing its traffic" + - "aws-network-policy-agent is running on the node" + explicit_disqualifiers: + - "If no NetworkPolicy CRDs exist → network policy agent allows all traffic" + - "If aws-network-policy-agent is not running → no enforcement happens" + - "If pod is on GPU instance, Fargate, Windows, or hostNetwork=true → not supported" + - "Do NOT confuse with POD_SECURITY_GROUP_ENFORCING_MODE" + repo_citation: + env_def: "pkg/ipamd/ipamd.go — envNetworkPolicyEnforcingMode" + agent_binary: "Separate binary: aws-network-policy-agent (NOT in VPC CNI repo)" + grpc_socket: "/var/run/aws-node/networkpolicy/aws-node.sock" + + # ───────────────────────────────────────────────────────────────────────── + # G10: nm-cloud-setup incompatibility + # ───────────────────────────────────────────────────────────────────────── + - id: G10-nm-cloud-setup + category: os_compatibility + common_mistaken_interpretation: > + "nm-cloud-setup is a normal NetworkManager service that helps with + cloud networking. It should be left running." + repo_verified_correct_interpretation: > + nm-cloud-setup (NetworkManager-cloud-setup) is INCOMPATIBLE with VPC CNI. + It creates routing tables 30200 and 30400 that overwrite the ip rules + VPC CNI installs for per-ENI policy routing. This breaks pod networking + on secondary ENIs. Known to affect RHEL 8 AMIs. Must be disabled. + required_evidence_to_blame: + - "ip rule list shows rules referencing table 30200 or 30400" + - "systemctl status nm-cloud-setup.service shows active" + - "Pod networking on secondary ENIs is broken" + explicit_disqualifiers: + - "If table 30200/30400 not present → nm-cloud-setup is not the issue" + - "If only primary ENI pods affected → different issue" + repo_citation: + known_issue: "README.md — nm-cloud-setup incompatibility documented" + + # ───────────────────────────────────────────────────────────────────────── + # G11: iptables vs nftables mode detection + # ───────────────────────────────────────────────────────────────────────── + - id: G11-iptables-nftables + category: os_compatibility + common_mistaken_interpretation: > + "Missing iptables rules means VPC CNI is broken or not running." + repo_verified_correct_interpretation: > + If the host OS uses nftables (RHEL 8.x+, Ubuntu 21.x+) but VPC CNI + uses iptables-legacy mode, rules created by the CNI are invisible to + nft commands and vice versa. In current VPC CNI versions (v1.13.1+), + the ENABLE_NFTABLES env var has been REMOVED — the CNI auto-detects + iptables mode from kubelet. The env var no longer exists in the codebase. + For older versions, manual configuration was required. + required_evidence_to_blame: + - "iptables-save shows no AWS-SNAT-CHAIN-0 but nft list ruleset does" + - "Host OS is nftables-based (check: readlink /usr/sbin/iptables)" + - "VPC CNI version < 1.13.1 (older versions lacked auto-detection)" + explicit_disqualifiers: + - "If iptables-save shows CNI chains → iptables mode is working correctly" + - "If VPC CNI >= 1.13.1 → auto-detection should handle this" + repo_citation: + env_def: "ENABLE_NFTABLES has been REMOVED from pkg/networkutils/network.go — auto-detection replaced it" + + # ───────────────────────────────────────────────────────────────────────── + # G12: IP cooldown period — transient errors are normal + # ───────────────────────────────────────────────────────────────────────── + - id: G12-ip-cooldown-transient + category: ip_management + common_mistaken_interpretation: > + "'IP not in datastore' or 'no available IPs' errors mean IPAMD is + broken and needs to be restarted." + repo_verified_correct_interpretation: > + After pod deletion, VPC CNI holds the IP in a cooldown cache + (IP_COOLDOWN_PERIOD, default 30s) before returning it to the warm pool. + During this window, transient "IP not in datastore" or "no available IP" + messages are NORMAL and self-resolving. The cooldown exists to let + kube-proxy finish updating iptables rules for the deleted pod. + Additionally, IPAMD has a reconciliation cooldown cache (60s) and + insufficient CIDR cooldown (120s) that cause temporary pauses. + required_evidence_to_blame: + - "Errors persist for > 2 minutes (beyond all cooldown windows)" + - "ipamd logs show repeated failures, not just transient messages" + - "New pods stuck in ContainerCreating for > 5 minutes" + explicit_disqualifiers: + - "If errors last < 30 seconds → normal cooldown behavior" + - "If errors last 30-120 seconds → may be reconciliation cooldown" + - "Do NOT restart aws-node for transient cooldown messages" + repo_citation: + cooldown: "cmd/aws-vpc-cni/main.go — envIPCooldownPeriod default 30" + reconcile_cache: "pkg/ipamd/ipamd.go — reconcileCooldownCache 60s" + insufficient_cidr: "pkg/ipamd/ipamd.go — insufficientCIDRCooldown 120s" + + # ───────────────────────────────────────────────────────────────────────── + # G13: Warm pool over-provisioning → EC2 API throttling + # ───────────────────────────────────────────────────────────────────────── + - id: G13-warm-pool-ec2-throttling + category: ip_management + common_mistaken_interpretation: > + "Setting high WARM_IP_TARGET or WARM_ENI_TARGET values ensures pods + always get IPs quickly with no downside." + repo_verified_correct_interpretation: > + High warm pool targets cause frequent EC2 API calls (CreateNetworkInterface, + AssignPrivateIpAddresses, AttachNetworkInterface). EC2 API throttling is + per-account per-region, so aggressive warm pool settings on many nodes + can exhaust the API quota for ALL instances in the account. This prevents + new ENIs/IPs from being attached to ANY node. Default WARM_ENI_TARGET=1 + is recommended for most clusters. WARM_IP_TARGET takes precedence over + WARM_ENI_TARGET. WARM_PREFIX_TARGET is overridden by WARM_IP_TARGET. + required_evidence_to_blame: + - "ipamd logs show 'Client.RequestLimitExceeded' or 'Throttling' errors" + - "Multiple nodes simultaneously failing to attach ENIs" + - "WARM_IP_TARGET or WARM_ENI_TARGET set to high values" + explicit_disqualifiers: + - "If only one node affected → likely not throttling (check ENI limits)" + - "If ipamd logs show 'InsufficientCidrBlocks' → subnet exhaustion, not throttling" + repo_citation: + warm_check: "pkg/ipamd/ipamd.go — nodeIPPoolReconcile() every 60s" + pool_monitor: "pkg/ipamd/ipamd.go — ipPoolMonitorInterval/2 = 2.5s" + precedence: "pkg/ipamd/ipamd.go — WARM_IP_TARGET overrides WARM_ENI_TARGET" + + # ───────────────────────────────────────────────────────────────────────── + # G14: iptables FORWARD policy must be ACCEPT + # ───────────────────────────────────────────────────────────────────────── + - id: G14-forward-policy-accept + category: os_compatibility + common_mistaken_interpretation: > + "iptables FORWARD DROP is a security best practice and should be + kept on EKS nodes." + repo_verified_correct_interpretation: > + VPC CNI requires iptables FORWARD policy to be ACCEPT. Pod-to-pod + traffic traverses the FORWARD chain via veth pairs. Custom AMIs + often set FORWARD to DROP as a hardening measure, which breaks all + pod networking. The EKS-optimized AMI sets FORWARD ACCEPT by default. + Fix: add "ExecStartPre=/sbin/iptables -P FORWARD ACCEPT" to kubelet.service. + required_evidence_to_blame: + - "iptables -L FORWARD shows policy DROP" + - "Pod-to-pod traffic fails on the node" + - "Custom AMI is being used" + explicit_disqualifiers: + - "If FORWARD policy is ACCEPT → this is not the issue" + - "If using EKS-optimized AMI → FORWARD is already ACCEPT" + repo_citation: + init_container: "cmd/aws-vpc-cni-init/main.go — sets FORWARD ACCEPT" + + # ───────────────────────────────────────────────────────────────────────── + # G15: systemd-udev MACAddressPolicy (Ubuntu 22.04+) + # ───────────────────────────────────────────────────────────────────────── + - id: G15-macaddress-policy + category: os_compatibility + common_mistaken_interpretation: > + "MACAddressPolicy=persistent is a standard systemd setting and + should not affect container networking." + repo_verified_correct_interpretation: > + On Ubuntu 22.04+ and other systemd-based distros, MACAddressPolicy=persistent + in /usr/lib/systemd/network/99-default.link can change the MAC address of + host-side veth interfaces after they are moved to the host namespace. This + breaks the static ARP entry inside pods (169.254.1.1 → original MAC). + Fix: set MACAddressPolicy=none in the link file. + required_evidence_to_blame: + - "Ubuntu 22.04+ or similar systemd-based distro" + - "Pod networking intermittently fails after veth creation" + - "ARP table inside pod shows stale MAC for 169.254.1.1" + explicit_disqualifiers: + - "If Amazon Linux 2/2023 → this issue does not apply" + - "If MACAddressPolicy=none already set → not the issue" + repo_citation: + known_issue: "README.md — MACAddressPolicy documented as known issue" + + # ───────────────────────────────────────────────────────────────────────── + # G16: Conntrack table exhaustion vs kube-proxy failure + # ───────────────────────────────────────────────────────────────────────── + - id: G16-conntrack-exhaustion + category: conntrack + common_mistaken_interpretation: > + "'nf_conntrack: table full' means kube-proxy is broken or VPC CNI + is misconfigured." + repo_verified_correct_interpretation: > + Conntrack table exhaustion is a kernel resource issue, not a kube-proxy + or CNI bug. Each connection (TCP, UDP, ICMP) consumes a conntrack entry + (~300 bytes). High-traffic nodes (ingress controllers, load balancers) + exhaust the table first. Fix: increase nf_conntrack_max via kube-proxy + conntrack.min/conntrack.maxPerCore settings. The CNI's connmark (0x80) + and kube-proxy's mark (0x4000) both create conntrack entries but are + independent systems. + required_evidence_to_blame: + - "dmesg shows 'nf_conntrack: table full, dropping packet'" + - "sysctl net.netfilter.nf_conntrack_count near nf_conntrack_max" + - "High connection rate on the node" + explicit_disqualifiers: + - "If conntrack table is not full → different issue" + - "Do NOT blame VPC CNI configuration for conntrack exhaustion" + - "Do NOT blame kube-proxy binary for conntrack exhaustion" + repo_citation: + connmark: "pkg/networkutils/network.go — defaultConnmark = 0x80" + kp_mark: "pkg/networkutils/network.go line 121 — kube-proxy 0x0000c000" + + # ───────────────────────────────────────────────────────────────────────── + # G17: kube-proxy version skew + # ───────────────────────────────────────────────────────────────────────── + - id: G17-kube-proxy-version-skew + category: kube_proxy + common_mistaken_interpretation: > + "kube-proxy version doesn't matter as long as it's running." + repo_verified_correct_interpretation: > + kube-proxy must be within 1 minor version of the cluster control plane. + Version skew beyond this can cause service routing failures due to API + incompatibilities. After cluster upgrade, the kube-proxy add-on must be + updated to match. kube-proxy is NOT part of VPC CNI — it's a separate + Kubernetes component managed as an EKS add-on. + required_evidence_to_blame: + - "kube-proxy version is > 1 minor version behind control plane" + - "Service routing failures after cluster upgrade" + explicit_disqualifiers: + - "If kube-proxy version matches control plane → not a skew issue" + - "Do NOT blame VPC CNI for kube-proxy version skew" + repo_citation: + not_in_cni: "kube-proxy is NOT part of the VPC CNI codebase" + + # ───────────────────────────────────────────────────────────────────────── + # G18: kube-proxy static stability during API server outage + # ───────────────────────────────────────────────────────────────────────── + - id: G18-kube-proxy-static-stability + category: kube_proxy + common_mistaken_interpretation: > + "If kube-proxy can't reach the API server, all service routing breaks." + repo_verified_correct_interpretation: > + kube-proxy has static stability — during API server disconnections, + existing iptables/IPVS rules continue to function. In-cluster service + routing remains available. kube-proxy pods continue running. Only NEW + services/endpoints will not be reflected until API server connectivity + is restored. Do NOT restart kube-proxy during API server outages. + required_evidence_to_blame: + - "New services created after API server disconnect are unreachable" + - "kube-proxy logs show API server connection errors" + explicit_disqualifiers: + - "If existing services work → static stability is functioning correctly" + - "Do NOT restart kube-proxy during API server outages" + repo_citation: + not_in_cni: "kube-proxy static stability is a Kubernetes feature, not VPC CNI" + + # ───────────────────────────────────────────────────────────────────────── + # G19: kube-proxy IPVS mode — missing iptables chains are expected + # ───────────────────────────────────────────────────────────────────────── + - id: G19-kube-proxy-ipvs + category: kube_proxy + common_mistaken_interpretation: > + "Missing KUBE-SVC-* iptables chains means kube-proxy is broken." + repo_verified_correct_interpretation: > + In IPVS mode, kube-proxy uses kernel IPVS hash tables instead of + iptables chains for service routing. KUBE-SVC-* and KUBE-SEP-* chains + will NOT exist — this is EXPECTED. Validate with "ipvsadm -L" instead + of iptables-save. KUBE-SERVICES chain may still exist as a fallback + but will have minimal rules. Required kernel modules: ip_vs, ip_vs_rr, + ip_vs_wrr, ip_vs_sh, nf_conntrack. + required_evidence_to_blame: + - "kube-proxy is in IPVS mode AND ipvsadm -L shows no entries" + - "Required kernel modules are not loaded" + explicit_disqualifiers: + - "If kube-proxy is in IPVS mode → missing KUBE-SVC chains is NORMAL" + - "Do NOT flag missing iptables chains as broken in IPVS mode" + repo_citation: + not_in_cni: "kube-proxy IPVS mode is a Kubernetes feature, not VPC CNI" + + # ───────────────────────────────────────────────────────────────────────── + # G20: DISABLE_NETWORK_RESOURCE_PROVISIONING — does NOT break networking + # ───────────────────────────────────────────────────────────────────────── + - id: G20-disable-network-resource-provisioning + category: ip_management + common_mistaken_interpretation: > + "DISABLE_NETWORK_RESOURCE_PROVISIONING=true disables all networking + on the node, breaking pod connectivity." + repo_verified_correct_interpretation: > + This setting only disables ENI provisioning during pod initialization. + It's used when an external controller (like VPC Resource Controller) + manages ENI lifecycle. Existing pod networking is UNAFFECTED. The name + is misleading — it disables resource PROVISIONING, not networking itself. + required_evidence_to_blame: + - "New pods cannot get IPs AND no external controller is managing ENIs" + explicit_disqualifiers: + - "If existing pods work fine → this setting is not breaking networking" + - "If an external ENI controller is running → this is expected configuration" + repo_citation: + env_def: "pkg/ipamd/ipamd.go — envDisableNetworkResourceProvisioning" + + # ───────────────────────────────────────────────────────────────────────── + # G21: WARM_IP_TARGET confused with max pods limit + # ───────────────────────────────────────────────────────────────────────── + - id: G21-warm-ip-vs-max-pods + category: ip_management + common_mistaken_interpretation: > + "WARM_IP_TARGET limits the maximum number of pods on the node." + repo_verified_correct_interpretation: > + WARM_IP_TARGET controls how many FREE IPs to keep in the warm pool — + it does NOT limit max pods. Max pods is determined by the kubelet + --max-pods flag (set by the EKS bootstrap script based on instance + type ENI/IP limits). IPAMD checks: if totalIPs >= maxPods, pool is + NOT low (prevents over-allocation). But WARM_IP_TARGET itself is + purely about pre-allocation speed, not capacity limits. + required_evidence_to_blame: + - "Pods stuck in ContainerCreating with 'no available IPs'" + - "WARM_IP_TARGET is set too low for pod churn rate" + explicit_disqualifiers: + - "If max pods limit reached → that's kubelet --max-pods, not WARM_IP_TARGET" + - "WARM_IP_TARGET does NOT cap the number of running pods" + repo_citation: + max_pods_check: "pkg/ipamd/ipamd.go — if totalIPs >= maxPods, pool not low" + warm_target: "pkg/ipamd/ipamd.go — getWarmIPTarget()" + + # ───────────────────────────────────────────────────────────────────────── + # G22: AWS_VPC_ENI_MTU vs POD_MTU — two different settings + # ───────────────────────────────────────────────────────────────────────── + - id: G22-eni-mtu-vs-pod-mtu + category: mtu + common_mistaken_interpretation: > + "AWS_VPC_ENI_MTU controls the MTU inside pods." + repo_verified_correct_interpretation: > + AWS_VPC_ENI_MTU (default 9001) controls the MTU of ENI interfaces on + the host (eth0, eth1, etc.). POD_MTU (default 0 = use ENI MTU) controls + the MTU of veth interfaces inside pods. They are set in different places: + AWS_VPC_ENI_MTU in pkg/networkutils/network.go, POD_MTU in + cmd/aws-vpc-cni/main.go written into 10-aws.conflist. If POD_MTU=0, + pods inherit the ENI MTU value. + required_evidence_to_blame: + - "MTU mismatch between host ENI and pod veth" + - "Packet fragmentation or PMTUD failures" + explicit_disqualifiers: + - "If POD_MTU=0 → pods use ENI MTU, no mismatch possible" + - "Do NOT confuse AWS_VPC_ENI_MTU with POD_MTU" + repo_citation: + eni_mtu: "pkg/networkutils/network.go — GetEthernetMTU()" + pod_mtu: "cmd/aws-vpc-cni/main.go — envPodMTU" + + # ───────────────────────────────────────────────────────────────────────── + # G23: Empty per-ENI route table is normal with multiple ENIs + # ───────────────────────────────────────────────────────────────────────── + - id: G23-empty-route-table-multi-eni + category: routing + common_mistaken_interpretation: > + "An empty or minimal main route table means routing is broken." + repo_verified_correct_interpretation: > + On multi-ENI EKS nodes, the main route table may appear empty or have + no default gateway. This is NORMAL. VPC CNI creates per-ENI route tables + (table 2 for eth1, table 3 for eth2, etc.) with policy routing rules + (ip rule from lookup ). Pod traffic uses these per-ENI + tables, not the main table. The main table is only used for host traffic. + required_evidence_to_blame: + - "ip rule list shows NO per-ENI rules (missing 'from lookup
')" + - "Per-ENI route tables are actually empty (ip route show table 2)" + explicit_disqualifiers: + - "If per-ENI route tables exist and have routes → routing is working" + - "Do NOT flag empty main route table as broken on multi-ENI nodes" + repo_citation: + setup_eni: "pkg/networkutils/network.go — SetupENINetwork()" + rule_priority: "pkg/networkutils/network.go — toContainerRulePriority=512, fromContainerRulePriority=1536" + + # ───────────────────────────────────────────────────────────────────────── + # G24: IPv6 egress plugin — separate from main CNI SNAT + # ───────────────────────────────────────────────────────────────────────── + - id: G24-ipv6-egress-plugin + category: egress + common_mistaken_interpretation: > + "ENABLE_V6_EGRESS changes the main CNI SNAT behavior." + repo_verified_correct_interpretation: > + The egress-v6 plugin is a SEPARATE CNI plugin in the conflist chain. + It creates per-pod SNAT chains (CNI-E6-) for IPv6 egress + on IPv4 clusters. It does NOT modify AWS-SNAT-CHAIN-0 or any main CNI + chains. The egress plugin has its own randomizeSNAT setting and its own + iptables rules in ip6tables (not iptables). + required_evidence_to_blame: + - "ENABLE_V6_EGRESS=true AND IPv6 egress traffic fails" + - "ip6tables shows missing CNI-E6-* chains" + explicit_disqualifiers: + - "If IPv4 SNAT is broken → not related to egress-v6 plugin" + - "If ENABLE_V6_EGRESS=false → egress plugin is not involved" + repo_citation: + plugin: "cmd/egress-cni-plugin/main.go" + chains: "cmd/egress-cni-plugin/snat/snat.go — CNI-E6 and CNI-E4 chains" + + # ───────────────────────────────────────────────────────────────────────── + # G25: ENABLE_BANDWIDTH_PLUGIN — only affects annotated pods + # ───────────────────────────────────────────────────────────────────────── + - id: G25-bandwidth-plugin + category: traffic_shaping + common_mistaken_interpretation: > + "ENABLE_BANDWIDTH_PLUGIN=true applies traffic shaping to all pods." + repo_verified_correct_interpretation: > + The bandwidth plugin only affects pods with kubernetes.io/ingress-bandwidth + or kubernetes.io/egress-bandwidth annotations. Unannotated pods are + completely unaffected. The plugin is added to the CNI conflist chain + but is a no-op for pods without bandwidth annotations. + required_evidence_to_blame: + - "Pod has bandwidth annotations AND traffic shaping is wrong" + explicit_disqualifiers: + - "If pod has no bandwidth annotations → bandwidth plugin is not involved" + repo_citation: + conflist: "cmd/aws-vpc-cni/main.go — envEnBandwidthPlugin" + + # ───────────────────────────────────────────────────────────────────────── + # G26: Non-schedulable node ENI management + # ───────────────────────────────────────────────────────────────────────── + - id: G26-non-schedulable-eni + category: node_lifecycle + common_mistaken_interpretation: > + "Cordoned nodes should still have all ENIs attached for existing pods." + repo_verified_correct_interpretation: > + By default (AWS_MANAGE_ENIS_NON_SCHEDULABLE=false), IPAMD does NOT + manage ENIs on cordoned/non-schedulable nodes. This means warm pool + maintenance stops — no new ENIs attached, no excess ENIs removed. + Existing pod IPs remain functional. The non-schedulable check is a + real-time K8s API call, not cached. Set to true if you need ENI + management to continue during node drain operations. + required_evidence_to_blame: + - "Node is cordoned AND new pods can't get IPs on it" + - "AWS_MANAGE_ENIS_NON_SCHEDULABLE=false (default)" + explicit_disqualifiers: + - "If node is schedulable → this setting is irrelevant" + - "If existing pods work fine on cordoned node → expected behavior" + repo_citation: + env_def: "pkg/ipamd/ipamd.go — envManageENIsNonSchedulable" + api_check: "pkg/ipamd/ipamd.go — isNodeNonSchedulable() real-time K8s API call" + + # ───────────────────────────────────────────────────────────────────────── + # G27: ENABLE_IMDS_ONLY_MODE — does NOT disable networking + # ───────────────────────────────────────────────────────────────────────── + - id: G27-imds-only-mode + category: ip_management + common_mistaken_interpretation: > + "ENABLE_IMDS_ONLY_MODE=true disables networking or breaks ENI management." + repo_verified_correct_interpretation: > + ENABLE_IMDS_ONLY_MODE=true only skips EC2 API calls (DescribeNetworkInterfaces) + and uses IMDS metadata exclusively for ENI discovery. It implicitly disables + ENI provisioning and leaked ENI cleanup (since those require EC2 API). Existing + pod networking is UNAFFECTED. Used in environments with restricted EC2 API access. + required_evidence_to_blame: + - "ENABLE_IMDS_ONLY_MODE=true AND new pods cannot get IPs" + - "ipamd logs show IMDS-related errors" + explicit_disqualifiers: + - "If existing pods work fine → IMDS-only mode is not breaking networking" + - "If EC2 API calls are succeeding → IMDS-only mode is not active" + repo_citation: + env_def: "utils/constants.go — EnvEnableImdsOnlyMode" + usage: "pkg/ipamd/ipamd.go — enableImdsOnlyMode() → disableENIProvisioning()" + ec2_skip: "pkg/awsutils/awsutils.go — 'skipping EC2 API call and using IMDS metadata only'" + + # ───────────────────────────────────────────────────────────────────────── + # G28: ENABLE_V4_EGRESS vs ENABLE_V6_EGRESS — different cluster modes + # ───────────────────────────────────────────────────────────────────────── + - id: G28-v4-v6-egress-confusion + category: egress + common_mistaken_interpretation: > + "ENABLE_V4_EGRESS and ENABLE_V6_EGRESS are the same thing or can be + used interchangeably." + repo_verified_correct_interpretation: > + These are for OPPOSITE cluster modes. ENABLE_V6_EGRESS=true enables + IPv6 egress on IPv4 clusters (egress-v6 plugin, ip6tables CNI-E6-* chains). + ENABLE_V4_EGRESS=true (default) enables IPv4 egress on IPv6 clusters + (egress-v4 plugin, iptables CNI-E4-* chains, 169.254.172.0/22 IPAM). + Using the wrong one for your cluster mode has no effect. + required_evidence_to_blame: + - "Egress traffic in the non-native IP family fails" + - "Correct egress env var for cluster mode is set" + explicit_disqualifiers: + - "If cluster is IPv4 → ENABLE_V4_EGRESS is irrelevant (it's for IPv6 clusters)" + - "If cluster is IPv6 → ENABLE_V6_EGRESS is irrelevant (it's for IPv4 clusters)" + - "Do NOT confuse which egress var applies to which cluster mode" + repo_citation: + v4_egress: "cmd/aws-vpc-cni/main.go — envEnIPv4Egress, default true" + v6_egress: "cmd/aws-vpc-cni/main.go — envEnIPv6Egress" + v4_chains: "cmd/egress-cni-plugin/snat/snat.go — CNI-E4-" + v6_chains: "cmd/egress-cni-plugin/snat/snat.go — CNI-E6-" diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/cni-semantics/hallucination-patterns.yaml b/mcp/aws-eks-node-diagnostics-mcp/sops/cni-semantics/hallucination-patterns.yaml new file mode 100644 index 0000000..ac69eaa --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/cni-semantics/hallucination-patterns.yaml @@ -0,0 +1,323 @@ +# ============================================================================= +# "Looks Real but Wrong" Hallucination Pattern Mining +# Source of truth: amazon-vpc-cni-k8s (github.com/aws/amazon-vpc-cni-k8s) +# Patterns that LLMs commonly misinterpret about VPC CNI behavior +# ============================================================================= + +patterns: + + # ───────────────────────────────────────────────────────────────────────── + # P1: Security-sounding keys that don't do what the name implies + # ───────────────────────────────────────────────────────────────────────── + - id: P1-enforcing-mode-sounds-like-firewall + trigger_words: ["enforcingMode", "strict", "enforcing", "security group enforcing"] + hallucination_risk: CRITICAL + why_llms_get_this_wrong: > + "strict" and "enforcing" sound like firewall/deny semantics. LLMs + pattern-match these words to "default-deny" or "blocking" behavior. + In reality, strict mode only changes the veth prefix from "eni" to + "vlan" for SGP-annotated pods, opting them out of SNAT and Calico. + actual_behavior: > + POD_SECURITY_GROUP_ENFORCING_MODE controls veth naming for pods that + already have branch ENIs (PodVlanId != 0). It has zero effect on + unannotated pods. The "strict" vs "standard" choice only determines + whether SGP pods get SNAT and Calico management or not. + common_false_claim: > + "podSGEnforcingMode: strict is creating a default-deny posture that + blocks traffic to pods without explicit security group rules." + repo_proof: "pkg/sgpp/utils.go — BuildHostVethNamePrefix() just returns a string prefix" + guardrail_ref: G1-sgp-pod-eni + + - id: P2-enable-pod-eni-sounds-like-all-pods + trigger_words: ["ENABLE_POD_ENI", "pod ENI", "branch ENI", "trunk ENI"] + hallucination_risk: HIGH + why_llms_get_this_wrong: > + "ENABLE_POD_ENI" sounds like it enables ENIs for all pods. LLMs + assume this changes networking for every pod on the node. + actual_behavior: > + ENABLE_POD_ENI=true only attaches a trunk ENI to the node and enables + the SGP feature. Individual pods still need a SecurityGroupPolicy CRD + match AND the vpc.amazonaws.com/pod-eni annotation (set by VPC Resource + Controller) to actually get a branch ENI. Unannotated pods are unaffected. + common_false_claim: > + "ENABLE_POD_ENI=true causes all pods to use branch ENIs, which changes + their networking behavior and security group assignment." + repo_proof: "pkg/ipamd/rpc_handler.go — only pods with resource limit 'vpc.amazonaws.com/pod-eni' enter the branch ENI path" + guardrail_ref: G1-sgp-pod-eni + + # ───────────────────────────────────────────────────────────────────────── + # P3: Annotation-dependent features mistaken for global settings + # ───────────────────────────────────────────────────────────────────────── + - id: P3-annotation-gated-features + trigger_words: ["pod-eni annotation", "SecurityGroupPolicy", "ENIConfig"] + hallucination_risk: HIGH + why_llms_get_this_wrong: > + LLMs see env vars like ENABLE_POD_ENI or AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG + and assume they globally change behavior. They miss that the actual + behavior change requires per-pod annotations or per-node CRDs. + actual_behavior: > + Several VPC CNI features are two-stage: (1) env var enables the feature + at node level, (2) annotation/CRD selects which pods/nodes are affected. + Without the annotation/CRD, the env var alone does nothing to traffic. + Examples: SGP requires pod-eni annotation, custom networking requires + ENIConfig CRD matching the node. + common_false_claim: > + "Setting ENABLE_POD_ENI=true or AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG=true + immediately changes networking for all pods on the node." + repo_proof: "pkg/ipamd/rpc_handler.go — annotation check; pkg/ipamd/ipamd.go — ENIConfig lookup" + guardrail_ref: G1-sgp-pod-eni + + # ───────────────────────────────────────────────────────────────────────── + # P4: kube-proxy ownership areas blamed on CNI + # ───────────────────────────────────────────────────────────────────────── + - id: P4-kube-proxy-blamed-on-cni + trigger_words: ["KUBE-SERVICES", "ClusterIP", "service unreachable", "DNAT", "service routing"] + hallucination_risk: CRITICAL + why_llms_get_this_wrong: > + LLMs know VPC CNI manages iptables rules, so they assume it manages + ALL iptables rules including service routing. They don't distinguish + between CNI-owned chains (AWS-SNAT-CHAIN-0, AWS-CONNMARK-CHAIN-0) + and kube-proxy-owned chains (KUBE-SERVICES, KUBE-SVC-*, KUBE-SEP-*). + actual_behavior: > + The VPC CNI has ZERO involvement in service routing. It never creates, + modifies, or reads KUBE-SERVICES or any KUBE-* chain. Service ClusterIP + DNAT, NodePort DNAT, and LoadBalancer routing are 100% kube-proxy. + The CNI only creates AWS-SNAT-CHAIN-0 and AWS-CONNMARK-CHAIN-0. + common_false_claim: > + "The VPC CNI's strict enforcing mode or SNAT configuration is preventing + service ClusterIP from being reachable." + repo_proof: "grep -r 'KUBE-SERVICES' amazon-vpc-cni-k8s/ returns ZERO results" + guardrail_ref: G6-service-vs-pod-routing + + - id: P5-empty-kube-services-blamed-on-cni + trigger_words: ["KUBE-SERVICES empty", "no iptables rules", "service chain missing"] + hallucination_risk: CRITICAL + why_llms_get_this_wrong: > + When KUBE-SERVICES chain is empty, LLMs see CNI config like + podSGEnforcingMode=strict and blame it, because "strict" sounds + like it could block or remove rules. + actual_behavior: > + An empty KUBE-SERVICES chain means kube-proxy is not running or not + syncing. The CNI cannot empty this chain because it never touches it. + Common causes: kube-proxy DaemonSet not scheduled to node, kube-proxy + crashlooping, kube-proxy unable to reach API server. + common_false_claim: > + "podSGEnforcingMode: strict is causing KUBE-SERVICES chain to be empty + or blocking service routing rules from being created." + repo_proof: "The string 'KUBE-SERVICES' does not appear anywhere in the VPC CNI codebase" + guardrail_ref: G6-service-vs-pod-routing + + # ───────────────────────────────────────────────────────────────────────── + # P6: SNAT confusion — CNI SNAT vs kube-proxy masquerade + # ───────────────────────────────────────────────────────────────────────── + - id: P6-snat-vs-masquerade-confusion + trigger_words: ["SNAT", "masquerade", "NAT", "source NAT"] + hallucination_risk: MEDIUM + why_llms_get_this_wrong: > + LLMs conflate the CNI's SNAT (AWS-SNAT-CHAIN-0 for off-VPC traffic) + with kube-proxy's masquerade (KUBE-POSTROUTING for service traffic). + They assume changing AWS_VPC_K8S_CNI_EXTERNALSNAT affects service SNAT. + actual_behavior: > + Two independent SNAT mechanisms exist: + (1) CNI: AWS-SNAT-CHAIN-0 — SNATs pod traffic leaving the VPC + (2) kube-proxy: KUBE-POSTROUTING — masquerades service traffic + They use different marks (CNI=0x80, kube-proxy=0x0000c000) and + different chains. Changing one does not affect the other. + common_false_claim: > + "Setting AWS_VPC_K8S_CNI_EXTERNALSNAT=true breaks service masquerade." + repo_proof: "pkg/networkutils/network.go — only creates AWS-SNAT-CHAIN-0, never touches KUBE-POSTROUTING" + guardrail_ref: G2-snat-external-snat + + # ───────────────────────────────────────────────────────────────────────── + # P7: Node-local config mistaken for cluster-wide effect + # ───────────────────────────────────────────────────────────────────────── + - id: P7-node-local-vs-cluster-wide + trigger_words: ["cluster-wide", "all nodes", "global setting"] + hallucination_risk: MEDIUM + why_llms_get_this_wrong: > + LLMs see aws-node DaemonSet env vars and assume they're cluster-wide + settings. They don't realize each node independently reads its own + env vars and applies its own iptables rules. + actual_behavior: > + All VPC CNI configuration is node-local. Each node's IPAMD reads env + vars from its own aws-node pod, applies iptables rules locally, and + manages its own ENIs. Cross-node pod communication uses VPC routing + (route tables pointing secondary IPs to ENIs), not CNI iptables. + common_false_claim: > + "Changing WARM_IP_TARGET on one node affects IP allocation on other nodes." + repo_proof: "pkg/ipamd/ipamd.go — all env vars read via os.Getenv, local to the process" + guardrail_ref: G5-node-vs-pod-routing + + # ───────────────────────────────────────────────────────────────────────── + # P8: Prefix delegation mistaken for routing change + # ───────────────────────────────────────────────────────────────────────── + - id: P8-prefix-delegation-routing-confusion + trigger_words: ["prefix delegation", "/28", "ENABLE_PREFIX_DELEGATION"] + hallucination_risk: LOW + why_llms_get_this_wrong: > + "/28 prefix" sounds like a routing prefix, leading LLMs to think + prefix delegation changes routing behavior or subnet masks. + actual_behavior: > + Prefix delegation only changes how IPs are allocated from ENIs — + /28 blocks instead of individual IPs. Routing, iptables, SNAT, + and security groups are completely unaffected. It's purely an + IPAM optimization for higher pod density. + common_false_claim: > + "ENABLE_PREFIX_DELEGATION=true changes the pod subnet mask or routing." + repo_proof: "pkg/ipamd/ipamd.go — prefix delegation only affects assignPodIPv4AddressUnsafe()" + guardrail_ref: G4-prefix-delegation + + # ───────────────────────────────────────────────────────────────────────── + # P9: DISABLE_TCP_EARLY_DEMUX sounds like it disables TCP + # ───────────────────────────────────────────────────────────────────────── + - id: P9-tcp-early-demux-sounds-scary + trigger_words: ["DISABLE_TCP_EARLY_DEMUX", "tcp_early_demux", "disable TCP"] + hallucination_risk: MEDIUM + why_llms_get_this_wrong: > + "DISABLE_TCP" in the name sounds like it disables TCP functionality. + LLMs may claim this breaks TCP connections. + actual_behavior: > + DISABLE_TCP_EARLY_DEMUX=true sets the kernel sysctl net/ipv4/tcp_early_demux + to 0. This disables an optimization where the kernel skips routing lookup + for established TCP connections. It's needed for SGP strict mode so + kubelet probes can reach pods on branch ENIs. The only effect is a minor + performance impact on TCP connection setup — TCP itself works fine. + common_false_claim: > + "DISABLE_TCP_EARLY_DEMUX=true is breaking TCP connections on the node." + repo_proof: "cmd/aws-vpc-cni-init/main.go — procSys.Set('net/ipv4/tcp_early_demux', '0')" + guardrail_ref: null + + # ───────────────────────────────────────────────────────────────────────── + # P10: NETWORK_POLICY_ENFORCING_MODE confused with SGP enforcing mode + # ───────────────────────────────────────────────────────────────────────── + - id: P10-network-policy-vs-sgp-enforcing + trigger_words: ["NETWORK_POLICY_ENFORCING_MODE", "network policy strict", "default deny"] + hallucination_risk: CRITICAL + why_llms_get_this_wrong: > + Both settings contain "ENFORCING_MODE" and both have a "strict" option. + LLMs conflate them because the names are nearly identical. But they + control completely different subsystems with different code paths. + actual_behavior: > + NETWORK_POLICY_ENFORCING_MODE controls Kubernetes NetworkPolicy enforcement + via aws-network-policy-agent (eBPF). "strict" = default-deny until + NetworkPolicy allows. POD_SECURITY_GROUP_ENFORCING_MODE controls veth + naming for SGP pods. They share zero code paths. + common_false_claim: > + "NETWORK_POLICY_ENFORCING_MODE=strict is the same as SGP strict mode + and blocks traffic to pods without security groups." + repo_proof: "pkg/ipamd/ipamd.go — two separate env var reads, two separate code paths" + guardrail_ref: G9-network-policy-enforcing-mode + + # ───────────────────────────────────────────────────────────────────────── + # P11: DISABLE_NETWORK_RESOURCE_PROVISIONING sounds like it breaks networking + # ───────────────────────────────────────────────────────────────────────── + - id: P11-disable-network-resource-provisioning + trigger_words: ["DISABLE_NETWORK_RESOURCE_PROVISIONING", "disable network", "resource provisioning"] + hallucination_risk: HIGH + why_llms_get_this_wrong: > + "DISABLE_NETWORK" in the name sounds like it disables networking. + LLMs pattern-match "disable" + "network" to "networking is broken." + actual_behavior: > + This only disables ENI provisioning during pod init — used when an + external controller manages ENIs. Existing networking is unaffected. + The name is misleading but the behavior is narrow. + common_false_claim: > + "DISABLE_NETWORK_RESOURCE_PROVISIONING=true disables all pod networking." + repo_proof: "pkg/ipamd/ipamd.go — only skips ENI creation in tryAllocateENI()" + guardrail_ref: G20-disable-network-resource-provisioning + + # ───────────────────────────────────────────────────────────────────────── + # P12: WARM_IP_TARGET confused with max pods limit + # ───────────────────────────────────────────────────────────────────────── + - id: P12-warm-ip-vs-max-pods + trigger_words: ["WARM_IP_TARGET", "warm IP", "max pods", "pod limit"] + hallucination_risk: MEDIUM + why_llms_get_this_wrong: > + "IP TARGET" sounds like a limit on IPs available for pods. LLMs + assume it caps the number of pods that can run. + actual_behavior: > + WARM_IP_TARGET controls pre-allocation of FREE IPs in the warm pool. + It does NOT limit max pods. Max pods is kubelet --max-pods (set by + EKS bootstrap based on instance ENI/IP limits). IPAMD only checks: + if totalIPs >= maxPods, pool is not low (prevents over-allocation). + common_false_claim: > + "WARM_IP_TARGET=5 means only 5 pods can run on the node." + repo_proof: "pkg/ipamd/ipamd.go — getWarmIPTarget() only affects warm pool, not max pods" + guardrail_ref: G21-warm-ip-vs-max-pods + + # ───────────────────────────────────────────────────────────────────────── + # P13: AWS_VPC_ENI_MTU confused with pod MTU + # ───────────────────────────────────────────────────────────────────────── + - id: P13-eni-mtu-vs-pod-mtu + trigger_words: ["AWS_VPC_ENI_MTU", "ENI MTU", "pod MTU", "MTU 9001"] + hallucination_risk: LOW + why_llms_get_this_wrong: > + LLMs see "ENI MTU" and assume it's the MTU inside pods. They don't + know about the separate POD_MTU setting. + actual_behavior: > + AWS_VPC_ENI_MTU sets host ENI MTU (eth0, eth1). POD_MTU sets pod + veth MTU. If POD_MTU=0 (default), pods inherit ENI MTU. They are + configured in different files and applied at different layers. + common_false_claim: > + "Setting AWS_VPC_ENI_MTU=1500 changes the MTU inside all pods." + repo_proof: "pkg/networkutils/network.go — GetEthernetMTU() vs cmd/aws-vpc-cni/main.go — envPodMTU" + guardrail_ref: G22-eni-mtu-vs-pod-mtu + + # ───────────────────────────────────────────────────────────────────────── + # P14: Empty route table flagged as broken (normal with multiple ENIs) + # ───────────────────────────────────────────────────────────────────────── + - id: P14-empty-route-table-multi-eni + trigger_words: ["empty route table", "no default gateway", "missing routes", "route table empty"] + hallucination_risk: MEDIUM + why_llms_get_this_wrong: > + Traditional Linux networking expects a default gateway in the main + route table. LLMs flag its absence as critical. On EKS multi-ENI + nodes, per-ENI policy routing replaces the main table. + actual_behavior: > + VPC CNI creates per-ENI route tables (2, 3, 4...) with ip rules + directing pod traffic. The main route table may be empty or minimal. + This is NORMAL and EXPECTED on multi-ENI nodes. + common_false_claim: > + "The main route table has no default gateway — routing is broken." + repo_proof: "pkg/networkutils/network.go — SetupENINetwork() creates per-ENI tables" + guardrail_ref: G23-empty-route-table-multi-eni + + # ───────────────────────────────────────────────────────────────────────── + # P15: ENABLE_IMDS_ONLY_MODE sounds like it disables networking + # ───────────────────────────────────────────────────────────────────────── + - id: P15-imds-only-mode-sounds-like-disable + trigger_words: ["ENABLE_IMDS_ONLY_MODE", "IMDS only", "skip EC2 API"] + hallucination_risk: MEDIUM + why_llms_get_this_wrong: > + "IMDS only mode" sounds restrictive. LLMs may assume it disables + networking features or breaks ENI management entirely. + actual_behavior: > + ENABLE_IMDS_ONLY_MODE=true only changes how IPAMD discovers ENI + metadata — using IMDS instead of EC2 API. It implicitly disables + ENI provisioning and leaked ENI cleanup (which need EC2 API), but + existing pod networking is completely unaffected. + common_false_claim: > + "ENABLE_IMDS_ONLY_MODE=true is breaking pod networking because + IPAMD can't manage ENIs." + repo_proof: "pkg/awsutils/awsutils.go — only skips DescribeNetworkInterfaces, uses IMDS metadata instead" + guardrail_ref: G27-imds-only-mode + + # ───────────────────────────────────────────────────────────────────────── + # P16: ENABLE_V4_EGRESS confused with ENABLE_V6_EGRESS + # ───────────────────────────────────────────────────────────────────────── + - id: P16-v4-v6-egress-confusion + trigger_words: ["ENABLE_V4_EGRESS", "ENABLE_V6_EGRESS", "egress plugin", "egress-v4", "egress-v6"] + hallucination_risk: MEDIUM + why_llms_get_this_wrong: > + The naming is counterintuitive. ENABLE_V6_EGRESS is for IPv4 clusters + (enables IPv6 egress). ENABLE_V4_EGRESS is for IPv6 clusters (enables + IPv4 egress). LLMs assume V4 egress is for IPv4 clusters. + actual_behavior: > + ENABLE_V6_EGRESS=true → IPv4 cluster gets IPv6 egress capability + (egress-v6 plugin, ip6tables CNI-E6-* chains). + ENABLE_V4_EGRESS=true → IPv6 cluster gets IPv4 egress capability + (egress-v4 plugin, iptables CNI-E4-* chains, 169.254.172.0/22 IPAM). + common_false_claim: > + "ENABLE_V4_EGRESS controls IPv4 egress on IPv4 clusters." + repo_proof: "cmd/aws-vpc-cni/main.go lines 230-240 — V4 egress only in IPv6 mode, V6 egress only in IPv4 mode" + guardrail_ref: G28-v4-v6-egress-confusion diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/cni-semantics/hallucination-regression-tests.yaml b/mcp/aws-eks-node-diagnostics-mcp/sops/cni-semantics/hallucination-regression-tests.yaml new file mode 100644 index 0000000..be256de --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/cni-semantics/hallucination-regression-tests.yaml @@ -0,0 +1,442 @@ +# ============================================================================= +# Hallucination Regression Test Suite +# Each test presents a misleading config + real failure elsewhere +# The agent MUST NOT blame the misleading config +# ============================================================================= + +tests: + + # ───────────────────────────────────────────────────────────────────────── + # T1: SGP strict mode present, but kube-proxy is the real problem + # ───────────────────────────────────────────────────────────────────────── + - id: T1-sgp-strict-vs-kube-proxy-missing + guardrail_ref: G1-sgp-pod-eni + pattern_ref: P1-enforcing-mode-sounds-like-firewall + description: > + podSGEnforcingMode=strict is set, but the real failure is kube-proxy + not running on the node, causing empty KUBE-SERVICES chain. + misleading_config: + - "POD_SECURITY_GROUP_ENFORCING_MODE=strict in aws-node env" + - "ENABLE_POD_ENI=true in aws-node env" + real_injected_failure: + - "kube-proxy DaemonSet has nodeAffinity excluding this node" + - "KUBE-SERVICES chain in iptables is empty (0 rules)" + - "No KUBE-SVC-* or KUBE-SEP-* chains exist" + symptoms: + - "Service ClusterIP 10.100.0.10 (kubernetes) unreachable from pods" + - "Pod-to-pod direct IP works fine" + - "curl to pod IP succeeds, curl to service ClusterIP times out" + assertion: > + Agent MUST NOT blame podSGEnforcingMode or SGP configuration. + Agent MUST identify kube-proxy absence as root cause. + Agent MUST note that KUBE-SERVICES chain is empty. + repo_evidence_invalidating_mistake: > + pkg/sgpp/utils.go — BuildHostVethNamePrefix() only changes veth prefix. + The string "KUBE-SERVICES" does not appear in the VPC CNI codebase. + SGP only affects pods with vpc.amazonaws.com/pod-eni annotation. + + # ───────────────────────────────────────────────────────────────────────── + # T2: externalSNAT=true present, but missing NAT gateway is the problem + # ───────────────────────────────────────────────────────────────────────── + - id: T2-external-snat-vs-missing-nat-gw + guardrail_ref: G2-snat-external-snat + pattern_ref: P6-snat-vs-masquerade-confusion + description: > + AWS_VPC_K8S_CNI_EXTERNALSNAT=true is set correctly, but the VPC + route table has no route to a NAT gateway, so pods can't reach + the internet. + misleading_config: + - "AWS_VPC_K8S_CNI_EXTERNALSNAT=true in aws-node env" + - "AWS-SNAT-CHAIN-0 is empty (expected with externalSNAT)" + real_injected_failure: + - "VPC route table for pod subnet has no 0.0.0.0/0 → NAT gateway route" + - "NAT gateway was deleted or never created" + symptoms: + - "Pods cannot reach external endpoints (e.g., api.github.com)" + - "Pod-to-pod within VPC works fine" + - "Service ClusterIP routing works fine" + assertion: > + Agent MUST NOT blame externalSNAT setting (it's working as designed). + Agent MUST identify missing NAT gateway route as root cause. + Agent MUST note that service routing is unaffected (proving kube-proxy is fine). + repo_evidence_invalidating_mistake: > + pkg/networkutils/network.go — externalSNAT=true correctly removes + AWS-SNAT-CHAIN-0 rules. The expectation is that an external NAT + gateway handles SNAT. Missing NAT gateway is an infrastructure issue. + + # ───────────────────────────────────────────────────────────────────────── + # T3: Custom networking enabled, but SG blocking is the real problem + # ───────────────────────────────────────────────────────────────────────── + - id: T3-custom-networking-vs-sg-blocking + guardrail_ref: G3-custom-networking + pattern_ref: P3-annotation-gated-features + description: > + AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG=true is set with ENIConfig CRDs, + but the real failure is the security group on the ENIConfig-specified + subnet blocking traffic. + misleading_config: + - "AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG=true in aws-node env" + - "ENIConfig CRD specifies subnet-xyz with SG sg-restrictive" + - "Pod IPs are from a different subnet than node primary IP" + real_injected_failure: + - "Security group sg-restrictive has no inbound rules allowing pod traffic" + - "SG only allows traffic from a specific CIDR that doesn't include the pod subnet" + symptoms: + - "Pods on this node cannot receive traffic from pods on other nodes" + - "Pods on this node CAN reach external endpoints" + - "Node primary IP is reachable" + assertion: > + Agent MUST NOT blame custom networking configuration itself. + Agent MUST identify the restrictive security group as root cause. + Agent MUST check the SG rules on the ENIConfig-specified security group. + repo_evidence_invalidating_mistake: > + pkg/ipamd/ipamd.go — custom networking only changes which subnet/SG + secondary ENIs use. The networking path itself is unchanged. + + # ───────────────────────────────────────────────────────────────────────── + # T4: Prefix delegation enabled, but IP exhaustion is the real problem + # ───────────────────────────────────────────────────────────────────────── + - id: T4-prefix-delegation-vs-subnet-exhaustion + guardrail_ref: G4-prefix-delegation + pattern_ref: P8-prefix-delegation-routing-confusion + description: > + ENABLE_PREFIX_DELEGATION=true is set, but the subnet has run out of + /28 prefix blocks, causing new pods to fail IP allocation. + misleading_config: + - "ENABLE_PREFIX_DELEGATION=true in aws-node env" + - "WARM_PREFIX_TARGET=1" + - "Existing pods are running fine with /28 prefix IPs" + real_injected_failure: + - "Subnet has < 16 available IPs (cannot allocate a /28 block)" + - "ipamd logs show 'InsufficientCidrBlocks' errors" + symptoms: + - "New pods stuck in ContainerCreating" + - "Existing pods work fine" + - "ipamd logs: 'failed to allocate a private IP/Prefix address'" + assertion: > + Agent MUST NOT blame prefix delegation for routing or connectivity issues. + Agent MUST identify subnet IP exhaustion as root cause. + Agent MUST recommend expanding subnet CIDR or using a different subnet. + repo_evidence_invalidating_mistake: > + pkg/ipamd/ipamd.go — prefix delegation only changes allocation strategy. + InsufficientCidrBlocks is an EC2 API error, not a CNI bug. + + # ───────────────────────────────────────────────────────────────────────── + # T5: SGP strict + DISABLE_TCP_EARLY_DEMUX, but DNS is the real problem + # ───────────────────────────────────────────────────────────────────────── + - id: T5-sgp-config-vs-dns-failure + guardrail_ref: G1-sgp-pod-eni + pattern_ref: P9-tcp-early-demux-sounds-scary + description: > + Full SGP configuration is present (ENABLE_POD_ENI, strict mode, + DISABLE_TCP_EARLY_DEMUX), but the real failure is CoreDNS pods + crashlooping, causing DNS resolution failures for all pods. + misleading_config: + - "ENABLE_POD_ENI=true" + - "POD_SECURITY_GROUP_ENFORCING_MODE=strict" + - "DISABLE_TCP_EARLY_DEMUX=true" + real_injected_failure: + - "CoreDNS pods are in CrashLoopBackOff" + - "CoreDNS cannot reach API server (certificate expired)" + symptoms: + - "All pods fail DNS resolution" + - "nslookup kubernetes.default.svc.cluster.local times out" + - "Direct IP connectivity works (curl to pod IP succeeds)" + assertion: > + Agent MUST NOT blame SGP configuration or DISABLE_TCP_EARLY_DEMUX. + Agent MUST identify CoreDNS failure as root cause. + Agent MUST note that direct IP connectivity works (proving network path is fine). + repo_evidence_invalidating_mistake: > + SGP only affects pods with pod-eni annotation. CoreDNS pods typically + don't have SecurityGroupPolicy applied. DISABLE_TCP_EARLY_DEMUX only + affects kernel routing optimization, not DNS. + + # ───────────────────────────────────────────────────────────────────────── + # T6: SNAT exclusion CIDRs set, but VPC peering route missing + # ───────────────────────────────────────────────────────────────────────── + - id: T6-snat-exclusion-vs-missing-peering-route + guardrail_ref: G2-snat-external-snat + pattern_ref: P6-snat-vs-masquerade-confusion + description: > + AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS is set for a peered VPC CIDR, + but the VPC peering route is missing from the route table. + misleading_config: + - "AWS_VPC_K8S_CNI_EXCLUDE_SNAT_CIDRS=10.1.0.0/16" + - "iptables shows SNAT exclusion rule for 10.1.0.0/16" + real_injected_failure: + - "VPC route table has no route for 10.1.0.0/16 → peering connection" + - "VPC peering connection exists but route was never added" + symptoms: + - "Pods cannot reach 10.1.0.0/16 (peered VPC)" + - "Pods CAN reach internet and other VPC CIDRs" + assertion: > + Agent MUST NOT blame SNAT exclusion configuration. + Agent MUST identify missing VPC peering route as root cause. + Agent MUST note that SNAT exclusion is correctly configured (traffic + to 10.1.0.0/16 skips SNAT as intended). + repo_evidence_invalidating_mistake: > + pkg/networkutils/network.go — EXCLUDE_SNAT_CIDRS only adds iptables + rules to skip SNAT. It does not create VPC routes. + + # ───────────────────────────────────────────────────────────────────────── + # T7: Connmark value changed, but asymmetric routing is the real problem + # ───────────────────────────────────────────────────────────────────────── + - id: T7-connmark-vs-asymmetric-routing + guardrail_ref: G7-chain-ownership + pattern_ref: P4-kube-proxy-blamed-on-cni + description: > + AWS_VPC_K8S_CNI_CONNMARK is set to a non-default value, but the + real failure is asymmetric routing due to missing ip rules for a + secondary ENI. + misleading_config: + - "AWS_VPC_K8S_CNI_CONNMARK=0x40 (non-default, default is 0x80)" + real_injected_failure: + - "ip rule for secondary ENI's IP range is missing" + - "Traffic arrives on eth1 but replies go out eth0" + symptoms: + - "Pods on secondary ENI can initiate connections but cannot receive" + - "TCP connections from external sources to pods on eth1 fail" + - "Pods on primary ENI (eth0) work fine" + assertion: > + Agent MUST NOT blame connmark value change as root cause. + Agent MUST identify missing ip rule for secondary ENI as root cause. + Agent MUST check 'ip rule list' for missing per-ENI rules. + repo_evidence_invalidating_mistake: > + pkg/networkutils/network.go — connmark only affects NodePort return + path marking. Missing ip rules are a separate issue in SetupENINetwork(). + + # ───────────────────────────────────────────────────────────────────────── + # T8: IPv6 mode enabled, agent claims missing SNAT rules are a problem + # ───────────────────────────────────────────────────────────────────────── + - id: T8-ipv6-no-snat-is-normal + guardrail_ref: G8-ipv6-no-snat + pattern_ref: null + description: > + ENABLE_IPv6=true is set. Agent sees no AWS-SNAT-CHAIN-0 or + AWS-CONNMARK-CHAIN-0 and incorrectly flags this as a problem. + misleading_config: + - "ENABLE_IPv6=true" + - "ENABLE_PREFIX_DELEGATION=true (required for IPv6)" + - "No AWS-SNAT-CHAIN-0 in iptables (this is CORRECT for IPv6)" + real_injected_failure: + - "IPv6 security group missing inbound rule for pod CIDR" + symptoms: + - "Cross-node pod-to-pod IPv6 traffic blocked" + - "Same-node pod-to-pod works" + assertion: > + Agent MUST NOT flag missing SNAT/connmark chains as a problem in IPv6 mode. + Agent MUST identify security group as root cause. + Agent MUST state that IPv6 mode intentionally has no SNAT rules. + repo_evidence_invalidating_mistake: > + pkg/networkutils/network.go line 441 — updateHostIptablesRules() + returns nil immediately when v6Enabled=true. No SNAT or connmark + rules are created in IPv6 mode by design. + + # ───────────────────────────────────────────────────────────────────────── + # T9: Network policy strict vs SGP strict confusion + # ───────────────────────────────────────────────────────────────────────── + - id: T9-network-policy-strict-vs-sgp-strict + guardrail_ref: G9-network-policy-enforcing-mode + pattern_ref: P10-network-policy-vs-sgp-enforcing + description: > + NETWORK_POLICY_ENFORCING_MODE=strict is set AND + POD_SECURITY_GROUP_ENFORCING_MODE=strict is set. A pod without + NetworkPolicy is being blocked. Agent must blame network policy, + not SGP. + misleading_config: + - "POD_SECURITY_GROUP_ENFORCING_MODE=strict in aws-node env" + - "ENABLE_POD_ENI=true in aws-node env" + real_injected_failure: + - "NETWORK_POLICY_ENFORCING_MODE=strict is set" + - "Pod has NO NetworkPolicy allowing ingress" + - "aws-network-policy-agent is running and enforcing" + symptoms: + - "Pod cannot receive any traffic" + - "Pod has no vpc.amazonaws.com/pod-eni annotation" + - "iptables KUBE-SERVICES chain is populated (kube-proxy works)" + assertion: > + Agent MUST NOT blame POD_SECURITY_GROUP_ENFORCING_MODE. + Agent MUST identify NETWORK_POLICY_ENFORCING_MODE=strict as cause. + Agent MUST note pod lacks NetworkPolicy allowing traffic. + Agent MUST note pod has no pod-eni annotation (SGP not involved). + + # ───────────────────────────────────────────────────────────────────────── + # T10: nm-cloud-setup present vs actual routing issue + # ───────────────────────────────────────────────────────────────────────── + - id: T10-nm-cloud-setup-vs-sg-issue + guardrail_ref: G10-nm-cloud-setup + pattern_ref: null + description: > + nm-cloud-setup is detected (table 30200/30400 present), but the + real failure is a security group blocking pod traffic, not + nm-cloud-setup overwriting ip rules. + misleading_config: + - "nm-cloud-setup.service is active" + - "ip rule list shows table 30200 and 30400 entries" + - "RHEL 8 AMI in use" + real_injected_failure: + - "Security group on secondary ENI has no inbound rule for pod CIDR" + - "Per-ENI ip rules are intact (nm-cloud-setup hasn't overwritten them yet)" + symptoms: + - "Pods on secondary ENI cannot receive traffic from other nodes" + - "Pods on primary ENI work fine" + - "ip rule list shows correct per-ENI rules alongside table 30200" + assertion: > + Agent SHOULD flag nm-cloud-setup as a risk but MUST NOT blame it + as root cause if per-ENI ip rules are intact. + Agent MUST identify security group as the actual root cause. + + # ───────────────────────────────────────────────────────────────────────── + # T11: iptables-legacy vs nftables visibility + # ───────────────────────────────────────────────────────────────────────── + - id: T11-iptables-nftables-visibility + guardrail_ref: G11-iptables-nftables + pattern_ref: null + description: > + Host OS uses nftables but VPC CNI uses iptables-legacy. Agent sees + no CNI chains in nft output and incorrectly flags CNI as broken. + misleading_config: + - "nft list ruleset shows no AWS-SNAT-CHAIN-0" + - "Host OS is RHEL 8.6+ with nftables as default" + - "VPC CNI version < 1.13.1" + real_injected_failure: + - "iptables-legacy -t nat -L shows AWS-SNAT-CHAIN-0 correctly" + - "CNI is working fine in iptables-legacy mode" + - "Real issue: kube-proxy using nftables mode can't see CNI's iptables rules" + symptoms: + - "nft list ruleset shows no CNI chains" + - "iptables-save shows CNI chains correctly" + - "Service routing works (kube-proxy in nftables mode)" + assertion: > + Agent MUST NOT flag missing CNI chains in nft output as broken. + Agent MUST check iptables-save separately from nft list. + Agent MUST note the iptables-legacy vs nftables mode mismatch. + + # ───────────────────────────────────────────────────────────────────────── + # T12: IP cooldown transient errors vs actual IPAM failure + # ───────────────────────────────────────────────────────────────────────── + - id: T12-ip-cooldown-vs-ipam-failure + guardrail_ref: G12-ip-cooldown-transient + pattern_ref: null + description: > + ipamd logs show "IP not in datastore" messages during a pod churn + event. Agent incorrectly diagnoses IPAMD as broken. + misleading_config: + - "ipamd logs: 'IP not in datastore' messages (3 occurrences in 30s)" + - "IP_COOLDOWN_PERIOD=30" + - "Recent pod deletions visible in kubelet logs" + real_injected_failure: + - "Messages are transient — they stop after 30 seconds" + - "Real issue: subnet has only 5 IPs remaining (near exhaustion)" + symptoms: + - "Transient 'IP not in datastore' messages during pod churn" + - "New pods eventually get IPs but slowly" + - "ipamd logs show 'InsufficientCidrBlocks' after cooldown window" + assertion: > + Agent MUST NOT blame IPAMD or recommend restarting aws-node for + transient cooldown messages. + Agent MUST identify subnet IP exhaustion as the real issue. + Agent MUST note that cooldown messages lasting < 30s are normal. + + # ───────────────────────────────────────────────────────────────────────── + # T13: Conntrack exhaustion vs kube-proxy failure + # ───────────────────────────────────────────────────────────────────────── + - id: T13-conntrack-exhaustion-vs-kube-proxy + guardrail_ref: G16-conntrack-exhaustion + pattern_ref: null + description: > + dmesg shows "nf_conntrack: table full" and service routing is + intermittently failing. Agent blames kube-proxy or VPC CNI. + misleading_config: + - "kube-proxy logs show no errors" + - "VPC CNI aws-node pod is healthy" + - "AWS-SNAT-CHAIN-0 and KUBE-SERVICES chains are populated" + real_injected_failure: + - "nf_conntrack_count = nf_conntrack_max (table full)" + - "New connections being dropped at kernel level" + - "Node is running an ingress controller with high connection rate" + symptoms: + - "Intermittent service unreachability" + - "dmesg: 'nf_conntrack: table full, dropping packet'" + - "Existing long-lived connections work, new connections fail" + assertion: > + Agent MUST NOT blame kube-proxy or VPC CNI. + Agent MUST identify conntrack table exhaustion as root cause. + Agent MUST recommend increasing nf_conntrack_max via kube-proxy config. + + # ───────────────────────────────────────────────────────────────────────── + # T14: kube-proxy IPVS mode vs "missing iptables chains" + # ───────────────────────────────────────────────────────────────────────── + - id: T14-ipvs-mode-missing-iptables + guardrail_ref: G19-kube-proxy-ipvs + pattern_ref: null + description: > + kube-proxy is in IPVS mode. Agent sees no KUBE-SVC-* chains in + iptables and incorrectly flags kube-proxy as broken. + misleading_config: + - "iptables-save shows no KUBE-SVC-* or KUBE-SEP-* chains" + - "KUBE-SERVICES chain has minimal rules" + real_injected_failure: + - "kube-proxy is in IPVS mode (--proxy-mode=ipvs)" + - "ipvsadm -L shows all service entries correctly" + - "Real issue: ip_vs_rr kernel module not loaded on one node" + symptoms: + - "Service routing works on most nodes" + - "One node has no IPVS entries (ipvsadm -L empty)" + - "iptables shows no KUBE-SVC chains on any node (expected in IPVS)" + assertion: > + Agent MUST NOT flag missing KUBE-SVC iptables chains as broken + when kube-proxy is in IPVS mode. + Agent MUST check ipvsadm output instead of iptables. + Agent MUST identify missing ip_vs_rr kernel module as root cause + on the affected node. + + # ───────────────────────────────────────────────────────────────────────── + # T15: ENABLE_IMDS_ONLY_MODE present, but subnet exhaustion is the problem + # ───────────────────────────────────────────────────────────────────────── + - id: T15-imds-only-mode-vs-subnet-exhaustion + guardrail_ref: G27-imds-only-mode + pattern_ref: P15-imds-only-mode-sounds-like-disable + description: > + ENABLE_IMDS_ONLY_MODE=true is set. New pods can't get IPs. Agent + incorrectly blames IMDS-only mode for breaking ENI management. + misleading_config: + - "ENABLE_IMDS_ONLY_MODE=true in aws-node env" + - "ipamd logs: 'skipping EC2 API call and using IMDS metadata only'" + - "No EC2 DescribeNetworkInterfaces calls visible" + real_injected_failure: + - "Subnet has < 5 available IPs" + - "ipamd logs: 'InsufficientFreeAddressesInSubnet'" + - "Existing pods work fine" + symptoms: + - "New pods stuck in ContainerCreating" + - "Existing pods have connectivity" + - "ipamd logs show IP allocation failures" + assertion: > + Agent MUST NOT blame ENABLE_IMDS_ONLY_MODE for IP allocation failures. + Agent MUST identify subnet IP exhaustion as root cause. + Agent MUST note that existing pods working proves networking is fine. + + # ───────────────────────────────────────────────────────────────────────── + # T16: V4/V6 egress confusion — wrong egress var for cluster mode + # ───────────────────────────────────────────────────────────────────────── + - id: T16-v4-v6-egress-wrong-cluster-mode + guardrail_ref: G28-v4-v6-egress-confusion + pattern_ref: P16-v4-v6-egress-confusion + description: > + IPv4 cluster has ENABLE_V4_EGRESS=true set. IPv6 egress doesn't work. + Agent incorrectly blames ENABLE_V4_EGRESS for not working. + misleading_config: + - "ENABLE_V4_EGRESS=true in aws-node env" + - "Cluster is IPv4 mode (ENABLE_IPv4=true)" + real_injected_failure: + - "ENABLE_V6_EGRESS is not set (should be true for IPv6 egress on IPv4 cluster)" + - "No egress-v6 plugin in 10-aws.conflist" + symptoms: + - "Pods cannot reach IPv6 endpoints" + - "IPv4 connectivity works fine" + - "No CNI-E6-* chains in ip6tables" + assertion: > + Agent MUST NOT blame ENABLE_V4_EGRESS (it's for IPv6 clusters, irrelevant here). + Agent MUST identify that ENABLE_V6_EGRESS=true is needed for IPv6 egress on IPv4 clusters. + Agent MUST explain the counterintuitive naming convention. diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A1-node-not-ready-kubelet-oom.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A1-node-not-ready-kubelet-oom.md new file mode 100644 index 0000000..700605b --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A1-node-not-ready-kubelet-oom.md @@ -0,0 +1,139 @@ +--- +title: "A1 — Node NotReady Due to Kubelet OOM" +description: "Diagnose and remediate EKS worker node transitioning to NotReady caused by kubelet OOM-kill" +status: active +severity: CRITICAL +triggers: + - "invoked oom-killer.*kubelet" + - "Memory cgroup out of memory" + - "node.*NotReady" +owner: devops-agent +objective: "Confirm kubelet OOM as root cause, collect memory evidence, and restore node to Ready state" +context: "Kubelet process killed by kernel OOM-killer causes node to lose heartbeat and transition to NotReady. Pods on the node become orphaned until the node recovers or is replaced." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check node and pod state before any log collection: + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — confirm the node is NotReady + - Check node details: `kubectl describe node ` (via EKS MCP `read_k8s_resource`) — look at Conditions (MemoryPressure, DiskPressure, PIDPressure) and allocatable resources + - List pods on the affected node: `kubectl get pods --all-namespaces --field-selector spec.nodeName=` (via EKS MCP `list_k8s_resources` with field_selector) — check for pods in CrashLoopBackOff, OOMKilled, or Evicted state + - Check kubelet pod status on the node — if kubelet is OOM-killed, all pods on the node will be affected +- **PREREQUISITE — Is kubelet running?** Before investigating OOM, verify the kubelet process is alive: + - Use `collect` tool with instanceId to gather logs from the affected node + - Use `status` tool with executionId to poll until collection completes + - Use `search` tool with instanceId and query=`Active: active \(running\)|kubelet.*started|kubelet.service.*running` and logTypes=`kubelet` — if NO matches, kubelet is stopped/dead. That is the root cause, not necessarily OOM. + - Use `search` tool with instanceId and query=`Active: inactive|Active: failed|kubelet.service.*dead|kubelet.service.*failed` — if matches found, kubelet is stopped. Check dmesg for OOM evidence before concluding. + - If kubelet is stopped but NO OOM evidence found in dmesg, report "kubelet service not running — cause unknown, not OOM" and investigate further (check B1 SOP for config issues). + - ONLY if kubelet is confirmed stopped AND OOM evidence exists, OR kubelet is running but under memory pressure, proceed to OOM investigation below. +- Use `collect` tool with instanceId to start log collection from the affected node (skip if already collected in prerequisite) +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId and severity=critical to get pre-indexed OOM findings +- Use `search` tool with instanceId and query=`oom-killer|OOMKilled|out of memory|Memory cgroup` to find OOM evidence in dmesg and system logs + +SHOULD: +- Use `search` tool with query=`MemoryPressure|memory pressure|MemAvailable` to find memory pressure signals +- Use `search` tool with query=`kubelet.*restart|kubelet.*start` to detect kubelet restart events + +MAY: +- Use `cluster_health` tool with clusterName to check if multiple nodes are affected +- Use `compare_nodes` tool with instanceIds of affected + healthy node to diff findings + +## Phase 2 — Enrich + +MUST: +- Use `correlate` tool with instanceId and pivotEvent=`oom-killer` to build timeline around the OOM event +- Confirm the killed process is kubelet by reviewing findings from `errors` tool (look for finding with message containing kubelet PID) +- Use `validate` tool with instanceId to confirm log bundle has dmesg and kubelet logs + +SHOULD: +- Use `search` tool with query=`system-reserved|kube-reserved` in kubelet config logs to check memory reservation settings +- Use `errors` tool with severity=all to check for recurring OOM events (multiple findings with oom-killer pattern) + +MAY: +- Use `compare_nodes` tool to compare memory-related findings between affected node and a healthy peer + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from the OOM-related findings to generate incident summary +- State root cause: kubelet OOM-killed by kernel, with PID and timestamp evidence from findings +- List blast radius: affected node, number of orphaned pods (from triage output) +- Recommend immediate mitigation: cordon node, drain if needed (operator action) +- Recommend long-term fix: increase instance type or set system-reserved memory + +SHOULD: +- Include dmesg excerpt from `search` results showing OOM kill line +- Include memory stats from findings +- Provide estimated time to recovery + +MAY: +- Suggest Karpenter or cluster autoscaler configuration for dynamic scaling +- Recommend monitoring alert for node memory > 85% + +## Guardrails + +escalation_conditions: + - "Multiple nodes in NotReady state simultaneously (check via cluster_health)" + - "OOM kills recurring within 30 minutes of kubelet restart" + - "Node does not recover after kubelet restart + 5 minutes" + - "System-reserved memory already at recommended values and OOM persists" + +safety_ratings: + - "Log collection (collect), search, errors, correlate: GREEN (read-only)" + - "Cordon/drain node: YELLOW — operator action, not available via MCP tools" + - "Restart kubelet: YELLOW — operator action, not available via MCP tools" + - "Terminate and replace node: RED — operator action, requires approval" + +## Common Issues + +- symptoms: "search for kubelet service status returns Active: inactive or Active: failed, but no OOM evidence in dmesg" + diagnosis: "Kubelet is stopped but not due to OOM. Could be config error, manual stop, or other failure." + resolution: "Investigate kubelet config (see B1 SOP). Operator action: check journalctl -u kubelet for startup errors, then restart kubelet." + +- symptoms: "errors tool returns findings with oom-killer targeting kubelet PID" + diagnosis: "Kernel killed kubelet due to memory exhaustion. Use search tool with query=system-reserved to check if memory reservation is configured." + resolution: "Operator action: restart kubelet, then set --system-reserved=memory=1Gi and --kube-reserved=memory=512Mi in kubelet config" + +- symptoms: "search for MemAvailable shows < 100MB but no OOM kill found in errors" + diagnosis: "Memory pressure without OOM. Kubelet may recover on its own." + resolution: "Monitor for 5 minutes. If node stays NotReady, operator should drain and investigate memory consumers." + +- symptoms: "cluster_health shows multiple nodes NotReady simultaneously" + diagnosis: "Cluster-wide issue. Possible DaemonSet memory leak or undersized node group." + resolution: "Escalate immediately. Use compare_nodes to check DaemonSet resource usage across all nodes." + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Poll status +status(executionId="") +# Step 3: Get OOM findings +errors(instanceId="i-0abc123def456", severity="critical") +# Step 4: Deep search for OOM evidence +search(instanceId="i-0abc123def456", query="oom-killer|OOMKilled|out of memory") +# Step 5: Correlate timeline +correlate(instanceId="i-0abc123def456", pivotEvent="oom-killer", timeWindow=120) +# Step 6: Generate summary +summarize(instanceId="i-0abc123def456", finding_ids=["F-001","F-002","F-003"]) +``` + +## Output Format + +```yaml +root_cause: "Kubelet OOM-killed by kernel" +evidence: + - type: dmesg_finding + content: "" + - type: memory_search + content: "" +blast_radius: "node (), pods affected" +severity: CRITICAL +mitigation: + immediate: "Operator: cordon node, drain if pods need rescheduling" + short_term: "Operator: restart kubelet after memory pressure resolves" + long_term: "Increase instance type or set system-reserved memory" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A2-node-bootstrap-registration-failure.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A2-node-bootstrap-registration-failure.md new file mode 100644 index 0000000..0da04ac --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A2-node-bootstrap-registration-failure.md @@ -0,0 +1,123 @@ +--- +title: "A2 — Node Bootstrap / Registration Failure" +description: "Diagnose EKS worker node that never appears in kubectl get nodes after launch" +status: active +severity: HIGH +triggers: + - "Unauthorized" + - "Unable to register node" + - "connect: connection refused" + - "TLS handshake timeout" +owner: devops-agent +objective: "Identify why the node cannot register with the EKS API server and restore registration" +context: "Node instance is running in EC2 but never joins the cluster. Common causes include aws-auth misconfiguration, network connectivity issues to the API server, or bootstrap script failures." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check node state before any log collection: + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — check if the node appears in the node list at all (unregistered nodes won't appear) + - If node IS listed: check its status — it may be NotReady with registration errors + - If node is NOT listed: confirms bootstrap/registration failure — proceed with log collection +- Use `collect` tool with instanceId to gather logs from the unregistered node +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId to get pre-indexed findings — look for Unauthorized, TLS, or bootstrap errors +- Use `search` tool with instanceId and query=`Unauthorized|Unable to register|connection refused|TLS handshake timeout` to find registration failure evidence + +SHOULD: +- Use `search` tool with query=`cloud-init|bootstrap` to check bootstrap script output +- Use `cluster_health` tool with clusterName to verify cluster is healthy and accepting registrations + +MAY: +- Use `network_diagnostics` tool with instanceId and sections=routes,dns to check network path to API server +- Use `compare_nodes` tool with instanceIds of failed node + a healthy registered node to diff findings + +## Phase 2 — Enrich + +MUST: +- Use `search` tool with query=`Unauthorized` — indicates aws-auth or access entry missing node role +- Use `search` tool with query=`TLS handshake timeout` — indicates SG/NACL blocking 443 to API server +- Use `search` tool with query=`Too Many Requests` — indicates API throttling during bootstrap +- Use `search` tool with query=`no route to host` — indicates subnet routing issue + +SHOULD: +- Use `correlate` tool with instanceId to build timeline of bootstrap events and identify first failure point +- Use `search` tool with query=`aws-auth|iamidentitymapping` to check for auth configuration clues in logs + +MAY: +- Use `network_diagnostics` tool with sections=dns to verify DNS resolution of API server endpoint +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="certificatesigningrequests" to check for node CSR requests and whether they were approved or denied — unapproved CSRs prevent node registration +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="aws-auth" to check for recent aws-auth ConfigMap changes that may have removed the node role mapping +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="access" to check for access entry mutations + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from registration-related findings +- State root cause with specific evidence (auth failure, network block, or bootstrap error) +- Recommend specific fix (operator action: update aws-auth, fix SG rules, or pass bootstrap args) +- List blast radius: single node or node group affected + +SHOULD: +- Include relevant log excerpts from search results showing the failure +- Provide exact remediation steps for the operator + +MAY: +- Recommend managed node groups for automatic aws-auth management +- Suggest passing --apiserver-endpoint, --b64-cluster-ca, --dns-cluster-ip to bootstrap script + +## Guardrails + +escalation_conditions: + - "Node role is correctly mapped but still Unauthorized" + - "API server endpoint unreachable from VPC (potential VPC endpoint issue)" + - "Multiple nodes failing to register simultaneously (check via cluster_health)" + +safety_ratings: + - "Log collection (collect), search, errors, correlate: GREEN (read-only)" + - "Update aws-auth ConfigMap: YELLOW — operator action, not available via MCP tools" + - "Modify security groups: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "errors tool returns findings with Unauthorized in kubelet logs" + diagnosis: "aws-auth ConfigMap or EKS access entry missing node IAM role ARN" + resolution: "Operator action: add node role to aws-auth via eksctl create iamidentitymapping" + +- symptoms: "search for TLS handshake timeout returns matches in kubelet logs" + diagnosis: "Security group or NACL blocking port 443 to EKS API endpoint" + resolution: "Operator action: update security group to allow outbound 443 to EKS API server CIDR" + +- symptoms: "search for Too Many Requests returns matches in cloud-init logs" + diagnosis: "API throttling during bootstrap DescribeCluster call" + resolution: "Operator action: pass --apiserver-endpoint, --b64-cluster-ca, --dns-cluster-ip directly to bootstrap.sh" + +## Examples + +``` +# Step 1: Collect logs from unregistered node +collect(instanceId="i-0abc123def456") +# Step 2: Check for registration errors +errors(instanceId="i-0abc123def456", severity="critical") +# Step 3: Search for specific failure patterns +search(instanceId="i-0abc123def456", query="Unauthorized|TLS handshake|connection refused") +# Step 4: Check network path +network_diagnostics(instanceId="i-0abc123def456", sections="routes,dns") +``` + +## Output Format + +```yaml +root_cause: "" +evidence: + - type: finding + content: "" + - type: search_match + content: "" +blast_radius: "node ()" +severity: HIGH +mitigation: + immediate: "Operator: " + long_term: "Use managed node groups or pass bootstrap args to avoid API calls" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A2-node-not-ready-certificate-expired.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A2-node-not-ready-certificate-expired.md new file mode 100644 index 0000000..0de82e9 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A2-node-not-ready-certificate-expired.md @@ -0,0 +1,119 @@ +--- +title: "A2-cert — Node NotReady Due to Expired Certificate" +description: "Diagnose node NotReady caused by expired kubelet serving or client certificate" +status: active +severity: HIGH +triggers: + - "x509: certificate has expired" + - "certificate.*expired" + - "TLS handshake error" +owner: devops-agent +objective: "Confirm certificate expiry as root cause and restore node TLS communication" +context: "Kubelet uses client certificates to authenticate with the API server. If certificates expire and auto-rotation fails, the node loses communication and transitions to NotReady." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check node state before any log collection: + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — confirm the node is NotReady + - Check node details: `kubectl describe node ` (via EKS MCP `read_k8s_resource`) — look at Conditions and check for certificate-related events + - List pods on the affected node: `kubectl get pods --all-namespaces --field-selector spec.nodeName=` (via EKS MCP `list_k8s_resources` with field_selector) — check if pods are being evicted or stuck +- Use `collect` tool with instanceId to gather logs from the affected node +- Use `status` tool with executionId to poll until collection completes +- **PREREQUISITE — Is kubelet running?** Before investigating certificate expiry, verify kubelet is alive: + - Use `search` tool with instanceId and query=`Active: active \(running\)|kubelet.*started|kubelet.service.*running` and logTypes=`kubelet` — if NO matches, kubelet is stopped/dead. That may be the root cause, not necessarily a certificate issue. + - Use `search` tool with instanceId and query=`Active: inactive|Active: failed|kubelet.service.*dead|kubelet.service.*failed` — if matches found, kubelet is stopped. Check if certificate errors caused the stop, or if kubelet died for another reason (see B1 SOP for config issues, A1 for OOM). + - ONLY if kubelet is confirmed running (but failing TLS), OR kubelet is stopped AND certificate errors are found, proceed to certificate investigation below. +- Use `errors` tool with instanceId to get pre-indexed findings — look for x509 or certificate errors +- Use `search` tool with instanceId and query=`x509.*expired|certificate has expired|TLS handshake error` to find certificate failure evidence + +SHOULD: +- Use `search` tool with query=`rotateCertificates|certificate rotation` to check if rotation is enabled in kubelet config +- Use `search` tool with query=`kubelet-client-current.pem|kubelet.pem` to find certificate file references + +MAY: +- Use `cluster_health` tool with clusterName to check if multiple nodes have the same issue + +## Phase 2 — Enrich + +MUST: +- Use `correlate` tool with instanceId and pivotEvent=`x509` to build timeline around certificate failure +- Confirm certificate expiry by reviewing findings from `errors` tool for x509 messages with dates +- Use `search` tool with query=`rotateCertificates` to verify if kubelet certificate rotation is enabled + +SHOULD: +- Use `search` tool with query=`clock|time|chrony|ntp` to check if clock skew is causing false expiry (see A3-clock-skew SOP) +- Use `search` tool with query=`CSR|CertificateSigningRequest` to check for pending CSR approval issues + +MAY: +- Use `compare_nodes` tool to compare certificate-related findings between affected and healthy nodes + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from certificate-related findings +- State root cause: certificate expired with expiry date evidence from findings +- Recommend fix: operator should restart kubelet to trigger certificate rotation, or manually approve CSR +- List blast radius + +SHOULD: +- Include certificate expiry evidence from search results +- Verify rotateCertificates setting from search results + +MAY: +- Recommend monitoring certificate expiry dates + +## Guardrails + +escalation_conditions: + - "Certificate rotation enabled but CSR not auto-approved" + - "Multiple nodes with expired certificates simultaneously" + - "Kubelet restart does not trigger new CSR" + +safety_ratings: + - "Log collection (collect), search, errors, correlate: GREEN (read-only)" + - "Restart kubelet: YELLOW — operator action, not available via MCP tools" + - "Approve CSR: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "search for kubelet service status returns Active: inactive or Active: failed, but no x509 or certificate errors found" + diagnosis: "Kubelet is stopped but not due to certificate expiry. Could be config error, OOM, or other failure." + resolution: "Investigate kubelet config (see B1 SOP) or OOM (see A1 SOP). Operator action: check journalctl -u kubelet for startup errors." + +- symptoms: "errors tool returns findings with x509: certificate has expired" + diagnosis: "Kubelet client certificate expired and rotation did not occur" + resolution: "Operator action: restart kubelet to trigger CSR. If CSR pending, approve via kubectl certificate approve" + +- symptoms: "search for rotateCertificates returns false or no matches" + diagnosis: "Certificate rotation not enabled in kubelet config" + resolution: "Operator action: set rotateCertificates: true in kubelet config and restart kubelet" + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Get certificate-related findings +errors(instanceId="i-0abc123def456", severity="critical") +# Step 3: Search for certificate errors +search(instanceId="i-0abc123def456", query="x509.*expired|certificate has expired") +# Step 4: Check rotation config +search(instanceId="i-0abc123def456", query="rotateCertificates") +``` + +## Output Format + +```yaml +root_cause: "Kubelet certificate expired" +evidence: + - type: finding + content: "" + - type: config_search + content: "" +severity: HIGH +mitigation: + immediate: "Operator: restart kubelet to trigger certificate rotation" + long_term: "Ensure rotateCertificates: true in kubelet config" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A3-clock-skew.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A3-clock-skew.md new file mode 100644 index 0000000..ee8c945 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A3-clock-skew.md @@ -0,0 +1,136 @@ +--- +title: "A3 — Clock Skew" +description: "Diagnose TLS/certificate failures caused by system clock drift on EKS worker nodes" +status: active +severity: HIGH +triggers: + - "x509:.*not yet valid" + - "clock skew detected" + - "time is out of sync" +owner: devops-agent +objective: "Confirm clock skew as root cause of TLS failures and restore time synchronization" +context: "System clock drift causes TLS certificate validation to fail because certificates appear not-yet-valid or expired. This breaks kubelet-to-API-server communication and can affect all TLS-dependent services on the node." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check node state before any log collection: + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — confirm the node is NotReady or showing clock-related issues + - Check node details: `kubectl describe node ` (via EKS MCP `read_k8s_resource`) — look for x509 certificate errors or time-related conditions + - List pods on the affected node: `kubectl get pods --all-namespaces --field-selector spec.nodeName=` (via EKS MCP `list_k8s_resources` with field_selector) — check for pods failing with TLS or certificate errors +- Use `collect` tool with instanceId to gather logs from the affected node +- Use `status` tool with executionId to poll until collection completes +- **PREREQUISITE — Is chronyd/ntpd running?** Before investigating clock drift values, verify the NTP service is alive: + - Use `search` tool with instanceId and query=`Active: active \(running\)|chronyd.*started|chronyd.service.*running|ntpd.*running` and logTypes=`system` — if NO matches for any NTP service, the time sync daemon is stopped. That is the root cause. + - Use `search` tool with instanceId and query=`chronyd.*inactive|chronyd.*failed|ntpd.*inactive|ntpd.*failed` — if matches found, NTP service is stopped. Report "NTP service (chronyd/ntpd) not running — clock will drift" as root cause. + - ONLY if NTP service is confirmed running but clock is still drifting, proceed to clock skew investigation below. +- Use `errors` tool with instanceId to get pre-indexed findings — look for x509 or time-related errors +- Use `search` tool with instanceId and query=`x509.*not yet valid|clock skew|time.*out of sync` to find clock-related TLS failures + +SHOULD: +- Use `search` tool with query=`chronyd|ntpd|timedatectl|time sync` to check NTP service status in collected logs +- Use `search` tool with query=`chrony.*offset|ntp.*offset` to find time offset values + +MAY: +- Use `network_diagnostics` tool with instanceId and sections=dns to check if 169.254.169.123 (Amazon Time Sync) is reachable +- Use `cluster_health` tool with clusterName to check if multiple nodes have clock skew + +## Phase 2 — Enrich + +MUST: +- Use `correlate` tool with instanceId and pivotEvent=`x509` to build timeline around TLS failures +- Confirm clock drift by reviewing findings — look for "not yet valid" (clock behind) vs "expired" (clock ahead) +- Use `search` tool with query=`chronyd|ntpd` and logTypes=`system` to check NTP daemon status + +SHOULD: +- Use `search` tool with query=`linklocal_allowance_exceeded` to check if ENA throttling is blocking NTP access to 169.254.169.123 +- Use `errors` tool with severity=all to check for other time-dependent failures (token expiry, lease renewal) + +MAY: +- Use `compare_nodes` tool to compare time-related findings between affected and healthy nodes + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from clock/TLS-related findings +- State root cause: clock skew causing TLS validation failure, with evidence from findings +- Recommend immediate fix: operator should force time sync (chronyc makestep) +- Confirm node should return to Ready after sync + +SHOULD: +- Include time offset evidence from search results +- Include kubelet error showing TLS failure from findings + +MAY: +- Recommend CloudWatch alarm for NTP sync status + +## Guardrails + +escalation_conditions: + - "chronyd restart does not restore time sync" + - "linklocal_allowance_exceeded preventing NTP access (see D6-ena-throttling SOP)" + - "Multiple nodes with clock skew simultaneously" + +safety_ratings: + - "Log collection (collect), search, errors, correlate: GREEN (read-only)" + - "Force time sync (chronyc makestep): YELLOW — operator action, not available via MCP tools" + - "Restart chronyd: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "search for chronyd/ntpd service status returns Active: inactive or Active: failed, or no NTP service found" + diagnosis: "NTP service is not running. Without time synchronization, the system clock will drift and cause TLS failures." + resolution: "Operator action: systemctl enable --now chronyd. Verify Amazon Time Sync (169.254.169.123) is configured in /etc/chrony.conf." + +- symptoms: "errors tool returns findings with x509: certificate not yet valid" + diagnosis: "Node clock is behind actual time. Certificates appear to be in the future." + resolution: "Operator action: chronyc makestep to force immediate sync, then systemctl restart chronyd" + +- symptoms: "search for chronyd returns no matches or shows chronyd not running" + diagnosis: "NTP service stopped or failed to start" + resolution: "Operator action: systemctl enable --now chronyd and verify Amazon Time Sync is configured" + +- symptoms: "search for chrony offset shows large offset values (>1 second) despite chronyd running" + diagnosis: "Chrony is running but not syncing properly. Source may be unreachable or misconfigured." + resolution: "Operator action: check chronyc sources -v for source status. Ensure Amazon Time Sync (169.254.169.123) is configured with 'prefer iburst minpoll 4 maxpoll 4' in chrony config. On AL2023 check /run/chrony.d/ and /etc/chrony.d/. On AL2 check /etc/chrony.d/." + +- symptoms: "search returns ntpd references instead of chronyd" + diagnosis: "Node is using ntpd instead of chrony. Best practice is chrony for faster sync and better accuracy." + resolution: "Operator action: migrate from ntpd to chrony — systemctl disable --now ntpd && systemctl enable --now chronyd. Configure Amazon Time Sync as primary source." + +- symptoms: "search for linklocal_allowance_exceeded shows nonzero values alongside clock skew" + diagnosis: "ENA linklocal throttling is blocking NTP access to Amazon Time Sync (169.254.169.123). See D6-ena-throttling SOP." + resolution: "Operator action: reduce linklocal traffic (deploy NodeLocal DNSCache, use IRSA instead of IMDS) or upgrade instance type. Add public fallback: 'pool time.aws.com iburst' in chrony config." + +- symptoms: "search for IPv6 NTP or fd00:ec2::123 shows connection failures on Nitro instances" + diagnosis: "IPv6 Amazon Time Sync endpoint (fd00:ec2::123) not reachable. May need IPv4 fallback." + resolution: "Operator action: ensure 169.254.169.123 (IPv4) is configured as primary NTP source. IPv6 fd00:ec2::123 is available on Nitro instances as alternative." + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Get time-related findings +errors(instanceId="i-0abc123def456") +# Step 3: Search for clock skew evidence +search(instanceId="i-0abc123def456", query="x509.*not yet valid|clock skew|time.*out of sync") +# Step 4: Check NTP status +search(instanceId="i-0abc123def456", query="chronyd|ntpd|timedatectl") +``` + +## Output Format + +```yaml +root_cause: "Clock skew causing TLS validation failure" +evidence: + - type: finding + content: "" + - type: ntp_search + content: "" +severity: HIGH +mitigation: + immediate: "Operator: chronyc makestep && systemctl restart chronyd" + long_term: "Ensure Amazon Time Sync (169.254.169.123) is configured in chrony.conf" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A4-worker-node-join-failure.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A4-worker-node-join-failure.md new file mode 100644 index 0000000..9a134b4 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/A4-worker-node-join-failure.md @@ -0,0 +1,337 @@ +--- +title: "A4 — Worker Node Fails to Join EKS Cluster" +description: "Comprehensive SOP for diagnosing all known reasons an EC2 worker node fails to register with an EKS cluster, covering IAM, networking, bootstrap, DNS, STS, AMI, security groups, tags, and VPC endpoint issues" +status: active +severity: CRITICAL +triggers: + - "Instances failed to join the kubernetes cluster" + - "NodeCreationFailure" + - "Unable to register node" + - "Unauthorized" + - "connect: connection refused" + - "TLS handshake timeout" + - "node.*not found" + - "InvalidClientTokenId" + - "AccessDenied.*sts" + - "Create failed" +owner: devops-agent +objective: "Systematically identify which of the 12+ known failure modes is preventing the worker node from joining the EKS cluster and recommend the specific fix" +context: > + A worker node that never appears in 'kubectl get nodes' after launch. This is one of the most common EKS issues + with many possible root causes spanning IAM (aws-auth/access entries, node role permissions), networking + (security groups, NACLs, route tables, VPC endpoints, NAT gateway), bootstrap configuration (user data, + cluster name, API endpoint args), DNS (VPC DNS settings, DHCP options), STS (regional endpoint activation), + AMI (missing required components), and tagging (kubernetes.io/cluster tag). This SOP covers all failure modes + documented in the AWS EKS troubleshooting guide and re:Post knowledge center. Cross-references A2 (bootstrap + registration) for overlap but provides deeper coverage of networking, IAM, and VPC endpoint scenarios. +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check node state before any log collection: + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — check if the node appears in the node list (failed join = not listed) + - If node IS listed: check its status and conditions for join-related errors + - If node is NOT listed: confirms join failure — proceed with log collection via SSM +- Use `collect` tool with instanceId to gather logs from the node that failed to join +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId and severity=critical to get pre-indexed findings +- Use `search` tool with instanceId and query=`Unauthorized|Unable to register|connection refused|TLS handshake timeout|InvalidClientTokenId|not found|AccessDenied|Create failed|NodeCreationFailure` to cast a wide net for join failure evidence + +SHOULD: +- Use `search` tool with query=`cloud-init|bootstrap|/etc/eks/bootstrap` to check if bootstrap script ran and what arguments were passed +- Use `search` tool with query=`kubelet.*error|kubelet.*fatal|kubelet.*failed` to check kubelet startup errors +- Use `network_diagnostics` tool with instanceId and sections=routes,dns,eni to check network path to API server +- Use `cluster_health` tool with clusterName to verify the cluster itself is healthy and accepting registrations + +MAY: +- Use `quick_triage` tool with instanceId for a fast overview +- Use `compare_nodes` tool with instanceIds of failed node + a healthy registered node to diff findings + +## Phase 2 — Enrich + +MUST check each failure domain systematically. Work through these in order — the first match is likely the root cause: + +### 2A — IAM / Authentication (most common) + +MUST: +- Use `search` tool with query=`Unauthorized|401|Forbidden|403` to check for authentication failures + - If found: aws-auth ConfigMap or EKS access entry is missing the node IAM role ARN + - Common mistake: using instance profile ARN instead of role ARN + - Common mistake: role ARN contains a path (e.g., /development/apps/my-role) — path must be removed + - Recommended: use EKS Access Entries (EC2 Linux or EC2 Windows type) instead of aws-auth ConfigMap for new clusters +- Use `search` tool with query=`AccessDenied|not authorized|AmazonEKSWorkerNodePolicy|AmazonEKS_CNI_Policy|AmazonEC2ContainerRegistryReadOnly|AmazonEC2ContainerRegistryPullOnly` to check node role permissions + - Node role must have: AmazonEKSWorkerNodePolicy, AmazonEKS_CNI_Policy, and AmazonEC2ContainerRegistryReadOnly (or AmazonEC2ContainerRegistryPullOnly for newer setups) + - Also verify the cluster IAM role has AmazonEKSClusterPolicy and the correct trust policy for eks.amazonaws.com +- Use `search` tool with query=`InvalidClientTokenId|SignatureDoesNotMatch|security token.*expired` to check STS/credential issues + - If found: regional STS endpoint may not be activated for this region + +SHOULD: +- Use `search` tool with query=`sts.*endpoint|sts.*region|regional.*sts` to check if regional STS is being used +- Use `search` tool with query=`access.entry|iamidentitymapping|aws-auth` to check for auth configuration clues + +### 2B — Network Connectivity to API Server + +MUST: +- Use `network_diagnostics` tool with instanceId and sections=routes,eni to check routing +- Use `search` tool with query=`TLS handshake timeout|connection timed out|connection refused|no route to host|dial tcp.*443` to find API server connectivity failures + - TLS handshake timeout: security group or NACL blocking port 443 to API server endpoint + - Connection refused: API server endpoint unreachable or private endpoint not enabled + - No route to host: routing issue — check route tables, NAT gateway, internet gateway +- Use `search` tool with query=`private.*endpoint|public.*endpoint|endpoint.*access` to check cluster endpoint configuration + +SHOULD: +- Use `search` tool with query=`NAT|nat-|igw-|internet gateway` to check NAT/IGW configuration in route tables +- Use `search` tool with query=`vpc.*endpoint|vpce-|PrivateLink` to check VPC endpoint configuration for private clusters + - Private clusters need VPC endpoints for: ec2, ecr.api, ecr.dkr, sts, s3 (gateway) +- Use `network_diagnostics` iptables section to check for firewall rules blocking outbound 443 + +### 2C — DNS Configuration + +MUST: +- Use `network_diagnostics` tool with instanceId and sections=dns to check DNS resolution +- Use `search` tool with query=`node.*not found|hostname.*not found|NXDOMAIN|resolve.*failed` to find DNS failures + - "node not found" error: VPC missing DHCP options for domain-name and domain-name-servers + - DNS resolution failure: VPC DNS support or DNS hostnames not enabled +- Use `search` tool with query=`DHCP|domain-name|AmazonProvidedDNS` to check DHCP options + +### 2D — Bootstrap / User Data Configuration + +MUST: +- Use `search` tool with query=`bootstrap\.sh|/etc/eks/bootstrap|cloud-init.*error|cloud-init.*fatal` to check bootstrap execution + - Bootstrap script not found: AMI may not be EKS-optimized + - Bootstrap args wrong: ClusterName mismatch, missing --apiserver-endpoint for private clusters +- Use `search` tool with query=`ClusterName|cluster-name|--b64-cluster-ca|--apiserver-endpoint|--dns-cluster-ip` to verify bootstrap arguments + - For private clusters: --apiserver-endpoint, --b64-cluster-ca, and --dns-cluster-ip MUST be passed explicitly + - ClusterName must exactly match the EKS cluster name (case-sensitive) +- Use `search` tool with query=`nodeadm|NodeConfig|node.eks.aws` to check AL2023 nodeadm configuration (if applicable) + +SHOULD: +- Use `search` tool with query=`Too Many Requests|throttl|rate limit|DescribeCluster` to check for API throttling during bootstrap + - If found: pass --apiserver-endpoint, --b64-cluster-ca, --dns-cluster-ip directly to avoid DescribeCluster API call + +### 2E — Security Groups and NACLs + +MUST: +- Use `search` tool with query=`security group|sg-|inbound|outbound|egress|ingress` to find security group references + - Control plane SG must allow inbound 443 from worker node SG + - Worker node SG must allow outbound 443 to control plane SG and 10250 from control plane SG + - Worker node SG must allow outbound 443 to 0.0.0.0/0 (for ECR, STS, etc.) or to VPC endpoints +- Use `search` tool with query=`NACL|network ACL|acl-` to check for NACL restrictions + - NACLs must allow ports 80, 443, and 1025-65535 inbound and outbound + +### 2F — Tagging + +SHOULD: +- Use `search` tool with query=`kubernetes.io/cluster|tag.*owned|tag.*shared` to check instance tags + - Node must have tag: kubernetes.io/cluster/ = owned + +### 2G — AMI Issues + +SHOULD: +- Use `search` tool with query=`ami-|AMI|image.*id|kubelet.*not found|containerd.*not found` to check AMI + - Custom AMIs must include kubelet, containerd/docker, aws-iam-authenticator, and bootstrap.sh + - AMI must match the cluster's Kubernetes version +- Use `search` tool with query=`Not authorized for images|private.*AMI|Windows.*AMI` to check for AMI access issues + +### 2H — Subnet IP Exhaustion + +MAY: +- Use `search` tool with query=`InsufficientFreeAddresses|no available IP|subnet.*full` to check for IP exhaustion + - If subnet has no available IPs, node gets an IP but pods cannot — or node may fail to launch entirely + - Resolution: add secondary CIDR to VPC or use different subnets + +### 2I — Control Plane kube-audit Logs + +SHOULD: +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="certificatesigningrequests" to check for node CSR requests and whether they were approved or denied +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="aws-auth" to check for recent aws-auth ConfigMap changes that may have removed the node role mapping +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="access" to check for access entry create/update/delete events +- Correlate timestamps of auth configuration changes with the node join failure — a recently removed mapping is a common root cause + +### Timeline Correlation + +MUST: +- Use `correlate` tool with instanceId and pivotEvent set to the first error found above to build a timeline of the join failure sequence + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from all relevant findings +- State root cause with the specific failure domain identified: + - IAM: missing aws-auth entry, wrong ARN format, missing node role policies, STS endpoint not activated + - Network: SG blocking 443, missing NAT/IGW, missing VPC endpoints for private cluster, route table misconfiguration + - DNS: VPC DNS disabled, missing DHCP options, node hostname resolution failure + - Bootstrap: wrong cluster name, missing private cluster args, cloud-init failure, API throttling + - Security rules: NACL blocking required ports + - Tags: missing kubernetes.io/cluster tag + - AMI: missing required components, version mismatch, private AMI access denied + - IP exhaustion: subnet out of IPs +- Recommend specific fix (operator action — not available via MCP tools) +- Cross-reference: if the issue is specifically Unauthorized + kubelet restart, also see A2 SOP + +SHOULD: +- Include the specific log evidence from search results +- Include the failure domain and sub-category +- Provide the exact operator remediation command or configuration change needed + +MAY: +- Recommend using managed node groups to avoid manual aws-auth management +- Recommend the AWSSupport-TroubleshootEKSWorkerNode SSM runbook for additional automated diagnostics +- Recommend passing bootstrap args explicitly to avoid DescribeCluster API throttling + +## Guardrails + +escalation_conditions: + - "Node role is correctly mapped and network is open but node still cannot join — possible control plane issue" + - "API server endpoint completely unreachable from VPC — possible VPC endpoint or peering misconfiguration" + - "Multiple nodes failing to join simultaneously — check via cluster_health for cluster-level issues" + - "Managed node group stuck in Create failed for > 15 minutes" + - "Private cluster with no VPC endpoints configured — requires infrastructure changes" + - "AMI missing required EKS components — requires new AMI build" + +safety_ratings: + - "Log collection (collect), search, errors, network_diagnostics, correlate, compare_nodes, cluster_health: GREEN (read-only)" + - "Update aws-auth ConfigMap or create access entry: YELLOW — operator action, not available via MCP tools" + - "Modify security groups or NACLs: YELLOW — operator action, not available via MCP tools" + - "Create VPC endpoints: YELLOW — operator action, not available via MCP tools" + - "Modify bootstrap user data / launch template: YELLOW — operator action, not available via MCP tools" + - "Replace node or node group: RED — operator action, requires approval" + +## Common Issues + +- symptoms: "search returns Unauthorized or 401 in kubelet logs" + diagnosis: "aws-auth ConfigMap or EKS access entry missing node IAM role. Common mistake: using instance profile ARN instead of role ARN, or role ARN contains a path." + resolution: "Operator action: add node role ARN to aws-auth via 'eksctl create iamidentitymapping --cluster --arn --group system:bootstrappers --group system:nodes' or create an EC2_linux access entry." + +- symptoms: "search returns TLS handshake timeout on port 443" + diagnosis: "Security group or NACL blocking outbound 443 from worker node to EKS API server endpoint." + resolution: "Operator action: update worker node security group to allow outbound TCP 443 to the cluster security group and 0.0.0.0/0. Check NACLs allow ports 443 and 1025-65535." + +- symptoms: "search returns connection refused or connection timed out to API endpoint" + diagnosis: "Cluster private endpoint not enabled, or node is in private subnet without NAT gateway, or VPC endpoints missing for private cluster." + resolution: "Operator action: enable private endpoint access on cluster, or add NAT gateway to private subnet route table, or create VPC endpoints (ec2, ecr.api, ecr.dkr, sts, s3)." + +- symptoms: "search returns 'node not found' error in kubelet logs" + diagnosis: "VPC missing DHCP options for domain-name and domain-name-servers. Node cannot resolve its own hostname." + resolution: "Operator action: create DHCP options set with domain-name=.compute.internal and domain-name-servers=AmazonProvidedDNS, associate with VPC." + +- symptoms: "search returns InvalidClientTokenId" + diagnosis: "Regional STS endpoint not activated for this AWS region." + resolution: "Operator action: activate the regional STS endpoint in IAM console under Account Settings > STS > Endpoints." + +- symptoms: "search returns Too Many Requests or DescribeCluster throttling" + diagnosis: "API throttling during bootstrap when many nodes launch simultaneously. bootstrap.sh calls DescribeCluster API." + resolution: "Operator action: pass --apiserver-endpoint, --b64-cluster-ca, --dns-cluster-ip directly in user data to skip DescribeCluster call." + +- symptoms: "search returns bootstrap.sh not found or cloud-init error" + diagnosis: "AMI is not EKS-optimized or is missing required components (kubelet, containerd, bootstrap.sh)." + resolution: "Operator action: use an official EKS-optimized AMI, or ensure custom AMI includes all required components and matches cluster Kubernetes version." + +- symptoms: "search returns ClusterName mismatch or wrong cluster name in kubeconfig" + diagnosis: "Bootstrap user data has incorrect ClusterName parameter. Case-sensitive exact match required." + resolution: "Operator action: fix ClusterName in launch template user data to exactly match the EKS cluster name." + +- symptoms: "search returns kubernetes.io/cluster tag missing" + diagnosis: "Node not tagged as owned by the cluster. Required for node discovery." + resolution: "Operator action: add tag kubernetes.io/cluster/=owned to the EC2 instance or launch template." + +- symptoms: "search returns AccessDenied for AmazonEKSWorkerNodePolicy or AmazonEKS_CNI_Policy" + diagnosis: "Node IAM role missing required managed policies, or SCP/permissions boundary blocking the policies." + resolution: "Operator action: attach AmazonEKSWorkerNodePolicy, AmazonEKS_CNI_Policy, and AmazonEC2ContainerRegistryReadOnly (or AmazonEC2ContainerRegistryPullOnly for newer setups) to the node IAM role. Also verify the cluster IAM role has AmazonEKSClusterPolicy and the correct trust policy for eks.amazonaws.com." + +- symptoms: "errors tool returns no findings, node simply never appears" + diagnosis: "Bootstrap script may not have run at all. Check cloud-init logs for user data execution." + resolution: "Operator action: verify launch template user data is correctly formatted (MIME multipart for AL2023, bash script for AL2). Check cloud-init output log." + +- symptoms: "search returns AutoScalingGroupNotFound or managed node group shows Degraded" + diagnosis: "The Auto Scaling Group referenced by the managed node group was deleted or cannot be found." + resolution: "Operator action: delete the degraded node group and create a new one. The ASG cannot be recovered once deleted." + +- symptoms: "search returns ClusterUnreachable or etcd database size exceeded" + diagnosis: "EKS control plane etcd database has exceeded 8GB, making the cluster unreachable. Nodes cannot join because the API server is unavailable." + resolution: "Operator action: reduce the number of objects in the cluster (delete unused ConfigMaps, Secrets, Events). Contact AWS Support if the cluster remains unreachable." + +- symptoms: "search returns AutoScalingGroupInvalidConfiguration or subnet mismatch" + diagnosis: "Node group subnets do not match the cluster subnets, or the specified subnets no longer exist." + resolution: "Operator action: delete the node group and recreate with correct subnets that match the cluster VPC configuration." + +- symptoms: "search returns Ec2SecurityGroupNotFound" + diagnosis: "The security group referenced by the node group was deleted. Managed node groups cannot recover from a deleted SG." + resolution: "Operator action: delete the node group and create a new one with a valid security group. The deleted SG cannot be re-associated." + +- symptoms: "search returns Ec2LaunchTemplateNotFound or LaunchTemplateVersionMismatch" + diagnosis: "The launch template or the specified version was deleted or does not exist." + resolution: "Operator action: update the node group to use a valid launch template version, or delete and recreate the node group." + +- symptoms: "search returns AsgInstanceLaunchFailures or InsufficientInstanceCapacity for Spot" + diagnosis: "Spot instance capacity unavailable for the requested instance type in the specified AZ." + resolution: "Operator action: use mixed instance types in the node group to increase Spot availability. Configure multiple instance types across multiple AZs." + +- symptoms: "search returns IamInstanceProfileNotFound or IamNodeRoleNotFound" + diagnosis: "The IAM instance profile or node role referenced by the node group was deleted." + resolution: "Operator action: delete the degraded node group and create a new one with a valid IAM role and instance profile." + +## Examples + +``` +# Step 1: Collect logs from the node that failed to join +collect(instanceId="i-0abc123def456") +status(executionId="") + +# Step 2: Get all findings +errors(instanceId="i-0abc123def456", severity="critical") + +# Step 3: Wide search for join failure patterns +search(instanceId="i-0abc123def456", query="Unauthorized|TLS handshake|connection refused|InvalidClientTokenId|not found") + +# Step 4: Check bootstrap execution +search(instanceId="i-0abc123def456", query="cloud-init|bootstrap.sh|/etc/eks/bootstrap") + +# Step 5: Check network path to API server +network_diagnostics(instanceId="i-0abc123def456", sections="routes,dns,eni") + +# Step 6: Check IAM / auth errors +search(instanceId="i-0abc123def456", query="Unauthorized|AccessDenied|aws-auth|iamidentitymapping") + +# Step 7: Check security group / NACL issues +search(instanceId="i-0abc123def456", query="security group|NACL|DROP|REJECT") + +# Step 8: Check STS endpoint +search(instanceId="i-0abc123def456", query="InvalidClientTokenId|sts.*endpoint|regional.*sts") + +# Step 9: Check DNS +search(instanceId="i-0abc123def456", query="node.*not found|DHCP|domain-name|NXDOMAIN") + +# Step 10: Check VPC endpoints (private clusters) +search(instanceId="i-0abc123def456", query="vpc.*endpoint|vpce-|PrivateLink|private.*endpoint") + +# Step 11: Correlate timeline +correlate(instanceId="i-0abc123def456", pivotEvent="", timeWindow=300) + +# Step 12: Generate summary +summarize(instanceId="i-0abc123def456", finding_ids=["F-001","F-002","F-003"]) +``` + +## Output Format + +```yaml +root_cause: "" +failure_domain: "" +evidence: + - type: search + content: "" + - type: network_diagnostics + content: "" + - type: correlate + content: "" +blast_radius: "node () or node group ()" +severity: CRITICAL +mitigation: + immediate: "Operator: " + long_term: "Use managed node groups, pass bootstrap args explicitly, monitor node group health" +cross_reference: + - "A2 if specifically Unauthorized + kubelet restart loop" + - "H1 if node role missing IAM permissions for other AWS services" + - "D5 if DNS resolution failures extend beyond node join" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/B1-kubelet-configuration-errors.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/B1-kubelet-configuration-errors.md new file mode 100644 index 0000000..fad9917 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/B1-kubelet-configuration-errors.md @@ -0,0 +1,119 @@ +--- +title: "B1 — Kubelet Configuration Errors" +description: "Diagnose kubelet startup failures due to misconfiguration (cgroup driver mismatch, invalid config, wrong max-pods/dns-cluster-ip)" +status: active +severity: HIGH +triggers: + - "misconfiguration: kubelet cgroup driver" + - "failed to run Kubelet" + - "invalid configuration" + - "Failed to create cgroup" +owner: devops-agent +objective: "Identify the specific kubelet misconfiguration and restore kubelet to running state" +context: "Kubelet fails to start or crashes on startup due to configuration errors. Common causes include cgroup driver mismatch between kubelet and containerd, invalid config JSON, or wrong DNS/max-pods settings." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check node and pod state before any log collection: + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — verify the node is Ready or check what condition it's in + - Check node details: `kubectl describe node ` (via EKS MCP `read_k8s_resource`) — look at kubelet version, conditions, and capacity + - List pods on the affected node: `kubectl get pods --all-namespaces --field-selector spec.nodeName=` (via EKS MCP `list_k8s_resources` with field_selector) — check for pods in CrashLoopBackOff or Error state that indicate kubelet config issues +- **PREREQUISITE — Is kubelet running?** Before investigating configuration, verify the kubelet process is alive: + - Use `collect` tool with instanceId to gather logs from the affected node + - Use `status` tool with executionId to poll until collection completes + - Use `search` tool with instanceId and query=`Active: active \(running\)|kubelet.*started|kubelet.service.*running` and logTypes=`kubelet` — if NO matches, kubelet is stopped/dead. That is the root cause, not a config issue. + - Use `search` tool with instanceId and query=`Active: inactive|Active: failed|kubelet.service.*dead|kubelet.service.*failed` — if matches found, kubelet is stopped. Report "kubelet service not running" as root cause before investigating config. + - ONLY if kubelet is confirmed running but misbehaving, proceed to config investigation below. +- Use `errors` tool with instanceId to get pre-indexed findings — look for kubelet config errors +- Use `search` tool with instanceId and query=`failed to run Kubelet|invalid configuration|cgroup driver|Failed to create cgroup` to find config failure evidence + +SHOULD: +- Use `search` tool with query=`kubelet.*config|config.json` and logTypes=`kubelet` to find kubelet configuration content in collected logs +- Use `search` tool with query=`SystemdCgroup|cgroupDriver` to check cgroup driver settings + +MAY: +- Use `compare_nodes` tool with instanceIds of broken + healthy node to diff kubelet config findings +- Use `search` tool with query=`cloud-init|bootstrap` to check for bootstrap-time config errors + +## Phase 2 — Enrich + +MUST: +- Use `search` tool with query=`cgroup driver` to detect cgroup driver mismatch — both kubelet and containerd must use systemd +- Use `search` tool with query=`invalid configuration|parse error|json.*error` to detect config file parse errors +- Use `search` tool with query=`dns-cluster-ip|clusterDNS` to verify DNS IP matches service CIDR + +SHOULD: +- Use `search` tool with query=`max-pods|maxPods` to check max-pods setting against instance type ENI limits +- Use `correlate` tool with instanceId to build timeline of kubelet startup attempts and failures + +MAY: +- Use `compare_nodes` tool to compare config between broken and healthy nodes in the same node group +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="kubelet-config" to check for recent kubelet ConfigMap changes that may have introduced the misconfiguration +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="configmap" to check for other ConfigMap mutations in kube-system that affect kubelet behavior + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from config-related findings +- State root cause: specific configuration error with evidence from findings +- Recommend fix: exact config change needed (operator action) +- Confirm kubelet should start successfully after fix + +SHOULD: +- Include the offending config line from search results +- Provide corrected config snippet + +MAY: +- Recommend managed node groups to avoid manual kubelet config + +## Guardrails + +escalation_conditions: + - "Kubelet fails to start after config correction" + - "Config file is managed by automation and cannot be manually edited" + - "Multiple nodes in the node group have the same misconfiguration" + +safety_ratings: + - "Log collection (collect), search, errors, correlate: GREEN (read-only)" + - "Edit kubelet config: YELLOW — operator action, not available via MCP tools" + - "Restart kubelet: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "search for cgroup driver returns mismatch between kubelet and containerd" + diagnosis: "Kubelet and containerd using different cgroup drivers" + resolution: "Operator action: ensure both use systemd. Set SystemdCgroup=true in containerd config and cgroupDriver: systemd in kubelet config." + +- symptoms: "search for dns-cluster-ip shows wrong value" + diagnosis: "dns-cluster-ip does not match service CIDR" + resolution: "Operator action: set --dns-cluster-ip to match kube-dns ClusterIP (usually 10.100.0.10 or 172.20.0.10)" + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Get config-related findings +errors(instanceId="i-0abc123def456") +# Step 3: Search for specific config errors +search(instanceId="i-0abc123def456", query="cgroup driver|invalid configuration|failed to run Kubelet") +# Step 4: Check cgroup settings +search(instanceId="i-0abc123def456", query="SystemdCgroup|cgroupDriver") +``` + +## Output Format + +```yaml +root_cause: "" +evidence: + - type: finding + content: "" + - type: config_search + content: "" +severity: HIGH +mitigation: + immediate: "Operator: fix config and restart kubelet" + long_term: "Use managed node groups for automatic kubelet configuration" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/B2-eviction-manager-issues.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/B2-eviction-manager-issues.md new file mode 100644 index 0000000..3e336d1 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/B2-eviction-manager-issues.md @@ -0,0 +1,131 @@ +--- +title: "B2 — Eviction Manager Issues" +description: "Diagnose unexpected pod evictions caused by DiskPressure, MemoryPressure, or PIDPressure" +status: active +severity: HIGH +triggers: + - "eviction manager: attempting to reclaim" + - "eviction_signal.*memory.available" + - "node has conditions.*DiskPressure" + - "failed to garbage collect required amount of images" +owner: devops-agent +objective: "Identify the eviction trigger (disk, memory, or PID pressure), stop the eviction storm, and restore node stability" +context: "Kubelet eviction manager kills pods when node resources drop below configured thresholds. This can cascade into eviction storms where many pods are killed rapidly, causing service disruption." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check node and pod state before any log collection: + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — look for MemoryPressure, DiskPressure conditions that trigger evictions + - Check node details: `kubectl describe node ` (via EKS MCP `read_k8s_resource`) — look at Conditions and recent events for eviction activity + - List pods on the affected node: `kubectl get pods --all-namespaces --field-selector spec.nodeName=` (via EKS MCP `list_k8s_resources` with field_selector) — check for Evicted pods, pods in Terminating state, or pods being rescheduled +- Use `collect` tool with instanceId to gather logs from the affected node +- Use `status` tool with executionId to poll until collection completes +- Use `quick_triage` tool with instanceId to get combined validate + errors + triage in one call +- Use `search` tool with instanceId and query=`eviction manager|attempting to reclaim|DiskPressure|MemoryPressure|PIDPressure` to find eviction evidence + +SHOULD: +- Use `storage_diagnostics` tool with instanceId to check disk and inode usage from collected logs +- Use `search` tool with query=`garbage collect|image.*prune|crictl` to check image GC status + +MAY: +- Use `cluster_health` tool with clusterName to check if multiple nodes are in eviction state +- Use `compare_nodes` tool to compare resource pressure across nodes + +## Phase 2 — Enrich + +MUST: +- Use `errors` tool with instanceId and severity=critical to identify which eviction signal triggered: DiskPressure, MemoryPressure, or PIDPressure +- For DiskPressure: use `storage_diagnostics` tool with instanceId to get disk utilization details +- For MemoryPressure: use `search` tool with query=`oom-killer|OOMKilled|out of memory` to check for OOM kills +- For PIDPressure: use `search` tool with query=`pid_max|too many processes|fork.*failed` to check PID exhaustion + +SHOULD: +- Use `correlate` tool with instanceId and pivotEvent=`eviction` to build timeline of eviction events +- Use `search` tool with query=`eviction.*threshold|hard-eviction|soft-eviction` to check kubelet eviction threshold configuration + +MAY: +- Use `compare_nodes` tool to compare resource usage between affected and healthy nodes + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from eviction-related findings +- State root cause: specific pressure type with resource utilization evidence +- Recommend immediate action to stop eviction storm (operator action) +- Recommend long-term fix to prevent recurrence + +SHOULD: +- List evicted pods identified from findings +- Include disk/memory/PID utilization evidence from diagnostics tools + +MAY: +- Recommend ephemeral-storage limits, image GC thresholds, or PID limits + +## Guardrails + +escalation_conditions: + - "Eviction storm affecting critical system pods (kube-proxy, aws-node)" + - "Multiple nodes in eviction state simultaneously (check via cluster_health)" + - "Disk usage at 100% and garbage collection unable to free space" + +safety_ratings: + - "Log collection (collect), search, errors, storage_diagnostics: GREEN (read-only)" + - "Prune images, delete pods: YELLOW — operator action, not available via MCP tools" + - "Modify eviction thresholds: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "errors tool returns DiskPressure findings, storage_diagnostics shows >85% disk used" + diagnosis: "Disk full from container images, logs, or emptyDir volumes" + resolution: "Operator action: prune unused images (crictl rmi --prune), delete evicted pods, identify large emptyDir consumers" + +- symptoms: "errors tool returns MemoryPressure findings" + diagnosis: "Node memory exhausted by pod workloads" + resolution: "Operator action: identify memory-heavy pods, set system-reserved memory" + +- symptoms: "errors tool returns PIDPressure findings" + diagnosis: "Too many processes/threads on the node" + resolution: "Operator action: identify runaway process, set PID limits on containers" + +- symptoms: "errors tool returns DiskPressure findings, search shows 'Disk usage on image filesystem is over the high threshold, trying to free bytes'" + diagnosis: "Kubelet image garbage collection is failing to free enough space. The image-gc-high-threshold has been exceeded." + resolution: "Operator action: 1) Manually prune images: crictl rmi --prune. 2) Lower GC thresholds: set --image-gc-high-threshold=70 --image-gc-low-threshold=60 in kubelet config. 3) Increase EBS volume size or provision new nodes with larger root volumes." + +- symptoms: "storage_diagnostics shows root filesystem >85% used, search shows container log files consuming significant space" + diagnosis: "Container runtime log files (stdout/stderr) are not being rotated properly, consuming disk space." + resolution: "Operator action: configure containerd log rotation in /etc/containerd/config.toml — set log_file_max (max rotated files) and log_file_max_size (max size per file). Consider using a logging agent to ship logs externally." + +- symptoms: "errors tool returns DiskPressure findings, search shows pods without ephemeral-storage limits" + diagnosis: "Pods without ephemeral-storage limits can consume unlimited disk space on the node, triggering DiskPressure evictions for other pods." + resolution: "Operator action: set ephemeral-storage requests and limits on all pods. Example: resources.requests.ephemeral-storage=1Mi, resources.limits.ephemeral-storage=2Mi. Use LimitRange to enforce defaults." + +## Examples + +``` +# Step 1: One-shot triage +quick_triage(instanceId="i-0abc123def456") +# Step 2: Search for eviction events +search(instanceId="i-0abc123def456", query="eviction manager|attempting to reclaim") +# Step 3: Check storage if DiskPressure +storage_diagnostics(instanceId="i-0abc123def456") +# Step 4: Correlate eviction timeline +correlate(instanceId="i-0abc123def456", pivotEvent="eviction", timeWindow=300) +``` + +## Output Format + +```yaml +root_cause: "" +evidence: + - type: finding + content: "" + - type: diagnostics + content: "" +blast_radius: "node (), pods evicted" +severity: HIGH +mitigation: + immediate: "Operator: " + long_term: "Set resource limits, monitoring, scaling" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/B3-pleg-issues.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/B3-pleg-issues.md new file mode 100644 index 0000000..f8b08e9 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/B3-pleg-issues.md @@ -0,0 +1,138 @@ +--- +title: "B3 — PLEG (Pod Lifecycle Event Generator) Issues" +description: "Diagnose node NotReady caused by PLEG health check failures" +status: active +severity: HIGH +triggers: + - "PLEG is not healthy" + - "pleg was last seen active.*ago" + - "GenericPLEG.*relisting" +owner: devops-agent +objective: "Identify why PLEG is unhealthy and restore node stability" +context: "PLEG monitors pod lifecycle events by periodically relisting all containers. When containerd is slow or overloaded, PLEG relisting takes too long and kubelet reports the node as NotReady." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check node and pod state before any log collection: + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — PLEG issues cause NotReady state + - List pods on the affected node: `kubectl get pods --all-namespaces --field-selector spec.nodeName=` (via EKS MCP `list_k8s_resources` with field_selector) — check for pods stuck in Unknown, Terminating, or ContainerCreating state (PLEG failures prevent pod lifecycle updates) + - Check pod events: `kubectl describe pod ` (via EKS MCP `get_k8s_events`) for PLEG-related warnings +- Use `collect` tool with instanceId to gather logs from the affected node +- Use `status` tool with executionId to poll until collection completes +- **PREREQUISITE — Is containerd running?** PLEG relists containers via containerd — if containerd is dead, PLEG will always be unhealthy: + - Use `search` tool with instanceId and query=`Active: active \(running\)|containerd.*started|containerd.service.*running` and logTypes=`containerd` — if NO matches, containerd is stopped/dead. That is the root cause, not a PLEG-specific issue. + - Use `search` tool with instanceId and query=`Active: inactive|Active: failed|containerd.service.*dead|containerd.service.*failed` — if matches found, containerd is stopped. Report "containerd service not running — PLEG cannot relist containers" as root cause. + - ONLY if containerd is confirmed running, proceed to PLEG investigation below. +- Use `errors` tool with instanceId to get pre-indexed findings — look for PLEG health check failures +- Use `search` tool with instanceId and query=`PLEG is not healthy|PLEG.*relisting|pleg was last seen` to find PLEG failure evidence + +SHOULD: +- Use `search` tool with query=`containerd.*slow|containerd.*timeout|containerd.*error` and logTypes=`containerd` to check containerd health +- Use `storage_diagnostics` tool with instanceId to check if slow disk I/O is causing containerd slowness + +MAY: +- Use `cluster_health` tool with clusterName to check if multiple nodes show PLEG issues +- Use `search` tool with query=`crictl|container.*count` to estimate container density on the node + +## Phase 2 — Enrich + +MUST: +- Use `correlate` tool with instanceId and pivotEvent=`PLEG` to correlate PLEG unhealthy events with containerd latency +- Use `search` tool with query=`containerd` and logTypes=`containerd` to find slow operations or errors in containerd logs +- Use `errors` tool with severity=all to get full picture of node health issues + +SHOULD: +- Use `storage_diagnostics` tool with instanceId and sections=kubelet to check if disk I/O latency is elevated +- Use `search` tool with query=`container.*create|container.*start|container.*stop` to assess pod churn rate + +MAY: +- Use `compare_nodes` tool to compare PLEG-related findings between affected and healthy nodes + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from PLEG-related findings +- State root cause: PLEG unhealthy due to containerd overload, disk I/O, or high pod density +- Recommend mitigation based on root cause (operator action) +- Confirm node should return to Ready after remediation + +SHOULD: +- Include PLEG timing evidence from search results +- Include containerd latency evidence from findings + +MAY: +- Recommend spreading workloads across more nodes + +## Guardrails + +escalation_conditions: + - "PLEG unhealthy persists after containerd restart" + - "Multiple nodes showing PLEG issues simultaneously" + - "Disk I/O latency consistently >100ms (from storage_diagnostics)" + +safety_ratings: + - "Log collection (collect), search, errors, correlate, storage_diagnostics: GREEN (read-only)" + - "Restart containerd: YELLOW — operator action, not available via MCP tools" + - "Drain node: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "search for containerd service status returns Active: inactive or Active: failed" + diagnosis: "Containerd is stopped. PLEG cannot relist containers without the runtime, so it will always report unhealthy." + resolution: "Operator action: check journalctl -u containerd for startup errors, then restart containerd (systemctl restart containerd). PLEG should recover once containerd is running." + +- symptoms: "errors tool returns PLEG unhealthy findings, search shows containerd slow to respond" + diagnosis: "Containerd overloaded by high pod density or slow disk" + resolution: "Operator action: reduce pod count on node, check disk I/O, consider larger instance type with faster storage" + +- symptoms: "PLEG unhealthy findings correlate with high pod churn in correlate timeline" + diagnosis: "Rapid pod creation/deletion overwhelming container runtime" + resolution: "Operator action: reduce deployment rollout speed, spread across more nodes" + +- symptoms: "search for kubelet logs shows 'Skipping pod synchronization' with PLEG threshold exceeded (e.g., 'pleg was last seen active 4h5m ago; threshold is 3m0s')" + diagnosis: "PLEG has been unhealthy for an extended period. Kubelet is skipping all pod sync operations, meaning no pod lifecycle events are processed." + resolution: "Operator action: check kubelet and containerd logs for root cause. Restart kubelet (systemctl restart kubelet) if containerd is healthy. If containerd is also stuck, restart containerd first." + +- symptoms: "search for kubelet logs shows liveness/readiness probe failures ('Probe failed probeType=Liveness context deadline exceeded') alongside PLEG unhealthy" + diagnosis: "Frequent probe failures are contributing to PLEG overload. Each probe timeout adds latency to the PLEG relist cycle." + resolution: "Operator action: review probe configurations — increase timeoutSeconds, reduce frequency. Check if pods with security groups need DISABLE_TCP_EARLY_DEMUX=true or POD_SECURITY_GROUP_ENFORCING_MODE=standard." + +- symptoms: "search for kubelet logs shows 'disk usage and inodes count on following dirs took Xs' (>1 second) alongside PLEG unhealthy" + diagnosis: "Slow disk I/O is causing containerd and kubelet to stall during filesystem operations, which delays PLEG relisting. Monitor EBS volume IOPS and throughput." + resolution: "Operator action: check EBS volume type and IOPS limits (CloudWatch VolumeReadOps/VolumeWriteOps). Upgrade to gp3 with provisioned IOPS, or increase volume size for higher baseline IOPS." + +- symptoms: "search returns MemoryPressure or OOM alongside PLEG unhealthy" + diagnosis: "Instance-level OOM or memory pressure caused kubelet/containerd to stall, triggering PLEG unhealthy. Resource crunch is the root cause, not PLEG itself." + resolution: "Operator action: set CPU and memory limits on pods to prevent resource exhaustion. Consider using Container Insights to monitor node_cpu_utilization and node_memory_utilization." + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Get PLEG findings +errors(instanceId="i-0abc123def456") +# Step 3: Search for PLEG evidence +search(instanceId="i-0abc123def456", query="PLEG is not healthy|PLEG.*relisting") +# Step 4: Check containerd +search(instanceId="i-0abc123def456", query="containerd.*slow|containerd.*timeout", logTypes="containerd") +# Step 5: Correlate +correlate(instanceId="i-0abc123def456", pivotEvent="PLEG", timeWindow=120) +``` + +## Output Format + +```yaml +root_cause: "PLEG unhealthy — " +evidence: + - type: finding + content: "" + - type: containerd_search + content: "" +severity: HIGH +mitigation: + immediate: "Operator: reduce pod churn, check containerd health" + long_term: "Increase node resources, spread workloads" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/C1-image-pull-failures.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/C1-image-pull-failures.md new file mode 100644 index 0000000..7074ea4 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/C1-image-pull-failures.md @@ -0,0 +1,153 @@ +--- +title: "C1 — Image Pull Failures (ImagePullBackOff)" +description: "Diagnose pods stuck in ImagePullBackOff or ErrImagePull" +status: active +severity: HIGH +triggers: + - "failed to pull and unpack image" + - "401 Unauthorized" + - "403 Forbidden" + - "failed to resolve reference" + - "no space left on device" +owner: devops-agent +objective: "Identify why container images cannot be pulled and restore pod scheduling" +context: "Image pull failures prevent pods from starting. Causes range from auth issues (ECR token expired, missing imagePullSecrets) to network problems (DNS, SG blocking registry) to disk space exhaustion." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check pod and node state before any log collection: + - List pods in the affected namespace: `kubectl get pods -n -o wide` (via EKS MCP `list_k8s_resources`) + - Check pod status — ImagePullBackOff or ErrImagePull confirms this is the right SOP + - Check pod events: `kubectl describe pod ` (via EKS MCP `get_k8s_events`) for the exact image pull error message (auth failure, not found, timeout) + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — verify the node is Ready +- **PREREQUISITE — Is containerd running?** Before investigating image pull failures, verify the container runtime is alive: + - Use `collect` tool with instanceId to gather logs from the affected node + - Use `status` tool with executionId to poll until collection completes + - Use `search` tool with instanceId and query=`Active: active \(running\)|containerd.*started|containerd.service.*running` and logTypes=`containerd` — if NO matches, containerd is stopped/dead. That is the root cause, not an image pull issue. + - Use `search` tool with instanceId and query=`Active: inactive|Active: failed|containerd.service.*dead|containerd.service.*failed` — if matches found, containerd is stopped. Report "containerd service not running — image pulls impossible" as root cause. + - ONLY if containerd is confirmed running, proceed to image pull investigation below. +- **PREREQUISITE — Does the image exist?** Before investigating auth or network, check if the image reference is valid: + - Use `search` tool with instanceId and query=`manifest unknown|not found.*manifest|404.*manifest|repository.*not found|name unknown` — if matches found, the image name or tag does not exist in the registry. That is the root cause. Report "image not found in registry — verify image name and tag" immediately. + - ONLY if no "not found" errors, proceed to auth/network investigation below. +- Use `errors` tool with instanceId to get pre-indexed findings — look for image pull errors +- Use `search` tool with instanceId and query=`failed to pull|401 Unauthorized|403 Forbidden|failed to resolve reference|no space left` to find image pull failure evidence + +SHOULD: +- Use `search` tool with query=`ecr.*credential|imagePullSecret|registry.*auth` to check auth configuration +- Use `storage_diagnostics` tool with instanceId to check disk space from collected logs +- Use `search` tool with query=`toomanyrequests|rate limit|429|Too Many Requests` to check for Docker Hub or registry rate limiting + +MAY: +- Use `network_diagnostics` tool with instanceId and sections=dns to check DNS resolution for registry endpoints +- Use `search` tool with query=`IMDS|hop-limit|169.254.169.254` to check IMDS accessibility for ECR credential helper +- Use `search` tool with query=`vpce-|vpc.*endpoint|ecr.api|ecr.dkr` to check for VPC endpoints (required for private clusters pulling from ECR) + +## Phase 2 — Enrich + +MUST: +- Use `search` tool with query=`401|403` — auth issue (IRSA, node role, imagePullSecrets) +- Use `search` tool with query=`i/o timeout|dial tcp.*timeout` — DNS or network connectivity issue +- Use `search` tool with query=`no space left on device` — disk full (cross-ref with storage_diagnostics) +- Use `search` tool with query=`failed to resolve reference|not found|manifest unknown` — invalid image name/tag + +SHOULD: +- Use `correlate` tool with instanceId and pivotEvent=`failed to pull` to build timeline of pull failures +- Use `search` tool with query=`x509.*certificate` to check for proxy/firewall TLS interception issues + +MAY: +- Use `compare_nodes` tool to check if image pull failures are node-specific or cluster-wide + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from image-pull-related findings +- State root cause: specific pull failure reason with evidence +- Recommend targeted fix based on failure type (operator action) +- Confirm pods should transition to Running after fix + +SHOULD: +- Include the specific error message from findings +- Provide exact remediation steps for the operator + +MAY: +- Recommend IRSA for ECR access instead of node role +- Recommend proper IMDS hop limits for containerized workloads + +## Guardrails + +escalation_conditions: + - "All pods on node failing to pull images (node-wide issue)" + - "ECR service endpoint unreachable (potential AWS service issue)" + - "x509 certificate errors for ECR (potential proxy/firewall issue)" + +safety_ratings: + - "Log collection (collect), search, errors, storage_diagnostics, network_diagnostics: GREEN (read-only)" + - "Modify IAM roles, imagePullSecrets: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "search for containerd service status returns Active: inactive or Active: failed" + diagnosis: "Containerd is stopped. No images can be pulled without the container runtime." + resolution: "Operator action: check journalctl -u containerd for startup errors, then restart containerd (systemctl restart containerd)." + +- symptoms: "search returns manifest unknown or repository not found" + diagnosis: "Image name or tag does not exist in the registry. Typo in image reference or tag was never pushed." + resolution: "Operator action: verify image name and tag. Check ECR repository exists and the tag was pushed. Use 'aws ecr describe-images' to list available tags." + +- symptoms: "errors tool returns findings with 401 Unauthorized or 403 Forbidden" + diagnosis: "ECR auth failure. Node role missing registry permissions or ECR token expired." + resolution: "Operator action: verify node role has AmazonEC2ContainerRegistryReadOnly. For cross-account: configure ECR repository policy." + +- symptoms: "search for i/o timeout returns matches" + diagnosis: "Network cannot reach container registry. DNS or SG issue." + resolution: "Operator action: check DNS resolution, verify SG allows outbound 443 to registry endpoint." + +- symptoms: "storage_diagnostics shows disk full, search confirms no space left on device" + diagnosis: "Root volume full, cannot store pulled image layers." + resolution: "Operator action: prune unused images (crictl rmi --prune), increase root volume size." + +- symptoms: "search for IMDS hop-limit shows value of 1" + diagnosis: "IMDS hop limit is 1, pods cannot get ECR credentials via IMDS." + resolution: "Operator action: increase IMDS hop limit to 2 or use IRSA for ECR access." + +- symptoms: "search returns toomanyrequests, rate limit, or 429 Too Many Requests" + diagnosis: "Docker Hub rate limit exceeded. Anonymous pulls are limited to 100 pulls/6h, authenticated to 200 pulls/6h." + resolution: "Operator action: use ECR Public or a pull-through cache to avoid Docker Hub rate limits. Alternatively, authenticate Docker Hub pulls with imagePullSecrets containing a Docker Hub token." + +- symptoms: "search returns ECR repository policy AccessDenied or cross-account pull failure" + diagnosis: "ECR repository policy does not allow the node role or cross-account role to pull images. The repository-level policy is separate from the IAM policy on the node role." + resolution: "Operator action: update the ECR repository policy to allow ecr:GetDownloadUrlForLayer, ecr:BatchGetImage, ecr:BatchCheckLayerAvailability for the node role or cross-account principal. Use 'aws ecr set-repository-policy'." + +- symptoms: "search returns vpce- or VPC endpoint errors, or private cluster cannot reach ECR" + diagnosis: "Private cluster missing VPC endpoints for ECR. Private clusters (no internet access) require VPC endpoints for ecr.api, ecr.dkr, and an S3 gateway endpoint to pull images from ECR." + resolution: "Operator action: create VPC endpoints for com.amazonaws..ecr.api, com.amazonaws..ecr.dkr (interface endpoints), and com.amazonaws..s3 (gateway endpoint). Attach security groups allowing HTTPS (443) from the node subnets." + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Get image pull findings +errors(instanceId="i-0abc123def456") +# Step 3: Search for pull failures +search(instanceId="i-0abc123def456", query="failed to pull|401 Unauthorized|403 Forbidden") +# Step 4: Check disk space +storage_diagnostics(instanceId="i-0abc123def456") +# Step 5: Check DNS +network_diagnostics(instanceId="i-0abc123def456", sections="dns") +``` + +## Output Format + +```yaml +root_cause: "" +evidence: + - type: finding + content: "" +severity: HIGH +mitigation: + immediate: "Operator: " + long_term: "Use IRSA for ECR, set proper IMDS hop limits" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/C2-sandbox-creation-failures.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/C2-sandbox-creation-failures.md new file mode 100644 index 0000000..d9f8091 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/C2-sandbox-creation-failures.md @@ -0,0 +1,148 @@ +--- +title: "C2 — Sandbox Creation Failures" +description: "Diagnose pods stuck in ContainerCreating due to sandbox creation failures" +status: active +severity: HIGH +triggers: + - "failed to create sandbox" + - "failed to setup network for sandbox" + - "add cmd: failed to assign an IP address" + - "failed to create containerd task" +owner: devops-agent +objective: "Identify why container sandboxes cannot be created and restore pod scheduling" +context: "Sandbox creation is the first step in starting a container. Failures here block all pod creation on the node. Root causes include CNI/IP exhaustion, containerd runtime errors, disk/inode exhaustion, or runtime socket unavailability." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check pod and node state before any log collection: + - List pods in the affected namespace: `kubectl get pods -n -o wide` (via EKS MCP `list_k8s_resources`) + - Check pod status — pods stuck in ContainerCreating with sandbox errors confirms this SOP + - Check pod events: `kubectl describe pod ` (via EKS MCP `get_k8s_events`) for sandbox creation failure details + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — verify the node is Ready +- Use `collect` tool with instanceId to gather logs from the affected node +- Use `status` tool with executionId to poll until collection completes +- **PREREQUISITE — Is containerd running?** Sandbox creation requires the container runtime: + - Use `search` tool with instanceId and query=`Active: active \(running\)|containerd.*started|containerd.service.*running` and logTypes=`containerd` — if NO matches, containerd is stopped/dead. That is the root cause, not a sandbox-specific issue. + - Use `search` tool with instanceId and query=`Active: inactive|Active: failed|containerd.service.*dead|containerd.service.*failed` — if matches found, containerd is stopped. Report "containerd service not running — sandbox creation impossible" as root cause. + - ONLY if containerd is confirmed running, proceed to sandbox investigation below. +- **PREREQUISITE — Is aws-node (VPC CNI) running?** Most sandbox failures are CNI-related: + - Use `list_k8s_resources` with clusterName, kind=Pod, apiVersion=v1, namespace=kube-system, labelSelector=k8s-app=aws-node — check that aws-node pod on the affected node is Running. If CrashLoopBackOff or missing, that is the root cause. Report "aws-node (VPC CNI) not running on node — sandbox network setup will fail" immediately. + - ONLY if aws-node is confirmed running, proceed to sandbox investigation below. +- Use `errors` tool with instanceId to get pre-indexed findings — look for sandbox creation errors +- Use `search` tool with instanceId and query=`failed to create sandbox|failed to setup network|failed to assign an IP|failed to create containerd task` to find sandbox failure evidence + +SHOULD: +- Use `network_diagnostics` tool with instanceId and sections=cni,ipamd to check CNI/IP allocation status +- Use `storage_diagnostics` tool with instanceId to check disk and inode usage +- Use `search` tool with query=`Resource temporarily unavailable|cannot allocate memory|fork.*failed|too many open files` to check for PID/file limit exhaustion or zombie processes + +MAY: +- Use `search` tool with query=`containerd.*socket|runtime not ready` to check containerd health +- Use `cluster_health` tool with clusterName to check if multiple nodes are affected + +## Phase 2 — Enrich + +MUST: +- Use `search` tool with query=`failed to assign an IP` — CNI/IP exhaustion (cross-ref with D1 SOP) +- Use `search` tool with query=`failed to create containerd task` — runtime or disk issue +- Use `storage_diagnostics` tool to check inode usage — 100% means inode exhaustion (cross-ref with C3 SOP) +- Use `search` tool with query=`runtime not ready|containerd.*unavailable` — containerd socket issue + +SHOULD: +- Use `network_diagnostics` tool with sections=cni,ipamd to get IP allocation details +- Use `correlate` tool with instanceId and pivotEvent=`failed to create sandbox` to build failure timeline +- Use `search` tool with query=`DISABLE_TCP_EARLY_DEMUX|POD_SECURITY_GROUP_ENFORCING_MODE|strict.*mode|security.*groups.*pods` to check for Security Groups for Pods strict mode issues — DISABLE_TCP_EARLY_DEMUX must be set to true when POD_SECURITY_GROUP_ENFORCING_MODE is strict +- Use `search` tool with query=`aws-node.*serviceaccount|overwrite|managed.*plugin|Pod Identity|EKS Pod Identity` to check if the aws-node service account was overwritten by a managed CNI plugin update, or if Pod Identity is configured for aws-node (Pod Identity is NOT supported for aws-node — must use IRSA or node role) + +MAY: +- Use `compare_nodes` tool to check if sandbox failures are node-specific + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from sandbox-related findings +- State root cause: specific sandbox failure reason with evidence +- Recommend targeted fix (operator action) +- Cross-reference with related SOPs (D1 for IP exhaustion, C3 for inode exhaustion) + +SHOULD: +- Include evidence from network_diagnostics or storage_diagnostics + +MAY: +- Recommend prefix delegation for IP density +- Recommend larger root volumes + +## Guardrails + +escalation_conditions: + - "Containerd socket unresponsive after restart" + - "All pods on node stuck in ContainerCreating" + - "IP exhaustion across multiple nodes (check via cluster_health)" + +safety_ratings: + - "Log collection (collect), search, errors, network_diagnostics, storage_diagnostics: GREEN (read-only)" + - "Restart containerd, clean containers: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "search for containerd service status returns Active: inactive or Active: failed" + diagnosis: "Containerd is stopped. No containers can be created without the runtime." + resolution: "Operator action: check journalctl -u containerd for startup errors, then restart containerd (systemctl restart containerd)." + +- symptoms: "list_k8s_resources returns aws-node pod in CrashLoopBackOff or missing on the affected node" + diagnosis: "VPC CNI DaemonSet not running on the node. Sandbox network setup will fail for all new pods." + resolution: "Operator action: check aws-node logs (kubectl logs -n kube-system -l k8s-app=aws-node). Common fixes: update VPC CNI addon, check IAM permissions, verify subnet has available IPs." + +- symptoms: "search returns failed to assign an IP address, network_diagnostics shows IP exhaustion" + diagnosis: "VPC CNI IP exhaustion. See SOP D1." + resolution: "Operator action: check subnet IPs, ENI limits. Enable prefix delegation." + +- symptoms: "search returns failed to create containerd task, storage_diagnostics shows disk/inode issues" + diagnosis: "Containerd runtime error, often disk or inode related" + resolution: "Operator action: clean stopped containers and unused images. Increase root volume." + +- symptoms: "storage_diagnostics shows inodes at 100%" + diagnosis: "Too many small files from container layers or log rotation failures" + resolution: "Operator action: clean stopped containers and unused images. See SOP C3." + +- symptoms: "search returns Resource temporarily unavailable, fork failed, or too many open files" + diagnosis: "PID limit or file descriptor limit exhausted, or zombie processes consuming PIDs. The node cannot create new processes for sandbox setup." + resolution: "Operator action: check 'ps aux | wc -l' for process count, check /proc/sys/kernel/pid_max and /proc/sys/fs/file-max. Kill zombie processes. Increase PID limits if needed. Identify the pod consuming excessive PIDs." + +- symptoms: "search returns DISABLE_TCP_EARLY_DEMUX error or Security Groups for Pods strict mode failure" + diagnosis: "When POD_SECURITY_GROUP_ENFORCING_MODE is set to strict, DISABLE_TCP_EARLY_DEMUX must be set to true on the aws-node DaemonSet. Without this, sandbox network setup fails for pods using security groups." + resolution: "Operator action: set DISABLE_TCP_EARLY_DEMUX=true in the aws-node DaemonSet environment variables. Alternatively, use standard mode instead of strict mode." + +- symptoms: "search returns aws-node service account overwrite or Pod Identity errors for aws-node" + diagnosis: "The aws-node service account annotation was overwritten by a managed CNI addon update, or Pod Identity was configured for aws-node. EKS Pod Identity is NOT supported for the aws-node DaemonSet — it must use IRSA or the node IAM role." + resolution: "Operator action: if using IRSA, re-annotate the aws-node service account with eks.amazonaws.com/role-arn. If Pod Identity was configured, remove it and switch to IRSA. When updating the VPC CNI addon, use --resolve-conflicts PRESERVE to avoid overwriting the service account annotation." + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Get sandbox failure findings +errors(instanceId="i-0abc123def456") +# Step 3: Check CNI/IP status +network_diagnostics(instanceId="i-0abc123def456", sections="cni,ipamd") +# Step 4: Check disk/inodes +storage_diagnostics(instanceId="i-0abc123def456") +``` + +## Output Format + +```yaml +root_cause: "" +evidence: + - type: finding + content: "" + - type: diagnostics + content: "" +severity: HIGH +mitigation: + immediate: "Operator: " + long_term: "Enable prefix delegation, increase root volume" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/C3-overlayfs-inode-exhaustion.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/C3-overlayfs-inode-exhaustion.md new file mode 100644 index 0000000..05101be --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/C3-overlayfs-inode-exhaustion.md @@ -0,0 +1,116 @@ +--- +title: "C3 — OverlayFS / Inode Exhaustion" +description: "Diagnose no space left on device errors caused by inode exhaustion" +status: active +severity: HIGH +triggers: + - "no space left on device" + - "DiskPressure" +owner: devops-agent +objective: "Confirm inode exhaustion and free inodes to restore pod operations" +context: "Inode exhaustion occurs when the filesystem runs out of inode entries even though disk space is available. Common with many small files from container layers or log rotation failures." +--- + +## Phase 1 — Triage + +FIRST — Check node and pod state before collecting logs: +- Use `list_k8s_resources` with clusterName, kind=Node, apiVersion=v1 to list all nodes and identify the affected node's status (Ready/NotReady) and conditions (DiskPressure, MemoryPressure, PIDPressure) +- Use `read_k8s_resource` with clusterName, kind=Node, apiVersion=v1, name= to get detailed node conditions — look for DiskPressure=True which indicates inode or disk exhaustion +- Use `list_k8s_resources` with clusterName, kind=Pod, apiVersion=v1, fieldSelector=spec.nodeName= to list all pods on the affected node — check for pods in CrashLoopBackOff, Error, or ContainerCreating state (stuck creating due to no space) +- Use `get_k8s_events` with clusterName, kind=Node, name= to check for DiskPressure, EvictionThresholdMet, or FreeDiskSpaceFailed events + +MUST: +- Use `collect` tool with instanceId to gather logs from the affected node +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId to get pre-indexed findings — look for disk/inode errors +- Use `storage_diagnostics` tool with instanceId to get disk and inode usage from collected logs + +SHOULD: +- Use `search` tool with instanceId and query=`no space left on device|DiskPressure|inode` to find inode exhaustion evidence +- Use `search` tool with query=`garbage collect|image.*prune` to check image GC status + +MAY: +- Use `cluster_health` tool with clusterName to check if multiple nodes have disk pressure + +## Phase 2 — Enrich + +MUST: +- Review `storage_diagnostics` output for inode utilization — confirm IUse% at or near 100% +- Identify which filesystem is inode-exhausted from storage_diagnostics results +- Use `search` tool with query=`containerd|image.*layer|overlay` to identify inode consumers + +SHOULD: +- Use `search` tool with query=`image garbage collection|imageGCHighThreshold` to check if image GC is working +- Use `correlate` tool with instanceId and pivotEvent=`no space left` to build timeline + +MAY: +- Use `compare_nodes` tool to compare disk/inode usage across nodes + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from disk-related findings +- State root cause: inode exhaustion with IUse% evidence from storage_diagnostics +- Recommend immediate cleanup actions (operator action) +- Recommend long-term prevention + +SHOULD: +- Identify top inode consumers from storage_diagnostics + +MAY: +- Recommend XFS filesystem for better inode handling + +## Guardrails + +escalation_conditions: + - "Inode cleanup does not free sufficient inodes" + - "Root filesystem requires resize" + - "System pods affected by inode exhaustion" + +safety_ratings: + - "Log collection (collect), search, errors, storage_diagnostics: GREEN (read-only)" + - "Clean containers, prune images: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "storage_diagnostics shows IUse% near 100% but disk space available" + diagnosis: "Inode exhaustion from too many small files (container layers, logs)" + resolution: "Operator action: clean stopped containers and prune unused images. Increase root volume." + +- symptoms: "storage_diagnostics shows both disk usage >85% AND IUse% near 100%" + diagnosis: "Both disk space and inodes are exhausted. Container images, logs, and emptyDir volumes are consuming both resources." + resolution: "Operator action: 1) Prune unused images: crictl rmi --prune. 2) Clean stopped containers. 3) Increase EBS root volume size. 4) Set ephemeral-storage limits on pods to prevent unbounded disk usage." + +- symptoms: "search returns 'failed to garbage collect required amount of images' alongside DiskPressure" + diagnosis: "Kubelet image garbage collection cannot free enough space. All images may be in use by running containers." + resolution: "Operator action: 1) Lower GC thresholds: --image-gc-high-threshold=70 --image-gc-low-threshold=60. 2) Reduce number of unique images on the node. 3) Use smaller base images. 4) Provision new nodes with larger root volumes." + +- symptoms: "search returns 'no space left on device' during container creation but df -h shows disk space available" + diagnosis: "Inode exhaustion confirmed — filesystem has space but no free inodes. Common with ext4 filesystems that have many small container layer files." + resolution: "Operator action: clean up container layers and unused images. For long-term fix, consider XFS filesystem (dynamic inode allocation) or increase root volume size (more inodes allocated at mkfs time)." + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Check storage +storage_diagnostics(instanceId="i-0abc123def456") +# Step 3: Get disk-related findings +errors(instanceId="i-0abc123def456") +# Step 4: Search for inode evidence +search(instanceId="i-0abc123def456", query="no space left on device|inode") +``` + +## Output Format + +```yaml +root_cause: "Inode exhaustion on " +evidence: + - type: storage_diagnostics + content: "IUse% = " +severity: HIGH +mitigation: + immediate: "Operator: clean stopped containers and unused images" + long_term: "Increase root volume, configure image GC thresholds" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D1-vpc-cni-ip-allocation-failures.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D1-vpc-cni-ip-allocation-failures.md new file mode 100644 index 0000000..b200181 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D1-vpc-cni-ip-allocation-failures.md @@ -0,0 +1,145 @@ +--- +title: "D1 — VPC CNI IP Allocation Failures" +description: "Diagnose pods stuck in ContainerCreating due to VPC CNI IP address exhaustion" +status: active +severity: CRITICAL +triggers: + - "failed to assign an IP address to container" + - "no available IP addresses" + - "failed to allocate ENI" + - "InsufficientFreeAddresses" + - "ipamd.*failed to increase IP pool" +owner: devops-agent +objective: "Identify the IP allocation bottleneck and restore pod networking" +context: "VPC CNI assigns real VPC IP addresses to pods. When IPs are exhausted (subnet depletion, ENI limits, or ipamd issues), new pods cannot get network interfaces and remain stuck in ContainerCreating." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check pod and node state before any log collection: + - List pods in the affected namespace: `kubectl get pods -n -o wide` (via EKS MCP `list_k8s_resources`) + - Check pod status — pods stuck in ContainerCreating without an IP address confirms IP allocation failure + - Check pod events: `kubectl describe pod ` (via EKS MCP `get_k8s_events`) for "failed to assign an IP address" or CNI errors + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — verify the node is Ready + - Check aws-node (VPC CNI) pods: `kubectl get pods -n kube-system -l k8s-app=aws-node` — if CNI is not Running, IP allocation is broken +- **PREREQUISITE — Is aws-node (VPC CNI) running?** Before investigating IP allocation, verify the CNI DaemonSet is alive: + - Use `list_k8s_resources` with clusterName, kind=Pod, apiVersion=v1, namespace=kube-system, labelSelector=k8s-app=aws-node — check that aws-node pod on the affected node is Running. + - If aws-node pod is CrashLoopBackOff, Error, or missing: that is the root cause. Report "aws-node (VPC CNI) not running on node — IP allocation impossible" immediately. + - Use `describe_eks_resource` with clusterName, resourceType=addon, resourceName=vpc-cni — if addon not found or degraded, report this. + - ONLY if aws-node is confirmed running, proceed to IP allocation investigation below. +- Use `collect` tool with instanceId to gather logs from the affected node +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId to get pre-indexed findings — look for IP allocation errors +- Use `network_diagnostics` tool with instanceId and sections=cni,ipamd,eni to get CNI/ipamd/ENI status from collected logs + +SHOULD: +- Use `search` tool with instanceId and query=`failed to assign an IP|no available IP|InsufficientFreeAddresses|failed to allocate ENI` to find IP allocation failure evidence +- Use `search` tool with query=`WARM_IP_TARGET|MINIMUM_IP_TARGET|ENABLE_PREFIX_DELEGATION|WARM_PREFIX_TARGET` to check ipamd environment settings +- Use `search` tool with query=`vpc-cni.*version|VPC_CNI_VERSION|aws-node.*image|602401143452.*amazon-k8s-cni` to check VPC CNI version — version must be compatible with the cluster Kubernetes version. Incompatible versions cause ipamd failures. + +MAY: +- Use `cluster_health` tool with clusterName to check if multiple nodes have IP exhaustion +- Use `compare_nodes` tool to compare IP allocation findings across nodes + +## Phase 2 — Enrich + +MUST: +- Review `network_diagnostics` cni/ipamd sections for ENI and IP utilization +- If all ENIs at max IPs (from network_diagnostics): instance ENI/IP limit reached — need larger instance or prefix delegation +- If ipamd shows many IPs in cooldown: high churn + low warm targets — increase WARM_IP_TARGET +- Use `search` tool with query=`ipamd.*not running|aws-node.*CrashLoop` to check if ipamd pod is healthy +- Use `search` tool with query=`kube-proxy.*not running|kube-proxy.*CrashLoop|kube-proxy.*error` to check kube-proxy health — kube-proxy must be running for aws-node to reach Ready state. If kube-proxy is down, aws-node cannot allocate IPs. + +SHOULD: +- Use `correlate` tool with instanceId and pivotEvent=`failed to assign` to build timeline of IP allocation failures +- Calculate IP utilization from network_diagnostics: assigned IPs / max IPs for instance type +- Use `search` tool with query=`InsufficientCidrBlocks|InsufficientCIDR|prefix.*delegation.*failed|/28.*failed` to check for prefix delegation failures due to fragmented subnets — subnets need contiguous /28 blocks for prefix delegation +- Use `search` tool with query=`ENABLE_SUBNET_DISCOVERY|subnet.*discovery|secondary.*subnet` to check if enhanced subnet discovery is enabled — this allows the CNI to use additional subnets tagged with kubernetes.io/role/cni +- Use `search` tool with query=`configurationConflicts|addon.*conflict|resolve.*conflicts` to check for VPC CNI addon configuration conflicts — use --resolve-conflicts OVERWRITE when updating the addon if custom config was applied + +MAY: +- Use `search` tool with query=`prefix delegation|ENABLE_PREFIX_DELEGATION` to check if prefix delegation is already enabled + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from IP-allocation-related findings +- State root cause: specific IP allocation bottleneck with evidence from network_diagnostics +- Recommend targeted fix based on bottleneck type (operator action) +- Confirm new pods should get IPs after fix + +SHOULD: +- Include ENI/IP utilization numbers from network_diagnostics +- Note subnet available IP count if visible in findings + +MAY: +- Recommend prefix delegation for 16x IP density +- Recommend cni-metrics-helper for monitoring + +## Guardrails + +escalation_conditions: + - "Subnet completely exhausted (0 available IPs)" + - "ipamd pod CrashLooping on multiple nodes" + - "IP allocation failures across all nodes in cluster (check via cluster_health)" + +safety_ratings: + - "Log collection (collect), search, errors, network_diagnostics: GREEN (read-only)" + - "Modify CNI settings, scale subnets: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "list_k8s_resources returns aws-node pod in CrashLoopBackOff, Error, or missing on the affected node" + diagnosis: "VPC CNI DaemonSet not running. No IP allocation can occur without aws-node." + resolution: "Operator action: check aws-node logs (kubectl logs -n kube-system -l k8s-app=aws-node). Common fixes: update VPC CNI addon, check IAM permissions (AmazonEKS_CNI_Policy), verify subnet has available IPs." + +- symptoms: "network_diagnostics shows all ENIs at max IPs" + diagnosis: "Instance type ENI/IP limit reached" + resolution: "Operator action: enable prefix delegation (ENABLE_PREFIX_DELEGATION=true on aws-node DaemonSet) for 16x density, or scale to larger instance type." + +- symptoms: "network_diagnostics shows many IPs in cooldown" + diagnosis: "High pod churn with low warm IP targets" + resolution: "Operator action: increase WARM_IP_TARGET or MINIMUM_IP_TARGET in aws-node environment." + +- symptoms: "search returns ipamd not running or aws-node CrashLooping" + diagnosis: "aws-node DaemonSet issue or missing AmazonEKS_CNI_Policy" + resolution: "Operator action: check aws-node DaemonSet status, verify node IAM role has AmazonEKS_CNI_Policy." + +- symptoms: "search returns VPC CNI version incompatible or aws-node image version mismatch" + diagnosis: "VPC CNI version is not compatible with the cluster Kubernetes version. Each CNI version supports specific K8s versions." + resolution: "Operator action: update VPC CNI addon to a version compatible with the cluster version — aws eks update-addon --cluster-name --addon-name vpc-cni --addon-version . Check compatibility matrix in AWS docs." + +- symptoms: "search returns kube-proxy not running or kube-proxy CrashLoopBackOff" + diagnosis: "kube-proxy must be running for aws-node to reach Ready state. Without kube-proxy, aws-node cannot communicate with the API server properly and IP allocation fails." + resolution: "Operator action: fix kube-proxy first (see D8 SOP), then aws-node will recover. Check kube-proxy DaemonSet status and logs." + +- symptoms: "search returns InsufficientCidrBlocks with prefix delegation enabled" + diagnosis: "Subnet is fragmented and does not have contiguous /28 CIDR blocks available for prefix delegation. Even if individual IPs are available, prefix delegation requires contiguous /28 blocks." + resolution: "Operator action: create a subnet CIDR reservation for prefix delegation using 'aws ec2 create-subnet-cidr-reservation'. Alternatively, use a new subnet with sufficient contiguous space. Consider disabling prefix delegation and using secondary IP mode if the subnet is heavily fragmented." + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Get network diagnostics +network_diagnostics(instanceId="i-0abc123def456", sections="cni,ipamd,eni") +# Step 3: Get IP allocation findings +errors(instanceId="i-0abc123def456") +# Step 4: Search for IP failures +search(instanceId="i-0abc123def456", query="failed to assign an IP|InsufficientFreeAddresses") +``` + +## Output Format + +```yaml +root_cause: "" +evidence: + - type: network_diagnostics + content: "" +severity: CRITICAL +mitigation: + immediate: "Operator: " + long_term: "Enable prefix delegation, monitor ipamd metrics" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D2-kube-proxy-iptables-sync.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D2-kube-proxy-iptables-sync.md new file mode 100644 index 0000000..c2dd4e2 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D2-kube-proxy-iptables-sync.md @@ -0,0 +1,136 @@ +--- +title: "D2 — kube-proxy iptables/IPVS Sync Issues" +description: "Diagnose service connectivity failures caused by kube-proxy not syncing iptables or IPVS rules" +status: active +severity: HIGH +triggers: + - "Failed to list *v1.Endpoints" + - "error syncing iptables rules" + - "KUBE-SVC chains missing" +owner: devops-agent +objective: "Identify why kube-proxy is not syncing service rules and restore service connectivity" +context: "kube-proxy maintains iptables or IPVS rules that map Service ClusterIPs to pod endpoints. When sync fails, services become unreachable even though individual pod IPs work." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check node and pod state before any log collection: + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — verify the node is Ready + - List pods on the affected node: `kubectl get pods --all-namespaces --field-selector spec.nodeName=` (via EKS MCP `list_k8s_resources` with field_selector) — check for pods with connectivity issues + - Check kube-proxy pods: `kubectl get pods -n kube-system -l k8s-app=kube-proxy` (via EKS MCP `list_k8s_resources`) — if kube-proxy is not Running or CrashLoopBackOff, iptables rules won't sync +- **PREREQUISITE — Is kube-proxy running?** Before investigating iptables sync, verify kube-proxy is alive: + - Use `list_k8s_resources` with clusterName, kind=Pod, apiVersion=v1, namespace=kube-system, labelSelector=k8s-app=kube-proxy — check that kube-proxy pod on the affected node is Running. + - If kube-proxy pod is CrashLoopBackOff, Error, or missing: that is the root cause. Report "kube-proxy not running on node — iptables/IPVS rules will not be synced" immediately. + - ONLY if kube-proxy is confirmed running, proceed to sync investigation below. +- Use `collect` tool with instanceId to gather logs from the affected node +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId to get pre-indexed findings — look for kube-proxy errors +- Use `network_diagnostics` tool with instanceId and sections=iptables,kube_proxy to get iptables rules and kube-proxy status + +SHOULD: +- Use `search` tool with instanceId and query=`kube-proxy.*error|Failed to list.*Endpoints|error syncing iptables` to find kube-proxy failure evidence +- Use `search` tool with query=`KUBE-SVC|KUBE-SEP` to check if service chain rules exist in iptables output + +MAY: +- Use `search` tool with query=`ipvs|ipvsadm` to check IPVS rules if in IPVS mode +- Use `cluster_health` tool with clusterName to check if multiple nodes have kube-proxy issues + +## Phase 2 — Enrich + +### ⚠️ MANDATORY PRE-CHECK: Read CRITICAL_WARNINGS First + +Before investigating ANY VPC CNI configuration, you MUST: +1. Check the `network_diagnostics` response for `CRITICAL_WARNINGS` and `rootCauseRanking` fields +2. If `CRITICAL_WARNINGS` exists, the root cause is ALREADY IDENTIFIED — do not investigate further +3. If `rootCauseRanking` shows kube-proxy as rank 1, the issue is kube-proxy NOT VPC CNI +4. Do NOT form hypotheses about podSGEnforcingMode, SNAT, or any CNI config until you have ruled out kube-proxy + +### ⚠️ MANDATORY: kube-proxy vs CNI Ownership Check + +If service connectivity (ClusterIP, NodePort) is failing: +- KUBE-SERVICES chain empty → kube-proxy issue, NOT CNI. Stop investigating CNI. +- KUBE-SERVICES chain populated → kube-proxy is fine, investigate CNI/routing/SG. + +The VPC CNI NEVER creates, modifies, or reads KUBE-SERVICES chains. The string "KUBE-SERVICES" does not appear anywhere in the VPC CNI codebase. If KUBE-SERVICES is empty, no amount of CNI config changes will fix it. + +MUST: +- Review `network_diagnostics` output for iptables section — check if KUBE-SVC rules exist +- Use `search` tool with query=`kube-proxy.*API|connection refused|unauthorized` to check API server connectivity from kube-proxy +- Use `correlate` tool with instanceId and pivotEvent=`kube-proxy` to build timeline of sync failures + +SHOULD: +- Review `network_diagnostics` kube_proxy section for mode (iptables vs IPVS) and error patterns +- Use `search` tool with query=`conntrack|nf_conntrack` to check if conntrack issues are contributing + +MAY: +- Use `compare_nodes` tool to compare kube-proxy findings between affected and healthy nodes + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from kube-proxy-related findings +- State root cause: kube-proxy not syncing due to API connectivity, crash, or config error +- Recommend fix (operator action): restart kube-proxy or fix connectivity +- Confirm services should be reachable after fix + +SHOULD: +- Include evidence of missing KUBE-SVC rules from network_diagnostics +- Include sync error evidence from search results + +MAY: +- Recommend monitoring kube-proxy health + +## Guardrails + +anti_hallucination: + - "CRITICAL: POD_SECURITY_GROUP_ENFORCING_MODE (podSGEnforcingMode) does NOT create a default-deny for unannotated pods. It ONLY affects pods that have the vpc.amazonaws.com/pod-eni annotation (Security Groups for Pods). In strict mode, annotated pods use their branch ENI exclusively instead of falling back to the primary ENI. Pods WITHOUT SGP annotations are COMPLETELY UNAFFECTED — they use the primary ENI and normal VPC routing. Do NOT blame podSGEnforcingMode for connectivity failures on pods without SGP annotations. If ClusterIP traffic is failing, check kube-proxy health and KUBE-SERVICES iptables chain FIRST." + +escalation_conditions: + - "kube-proxy restart does not restore service rules" + - "API server unreachable from kube-proxy pods" + - "All nodes missing service iptables rules" + +safety_ratings: + - "Log collection (collect), search, errors, network_diagnostics: GREEN (read-only)" + - "Restart kube-proxy pods: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "list_k8s_resources returns kube-proxy pod in CrashLoopBackOff, Error, or missing on the affected node" + diagnosis: "kube-proxy is not running. No iptables/IPVS rules will be synced on this node." + resolution: "Operator action: check kube-proxy logs (kubectl logs -n kube-system -l k8s-app=kube-proxy). Common fixes: restart kube-proxy DaemonSet, check kube-proxy-config ConfigMap, verify RBAC." + +- symptoms: "network_diagnostics iptables section shows no KUBE-SVC rules" + diagnosis: "kube-proxy not running or not syncing rules" + resolution: "Operator action: restart kube-proxy pods (kubectl delete pods -n kube-system -l k8s-app=kube-proxy)" + +- symptoms: "search returns kube-proxy API connection errors" + diagnosis: "kube-proxy cannot reach API server to get service/endpoint updates" + resolution: "Operator action: check network connectivity, security groups, and kube-proxy service account permissions" + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Get network diagnostics +network_diagnostics(instanceId="i-0abc123def456", sections="iptables,kube_proxy") +# Step 3: Get kube-proxy findings +errors(instanceId="i-0abc123def456") +# Step 4: Search for sync errors +search(instanceId="i-0abc123def456", query="kube-proxy.*error|error syncing iptables") +``` + +## Output Format + +```yaml +root_cause: "kube-proxy sync failure — " +evidence: + - type: network_diagnostics + content: "" +severity: HIGH +mitigation: + immediate: "Operator: restart kube-proxy pods" + long_term: "Monitor kube-proxy health, ensure API server connectivity" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D3-conntrack-exhaustion.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D3-conntrack-exhaustion.md new file mode 100644 index 0000000..2309653 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D3-conntrack-exhaustion.md @@ -0,0 +1,118 @@ +--- +title: "D3 — Conntrack Exhaustion" +description: "Diagnose connection failures caused by conntrack table full" +status: active +severity: CRITICAL +triggers: + - "nf_conntrack: table full, dropping packet" + - "conntrack_allowance_exceeded" +owner: devops-agent +objective: "Identify conntrack exhaustion level (kernel vs AWS instance) and restore connectivity" +context: "Connection tracking (conntrack) maintains state for NAT and stateful firewall rules. When the table fills up, new connections are dropped silently, causing intermittent failures and DNS timeouts." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check node and pod state before any log collection: + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — verify the node is Ready + - List pods on the affected node: `kubectl get pods --all-namespaces --field-selector spec.nodeName=` (via EKS MCP `list_k8s_resources` with field_selector) — check for pods with connection timeouts or refused connections (symptoms of conntrack exhaustion) +- Use `collect` tool with instanceId to gather logs from the affected node +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId to get pre-indexed findings — look for conntrack errors +- Use `search` tool with instanceId and query=`nf_conntrack.*table full|conntrack_allowance_exceeded` to find conntrack exhaustion evidence + +SHOULD: +- Use `network_diagnostics` tool with instanceId and sections=kube_proxy to get conntrack stats from collected logs +- Use `search` tool with query=`nf_conntrack_max|nf_conntrack_count` to find sysctl conntrack limits + +MAY: +- Use `search` tool with query=`ethtool.*conntrack|ena.*conntrack` to check ENA-level conntrack counters +- Use `cluster_health` tool with clusterName to check if multiple nodes are affected + +## Phase 2 — Enrich + +MUST: +- Use `search` tool with query=`table full` — if found, kernel conntrack limit hit (can increase via sysctl) +- Use `search` tool with query=`conntrack_allowance_exceeded` — if found, AWS instance-level limit (need bigger instance, sysctl will not help) +- Determine which limit is the bottleneck from findings + +SHOULD: +- Use `correlate` tool with instanceId and pivotEvent=`conntrack` to correlate conntrack exhaustion with connection failures +- Use `search` tool with query=`nf_conntrack_count|nf_conntrack_max` to calculate utilization percentage + +MAY: +- Use `compare_nodes` tool to compare conntrack findings across nodes + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from conntrack-related findings +- State root cause: kernel conntrack limit or AWS instance limit, with evidence +- Recommend specific fix based on which limit is hit (operator action) +- Confirm connections should be restored after fix + +SHOULD: +- Include conntrack count vs max values from search results +- Include ENA allowance counter values if available + +MAY: +- Recommend conntrack monitoring via CloudWatch agent + +## Guardrails + +escalation_conditions: + - "AWS instance-level conntrack limit hit (cannot fix with sysctl)" + - "Conntrack exhaustion causing DNS failures cluster-wide" + - "Conntrack table full on multiple nodes simultaneously" + +safety_ratings: + - "Log collection (collect), search, errors, network_diagnostics: GREEN (read-only)" + - "Modify sysctl settings: YELLOW — operator action, not available via MCP tools" + - "Upsize instance: RED — operator action, requires approval" + +## Common Issues + +- symptoms: "search returns nf_conntrack: table full in dmesg findings" + diagnosis: "Kernel conntrack table limit reached" + resolution: "Operator action: sysctl -w net.netfilter.nf_conntrack_max=. Make persistent in sysctl.d." + +- symptoms: "search returns conntrack_allowance_exceeded > 0 in ethtool findings" + diagnosis: "AWS instance-level conntrack limit. Kernel sysctl cannot fix this." + resolution: "Operator action: upgrade to instance type with higher connection tracking allowance." + +- symptoms: "search for nf_conntrack_max shows default value (e.g., 131072) with high connection workloads" + diagnosis: "Kernel conntrack limit too low for workload. Can be increased via kube-proxy ConfigMap or sysctl." + resolution: "Operator action: increase via kube-proxy ConfigMap — set conntrack.min and conntrack.maxPerCore. Formula: max(min, maxPerCore * number_of_CPU_cores). Then restart kube-proxy DaemonSet: kubectl rollout restart ds/kube-proxy -n kube-system. Each conntrack entry uses ~300 bytes of memory." + +- symptoms: "search returns both nf_conntrack table full AND conntrack_allowance_exceeded > 0" + diagnosis: "Both kernel and AWS instance-level limits are being hit. Kernel limit should be raised first, but AWS limit is the hard ceiling." + resolution: "Operator action: first increase kernel limit via sysctl or kube-proxy ConfigMap. If conntrack_allowance_exceeded persists, upgrade instance type. Monitor memory impact (~300 bytes per entry)." + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Get conntrack findings +errors(instanceId="i-0abc123def456") +# Step 3: Search for conntrack evidence +search(instanceId="i-0abc123def456", query="nf_conntrack.*table full|conntrack_allowance_exceeded") +# Step 4: Get network diagnostics +network_diagnostics(instanceId="i-0abc123def456", sections="kube_proxy") +``` + +## Output Format + +```yaml +root_cause: " exhaustion" +evidence: + - type: finding + content: "" + - type: search + content: "count= max=" +severity: CRITICAL +mitigation: + immediate: "Operator: increase sysctl limit or upsize instance" + long_term: "Monitor conntrack metrics, right-size instances" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D4-mtu-fragmentation.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D4-mtu-fragmentation.md new file mode 100644 index 0000000..703661a --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D4-mtu-fragmentation.md @@ -0,0 +1,113 @@ +--- +title: "D4 — MTU / Fragmentation Issues" +description: "Diagnose large packet drops and TLS failures caused by MTU mismatch" +status: active +severity: MEDIUM +triggers: + - "Frag needed" + - "message too long" +owner: devops-agent +objective: "Identify MTU mismatch between interfaces and restore large packet delivery" +context: "MTU mismatches between host interfaces (9001 jumbo) and pod interfaces can cause large packets to be dropped. TLS handshakes and large HTTP responses fail while small packets work fine." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check node and pod state before any log collection: + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — verify the node is Ready + - List pods on the affected node: `kubectl get pods --all-namespaces --field-selector spec.nodeName=` (via EKS MCP `list_k8s_resources` with field_selector) — check for pods experiencing packet loss or connection issues +- Use `collect` tool with instanceId to gather logs from the affected node +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId to get pre-indexed findings — look for fragmentation errors +- Use `network_diagnostics` tool with instanceId and sections=routes,eni to get interface MTU values and route table from collected logs + +SHOULD: +- Use `search` tool with instanceId and query=`Frag needed|message too long|PMTU|mtu` to find MTU-related errors +- Use `search` tool with query=`AWS_VPC_MTU_OVERRIDE|MTU` to check CNI MTU configuration + +MAY: +- Use `tcpdump_capture` tool with instanceId to capture fragmentation events (if needed for deeper analysis) +- Use `tcpdump_analyze` tool to analyze captured packets for MTU issues + +## Phase 2 — Enrich + +MUST: +- Review `network_diagnostics` routes section for interface MTU values — compare pod interface MTU against expected (9001 jumbo or 1500 standard) +- Use `search` tool with query=`AWS_VPC_MTU_OVERRIDE` to check if MTU override is set on aws-node DaemonSet +- Use `search` tool with query=`ICMP.*Frag needed|icmp.*type 3` to verify ICMP is not blocked + +SHOULD: +- Use `network_diagnostics` eni section to check ENI configuration +- Use `correlate` tool with instanceId and pivotEvent=`Frag needed` to correlate MTU issues with TLS failures + +MAY: +- Use `compare_nodes` tool to compare MTU settings across nodes + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from MTU-related findings +- State root cause: MTU mismatch with specific interface values from network_diagnostics +- Recommend MTU fix (operator action) +- Confirm large packets should be delivered after fix + +SHOULD: +- Include interface MTU values from network_diagnostics + +MAY: +- Recommend consistent MTU policy across VPC + +## Guardrails + +escalation_conditions: + - "MTU mismatch caused by VPC peering or Transit Gateway configuration" + - "ICMP blocked by network policy that cannot be changed" + +safety_ratings: + - "Log collection (collect), search, errors, network_diagnostics, tcpdump_capture: GREEN (read-only)" + - "Modify CNI MTU config: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "network_diagnostics shows pod interface MTU != host interface MTU, search returns TLS failures" + diagnosis: "MTU mismatch causing large packet drops. PMTUD may be blocked." + resolution: "Operator action: set AWS_VPC_MTU_OVERRIDE on aws-node DaemonSet. Ensure ICMP is not blocked." + +- symptoms: "search returns 'Destination Unreachable: Fragmentation Needed' (ICMP Type 3, Code 4) in dmesg or tcpdump" + diagnosis: "Path MTU Discovery is working — a device along the path has a smaller MTU than the packet size. The ICMP message instructs the sender to reduce packet size. If these messages are blocked by NACLs or security groups, PMTUD fails silently." + resolution: "Operator action: ensure network ACLs allow ICMP Type 3 Code 4 (Fragmentation Needed) inbound and outbound. For IPv6, ensure ICMPv6 Type 2 (Packet Too Big) is allowed. See AWS docs on Path MTU Discovery." + +- symptoms: "search returns TLS handshake failures or large HTTP response timeouts, but small requests (ping, DNS) work fine" + diagnosis: "Classic MTU black hole — large packets are silently dropped because ICMP PMTUD messages are blocked. Small packets under the path MTU work fine." + resolution: "Operator action: 1) Check NACLs allow ICMP Type 3 Code 4. 2) Set AWS_VPC_MTU_OVERRIDE=1500 on aws-node if using VPC peering or Transit Gateway (which may have lower MTU). 3) For cross-region or VPN traffic, MTU may be as low as 1300-1400." + +- symptoms: "network_diagnostics shows host interface MTU=9001 (jumbo) but traffic crosses VPC peering, Transit Gateway, or VPN" + diagnosis: "VPC peering, Transit Gateway, and VPN connections may not support jumbo frames (MTU 9001). Traffic crossing these boundaries gets fragmented or dropped." + resolution: "Operator action: set AWS_VPC_MTU_OVERRIDE=1500 on aws-node DaemonSet for pods that communicate across VPC boundaries. Alternatively, set MTU at the pod level." + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Get network diagnostics +network_diagnostics(instanceId="i-0abc123def456", sections="routes,eni") +# Step 3: Search for MTU issues +search(instanceId="i-0abc123def456", query="Frag needed|message too long|mtu") +# Step 4: Check CNI MTU config +search(instanceId="i-0abc123def456", query="AWS_VPC_MTU_OVERRIDE") +``` + +## Output Format + +```yaml +root_cause: "MTU mismatch — " +evidence: + - type: network_diagnostics + content: "" +severity: MEDIUM +mitigation: + immediate: "Operator: fix CNI MTU config via AWS_VPC_MTU_OVERRIDE" + long_term: "Ensure consistent MTU across VPC, do not block ICMP" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D5-dns-failures.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D5-dns-failures.md new file mode 100644 index 0000000..e1ab5a8 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D5-dns-failures.md @@ -0,0 +1,157 @@ +--- +title: "D5 — DNS Failures from Pods" +description: "Diagnose DNS resolution failures affecting pod workloads" +status: active +severity: HIGH +triggers: + - "UnknownHostException" + - "Could not resolve host" + - "SERVFAIL" + - "linklocal_allowance_exceeded" +owner: devops-agent +objective: "Identify the DNS failure root cause and restore name resolution" +context: "Pod DNS failures can stem from CoreDNS overload, VPC DNS throttling (1024 PPS per ENI), ENA linklocal allowance exceeded, incorrect resolv.conf, ndots:5 amplification, or network policies blocking UDP 53." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check pod and node state before any log collection: + - List pods in the affected namespace: `kubectl get pods -n -o wide` (via EKS MCP `list_k8s_resources`) + - Verify the affected pod is Running and Ready — DNS failures in a non-running pod are a symptom, not the cause + - Check CoreDNS pods: `kubectl get pods -n kube-system -l k8s-app=kube-dns` (via EKS MCP `list_k8s_resources`) — if CoreDNS pods are not Running, that is the root cause + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — NotReady nodes cannot reach CoreDNS +- **PREREQUISITE — Check firewall rules blocking DNS (port 53)**: Before investigating DNS config or CoreDNS health, rule out firewall blocks: + - Use `collect` tool with instanceId to gather logs from the affected node + - Use `status` tool with executionId to poll until collection completes + - Use `network_diagnostics` tool with instanceId and sections=iptables to get iptables rules + - Use `search` tool with instanceId and query=`DROP.*53|REJECT.*53|DROP.*dns|REJECT.*dns|DROP.*coredns|iptables.*53.*DROP|iptables.*53.*REJECT` — if matches found, firewall rules are blocking DNS traffic. That is the root cause. + - Use `search` tool with instanceId and query=`NetworkPolicy|network.*policy|calico.*deny|cilium.*deny` — check for Kubernetes NetworkPolicy or CNI policy rules that may block UDP/TCP 53 + - ONLY if no firewall blocks found, proceed to DNS config and CoreDNS investigation below. +- Use `errors` tool with instanceId to get pre-indexed findings — look for DNS errors +- Use `network_diagnostics` tool with instanceId and sections=dns to get DNS configuration from collected logs + +SHOULD: +- Use `search` tool with instanceId and query=`UnknownHostException|Could not resolve|SERVFAIL|linklocal_allowance_exceeded` to find DNS failure evidence +- Use `search` tool with query=`resolv.conf|nameserver|ndots` to check DNS configuration +- Use `search` tool with query=`security group|sg-|port 53|UDP.*53|TCP.*53` to check if security groups allow DNS traffic (TCP/UDP port 53) from the pod CIDR range to CoreDNS pods + +MAY: +- Use `search` tool with query=`ethtool.*linklocal|linklocal_allowance` to check ENA linklocal counters +- Use `cluster_health` tool with clusterName to check if DNS failures are cluster-wide + +## Phase 2 — Enrich + +MUST: +- Review `network_diagnostics` dns section for resolv.conf configuration and DNS issues +- Use `search` tool with query=`linklocal_allowance_exceeded` — if found, PPS throttling to VPC DNS (recommend NodeLocal DNSCache) +- Use `search` tool with query=`nameserver` in resolv.conf — if not kube-dns ClusterIP, bootstrap misconfiguration +- Use `search` tool with query=`CoreDNS|coredns.*error|coredns.*CrashLoop` to check CoreDNS health +- Use `search` tool with query=`OOM|oom-killer|out of memory|stress|memory.*exhaust` — check for memory exhaustion causing cascading failures including DNS. If found, DNS failure is a symptom, not the root cause — switch to A1 (OOM) SOP. + +SHOULD: +- Use `correlate` tool with instanceId and pivotEvent=`DNS|resolve` to correlate DNS failures with other events +- Use `search` tool with query=`ndots` to check ndots setting (ndots:5 causes 4x query amplification) +- Use `search` tool with query=`NodeLocalDNS|node-local-dns|nodelocaldns|169.254.20.10` to check if NodeLocalDNS is installed and running — if installed but CrashLoopBackOff, check for port 53 conflicts +- Use `search` tool with query=`ipvs|IPVS|kube-proxy.*mode.*ipvs` to check if kube-proxy is in IPVS mode — IPVS mode requires manual pod DNS configuration to use NodeLocalDNS link-local address (169.254.20.10) + +MAY: +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="coredns" to check for recent CoreDNS ConfigMap changes +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="NetworkPolicy" to check for NetworkPolicy changes blocking UDP 53 +- When log-level DNS evidence is inconclusive (no clear iptables blocks, CoreDNS healthy, resolv.conf correct, but DNS still failing), suggest operator run tcpdump to capture live DNS traffic: `tcpdump -i any -nn port 53 -c 50` on the affected node. This can reveal packet drops, timeouts, or unexpected responses not visible in logs. Recommend capturing both UDP and TCP on port 53. + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from DNS-related findings +- State root cause: specific DNS failure mechanism with evidence +- Recommend targeted fix (operator action) +- Confirm DNS resolution should be restored after fix + +SHOULD: +- Include linklocal counter values or CoreDNS error evidence from search results +- Recommend NodeLocal DNSCache if PPS throttling detected + +MAY: +- Recommend reducing ndots in pod spec for external-heavy workloads + +## Guardrails + +escalation_conditions: + - "CoreDNS pods CrashLooping and cannot be restarted" + - "VPC DNS throttling affecting all nodes" + - "DNS failures causing cascading application failures" + +safety_ratings: + - "Log collection (collect), search, errors, network_diagnostics: GREEN (read-only)" + - "Deploy NodeLocal DNSCache, modify CoreDNS: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "search returns DROP.*53 or REJECT.*53 in iptables output" + diagnosis: "Firewall rules (iptables/nftables) are blocking DNS traffic on port 53" + resolution: "Operator action: remove the offending iptables rule — iptables -D . Check for NetworkPolicy or security tooling that injected the rule." + +- symptoms: "search returns NetworkPolicy deny rules affecting kube-dns or CoreDNS pods" + diagnosis: "Kubernetes NetworkPolicy blocking UDP/TCP 53 to CoreDNS" + resolution: "Operator action: update NetworkPolicy to allow egress to kube-dns on UDP/TCP 53" + +- symptoms: "search returns linklocal_allowance_exceeded > 0" + diagnosis: "PPS throttling to VPC DNS (169.254.169.253)" + resolution: "Operator action: deploy NodeLocal DNSCache to reduce VPC DNS queries" + +- symptoms: "network_diagnostics dns section shows wrong nameserver IP in resolv.conf" + diagnosis: "Bootstrap misconfiguration — dns-cluster-ip wrong" + resolution: "Operator action: fix --dns-cluster-ip in kubelet config to match kube-dns ClusterIP" + +- symptoms: "search returns CoreDNS CrashLooping" + diagnosis: "CoreDNS resource exhaustion or configuration error" + resolution: "Operator action: scale CoreDNS replicas, increase memory limits, check Corefile" + +- symptoms: "search returns NodeLocalDNS CrashLoopBackOff on EKS Auto Mode nodes or port 53 conflict" + diagnosis: "NodeLocalDNS cannot bind to port 53 because another process (e.g., systemd-resolved on EKS Auto Mode nodes) is already listening on that port." + resolution: "Operator action: configure NodeLocalDNS to use a different port or disable systemd-resolved on the node. On EKS Auto Mode nodes, NodeLocalDNS may not be compatible — use CoreDNS scaling instead." + +- symptoms: "search returns IPVS mode and pods not using NodeLocalDNS link-local address" + diagnosis: "When kube-proxy runs in IPVS mode, pods must be manually configured to use the NodeLocalDNS link-local address (169.254.20.10) because IPVS does not intercept traffic to the kube-dns ClusterIP the same way iptables does." + resolution: "Operator action: update pod DNS config to use 169.254.20.10 as the nameserver, or configure NodeLocalDNS to listen on the kube-dns ClusterIP (requires disabling kube-dns service). See AWS docs for IPVS + NodeLocalDNS setup." + +- symptoms: "search returns security group blocking TCP/UDP port 53 from pod CIDR" + diagnosis: "Security group rules are blocking DNS traffic (TCP/UDP port 53) from the pod CIDR range to CoreDNS pods." + resolution: "Operator action: update security groups to allow TCP and UDP port 53 from the pod CIDR range to the CoreDNS pod IPs or the node security group." + +- symptoms: "tcpdump or DNS logs show queries with doubled/repeated domain suffixes such as 'myservice.mynamespace.svc.cluster.local.mynamespace.svc.cluster.local' — these queries return NXDomain but the final correct query succeeds" + diagnosis: "THIS IS NORMAL BEHAVIOR — NOT A MISCONFIGURATION. With the default ndots:5 setting, the glibc resolver treats any name with fewer than 5 dots as 'not fully qualified' and appends each search domain from /etc/resolv.conf before trying the name as-is. A standard Kubernetes resolv.conf has search domains like 'mynamespace.svc.cluster.local svc.cluster.local cluster.local ec2.internal'. When a pod resolves 'myservice.mynamespace.svc.cluster.local' (4 dots, which is < ndots:5), the resolver first tries appending the first search domain, producing 'myservice.mynamespace.svc.cluster.local.mynamespace.svc.cluster.local'. This gets NXDomain, then the next search domain is tried, and so on until the correct resolution succeeds. This generates 3-5 extra NXDomain queries per lookup but is functionally correct and expected." + resolution: "No action required — this is working as designed. If the extra DNS queries are causing performance concerns (CoreDNS load, latency), the operator can: (1) add a trailing dot to FQDNs in application config (e.g., 'myservice.mynamespace.svc.cluster.local.') to bypass search domain expansion entirely, (2) lower ndots to 2 in the pod spec dnsConfig, or (3) use short service names (e.g., 'myservice' or 'myservice.mynamespace') which resolve correctly on the first search domain attempt. See https://docs.aws.amazon.com/eks/latest/best-practices/scale-cluster-services.html" + +- symptoms: "tcpdump shows a small number of DNS SERVFAIL responses, but DNS resolution generally works" + diagnosis: "THIS IS NORMAL BEHAVIOR during CoreDNS scaling events. When CoreDNS pods scale down, there is a propagation delay for kube-proxy to update iptables rules. During this brief window, DNS queries may be routed to a terminating CoreDNS pod and receive SERVFAIL. The CoreDNS lameduck plugin mitigates this by delaying shutdown. A few SERVFAILs are transient and self-resolving." + resolution: "No action required if the count is small (<10) and transient. If persistent, check CoreDNS health and ensure the lameduck plugin is configured in the Corefile. See https://docs.aws.amazon.com/eks/latest/best-practices/scale-cluster-services.html" + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Get DNS diagnostics +network_diagnostics(instanceId="i-0abc123def456", sections="dns") +# Step 3: Get DNS-related findings +errors(instanceId="i-0abc123def456") +# Step 4: Search for DNS failures +search(instanceId="i-0abc123def456", query="linklocal_allowance_exceeded|Could not resolve|SERVFAIL") +``` + +## Output Format + +```yaml +root_cause: "" +evidence: + - type: network_diagnostics + content: "" + - type: finding + content: "" +severity: HIGH +mitigation: + immediate: "Operator: " + long_term: "Deploy NodeLocal DNSCache, reduce ndots, monitor DNS metrics" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D6-ena-throttling.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D6-ena-throttling.md new file mode 100644 index 0000000..4ee1223 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D6-ena-throttling.md @@ -0,0 +1,134 @@ +--- +title: "D6 — ENA Throttling / Bandwidth Allowance" +description: "Diagnose packet loss and latency caused by ENA network allowance exceeded" +status: active +severity: HIGH +triggers: + - "bw_in_allowance_exceeded" + - "bw_out_allowance_exceeded" + - "pps_allowance_exceeded" + - "conntrack_allowance_exceeded" + - "linklocal_allowance_exceeded" +owner: devops-agent +objective: "Identify which ENA allowance is exceeded and recommend instance right-sizing" +context: "AWS instances have network performance limits (bandwidth, PPS, conntrack, linklocal). When exceeded, packets are silently dropped causing intermittent failures. These are instance-level limits that cannot be changed via OS configuration." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check node and pod state before any log collection: + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — verify the node is Ready + - List pods on the affected node: `kubectl get pods --all-namespaces --field-selector spec.nodeName=` (via EKS MCP `list_k8s_resources` with field_selector) — check for pods experiencing network throttling or timeouts +- Use `collect` tool with instanceId to gather logs from the affected node +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId to get pre-indexed findings — look for ENA throttling errors +- Use `search` tool with instanceId and query=`allowance_exceeded|bw_in_allowance|bw_out_allowance|pps_allowance|conntrack_allowance|linklocal_allowance` to find ENA throttling evidence + +SHOULD: +- Use `network_diagnostics` tool with instanceId and sections=eni to get ENI stats from collected logs +- Use `search` tool with query=`instance-type|meta-data` to identify instance type + +MAY: +- Use `cluster_health` tool with clusterName to check if multiple nodes have ENA throttling +- Use `compare_nodes` tool to compare ENA stats across nodes + +## Phase 2 — Enrich + +MUST: +- Map nonzero counters from search results to root cause: + - bw_*_allowance_exceeded: traffic volume exceeds instance bandwidth + - pps_allowance_exceeded: small packet rate too high + - conntrack_allowance_exceeded: too many tracked connections (see D3 SOP) + - linklocal_allowance_exceeded: DNS/IMDS/NTP request rate too high (see D5 SOP) +- Use `correlate` tool with instanceId and pivotEvent=`allowance_exceeded` to correlate throttling with application failures + +SHOULD: +- Use `search` tool with query=`ethtool.*drop|rx_drop|tx_drop` to check for packet drops +- Determine if throttling is sustained or burst from correlate timeline + +MAY: +- Use `compare_nodes` tool to identify which nodes are most affected + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from ENA-related findings +- State which allowance(s) are exceeded with counter values from search results +- Recommend instance type upgrade with specific network specs needed (operator action) +- Confirm throttling should resolve after mitigation + +SHOULD: +- Include ENA counter values from search results +- Include instance type and its network limits + +MAY: +- Recommend CloudWatch alarms on allowance exceeded metrics +- Recommend NodeLocal DNSCache for linklocal throttling + +## Guardrails + +escalation_conditions: + - "Multiple allowance types exceeded simultaneously" + - "Largest available instance type still insufficient" + - "Throttling causing cascading application failures" + +safety_ratings: + - "Log collection (collect), search, errors, network_diagnostics: GREEN (read-only)" + - "Upsize instance: RED — operator action, requires approval" + +## Common Issues + +- symptoms: "search returns bw_in/out_allowance_exceeded > 0" + diagnosis: "Instance bandwidth limit hit" + resolution: "Operator action: upgrade to instance type with higher baseline bandwidth" + +- symptoms: "search returns pps_allowance_exceeded > 0" + diagnosis: "Packets per second limit hit (common with many small packets)" + resolution: "Operator action: reduce small packet workloads or upgrade instance type" + +- symptoms: "search returns linklocal_allowance_exceeded > 0" + diagnosis: "DNS/IMDS/NTP request rate too high" + resolution: "Operator action: deploy NodeLocal DNSCache, use IRSA instead of IMDS" + +- symptoms: "search returns conntrack_allowance_exceeded > 0 alongside bw or pps allowance exceeded" + diagnosis: "Multiple ENA allowances exceeded simultaneously. Connection tracking limit is separate from bandwidth/PPS limits. See D3-conntrack-exhaustion SOP for conntrack-specific troubleshooting." + resolution: "Operator action: upgrade to instance type with higher limits across all allowance types. Configure security group rules to avoid tracking where possible (untracked connections do not count against conntrack allowance)." + +- symptoms: "search returns ENA keep-alive watchdog timeout or 'Trigger reset is on' in dmesg" + diagnosis: "ENA device experienced a failure and triggered a reset. This causes brief traffic loss while the driver reinitializes. Check ethtool -S for wd_expired counter." + resolution: "Operator action: check for ENA driver version compatibility — update to latest ENA driver. If persistent, check for instance hardware issues and consider replacing the instance." + +- symptoms: "search returns queue_N_tx_queue_stop > 0 or queue_N_rx_page_alloc_fail > 0" + diagnosis: "ENA queue-level issues — tx_queue_stop indicates transmit queue full (bandwidth saturation), rx_page_alloc_fail indicates low memory preventing packet reception." + resolution: "Operator action: for tx_queue_stop, reduce traffic or upgrade instance. For rx_page_alloc_fail, check memory pressure on the node (see G2-oomkill SOP) and ensure sufficient free memory." + +- symptoms: "search returns conntrack_allowance_available showing low values (approaching 0)" + diagnosis: "Connection tracking allowance is nearly exhausted. New connections will be dropped when it reaches 0. Monitor conntrack_allowance_available metric proactively." + resolution: "Operator action: reduce tracked connections by configuring security group rules to avoid tracking (symmetric rules with 0.0.0.0/0 are untracked). Reduce idle connection timeout. Upgrade instance type if needed." + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Search for ENA throttling +search(instanceId="i-0abc123def456", query="allowance_exceeded") +# Step 3: Get ENI diagnostics +network_diagnostics(instanceId="i-0abc123def456", sections="eni") +# Step 4: Correlate with failures +correlate(instanceId="i-0abc123def456", pivotEvent="allowance_exceeded") +``` + +## Output Format + +```yaml +root_cause: " exceeded on " +evidence: + - type: search + content: "=" +severity: HIGH +mitigation: + immediate: "Operator: upgrade instance type for higher network limits" + long_term: "Monitor ENA metrics, right-size instances, use NodeLocal DNSCache" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D7-network-performance-degradation.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D7-network-performance-degradation.md new file mode 100644 index 0000000..010b915 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D7-network-performance-degradation.md @@ -0,0 +1,215 @@ +--- +title: "D7 — Network Performance Degradation" +description: "Diagnose high latency, packet loss, TCP retransmissions, and intermittent timeouts not explained by ENA throttling, MTU, or DNS issues" +status: active +severity: HIGH +triggers: + - "retransmit" + - "TCP.*reset" + - "connection timed out" + - "no route to host" + - "rx_errors" + - "tx_errors" + - "packet loss" + - "latency" +owner: devops-agent +objective: "Identify the layer causing network performance degradation (NIC errors, TCP stack, security rules, routing, or upstream) and recommend targeted remediation" +context: "Network performance issues that are not ENA throttling (D6/J1), MTU (D4), DNS (D5), or conntrack (D3) fall here. Common causes include TCP retransmissions from packet loss, interface errors, security group or NACL drops, route blackholes, IRQ imbalance, and ring buffer overflows. These manifest as high latency, intermittent timeouts, or degraded throughput without a clear ENA allowance_exceeded signal." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check node and pod state before any log collection: + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — verify the node is Ready + - List pods on the affected node: `kubectl get pods --all-namespaces --field-selector spec.nodeName=` (via EKS MCP `list_k8s_resources` with field_selector) — check for pods experiencing slow responses, timeouts, or packet loss +- Use `collect` tool with instanceId to gather logs from the affected node +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId to get pre-indexed findings — look for network error patterns +- Use `network_diagnostics` tool with instanceId and sections=eni,routes,iptables,cni,dns to get a full network picture + +SHOULD: +- Use `search` tool with instanceId and query=`retransmit|retrans|RST|reset|timeout|timed out|no route to host` to find TCP-level failure evidence +- Use `search` tool with query=`rx_errors|tx_errors|rx_dropped|tx_dropped|rx_crc|collisions|carrier` to find NIC-level errors +- Rule out other D-series SOPs first: + - Check for `allowance_exceeded` → D6/J1 + - Check for `Frag needed` → D4 + - Check for `nf_conntrack.*table full` → D3 + - Check for DNS failures → D5 + +MAY: +- Use `quick_triage` tool with instanceId for a fast overview before deep-diving +- Use `cluster_health` tool with clusterName to check if degradation is node-specific or cluster-wide + +## Phase 2 — Enrich + +MUST: +- Classify the failure layer from Phase 1 findings: + + Layer 1 — NIC/Driver errors: + - Use `search` tool with query=`rx_errors|tx_errors|rx_crc_errors|rx_missed_errors|carrier_errors` to find interface error counters + - Use `search` tool with query=`ena.*version|modinfo ena` to check ENA driver version (< 2.8 may have bugs) + - Use `search` tool with query=`ring buffer|rx_queue.*drop|tx_queue.*drop` to check ring buffer overflows + + Layer 2 — IRQ / CPU affinity: + - Use `search` tool with query=`irqbalance|smp_affinity|RPS|XPS` to check IRQ distribution + - Use `search` tool with query=`softirq.*NET_RX|ksoftirqd|cpu.*100` to check for softirq saturation on a single CPU + + Layer 3 — TCP stack: + - Use `search` tool with query=`retransmit|TCPRetrans|TCPLoss|TCPTimeouts|TCPAbort` to find TCP retransmission stats from /proc/net/snmp or netstat + - Use `search` tool with query=`tcp_rmem|tcp_wmem|somaxconn|backlog` to check TCP tuning parameters + - Use `search` tool with query=`SYN.*drop|SYN.*overflow|listen.*overflow` to check for SYN queue overflow + + Layer 4 — Security rules / routing: + - Use `network_diagnostics` iptables section to check for DROP/REJECT rules that may silently discard traffic + - Use `network_diagnostics` routes section to check for blackhole routes or missing routes + - Use `search` tool with query=`REJECT|DROP.*INPUT|DROP.*FORWARD|nflog` to find firewall drops + - Use `search` tool with query=`NetworkPolicy|calico|cilium` to check for Kubernetes NetworkPolicy enforcement + +- Use `correlate` tool with instanceId and pivotEvent set to the most prominent error pattern (e.g., `retransmit` or `rx_errors`) to build a timeline + +SHOULD: +- Use `tcpdump_capture` tool with instanceId to capture live traffic if the issue is intermittent and log evidence is insufficient + - For latency: capture on the affected pod interface or eth0 + - For packet loss: capture with a filter matching the affected traffic flow +- Use `tcpdump_analyze` tool to analyze the capture for retransmissions, resets, and latency patterns +- Use `search` tool with query=`nf_conntrack_count|nf_conntrack_max` to rule out conntrack pressure (even if not full, high utilization can cause slowness) + +MAY: +- Use `compare_nodes` tool with instanceIds of affected + healthy node to find NIC/TCP differences +- Use `search` tool with query=`placement group|cluster placement` to check if instances are in a placement group (affects inter-node latency) + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from all network performance findings +- State root cause with the specific layer identified: + - NIC errors → driver issue or hardware degradation + - IRQ imbalance → softirq saturation on single CPU + - TCP retransmissions → packet loss in path, security rule drops, or upstream issue + - Security rule drops → iptables/NetworkPolicy blocking traffic + - Route issue → missing or blackhole route +- Recommend targeted remediation (operator action — not available via MCP tools) +- Cross-reference: if ENA throttling found during investigation, redirect to D6/J1 + +SHOULD: +- Include specific counter values from search results (rx_errors, retransmit counts, drop counts) +- Include timeline from correlate showing when degradation started +- Include tcpdump analysis summary if capture was performed + +MAY: +- Recommend TCP tuning parameters if TCP stack is the bottleneck +- Recommend placement group for latency-sensitive workloads +- Recommend ENA driver update if version is outdated + +## Guardrails + +escalation_conditions: + - "NIC hardware errors (rx_crc_errors) increasing — possible hardware failure, request instance replacement" + - "Packet loss on multiple nodes simultaneously — possible upstream network issue" + - "Security group or NACL changes needed that affect other workloads" + - "Issue persists after all node-level checks — may be VPC, TGW, or peering issue" + - "tcpdump shows retransmissions but no local drops — loss is in the network path, not on the node" + +safety_ratings: + - "Log collection (collect), search, errors, network_diagnostics, correlate: GREEN (read-only)" + - "tcpdump_capture, tcpdump_analyze: GREEN (read-only packet capture)" + - "Modify TCP sysctl parameters: YELLOW — operator action, not available via MCP tools" + - "Modify security groups / NACLs: YELLOW — operator action, not available via MCP tools" + - "Replace instance (hardware errors): RED — operator action, requires approval" + +## Common Issues + +- symptoms: "search returns rx_errors or tx_errors > 0 and increasing" + diagnosis: "NIC-level errors. Check ENA driver version and instance health. Use search with query=ena.*version." + resolution: "Operator action: update ENA driver to latest. If errors persist, replace instance (possible hardware issue)." + +- symptoms: "search returns high TCPRetransSegs or TCPTimeouts from /proc/net/snmp" + diagnosis: "TCP retransmissions indicate packet loss in the network path. Use tcpdump_capture to identify where loss occurs." + resolution: "If loss is on-node: check iptables DROP rules via network_diagnostics. If loss is off-node: escalate as VPC/upstream issue." + +- symptoms: "network_diagnostics iptables section shows DROP rules on FORWARD chain" + diagnosis: "Kubernetes NetworkPolicy or custom iptables rules dropping inter-pod traffic." + resolution: "Operator action: review and adjust NetworkPolicy rules. Use search with query=NetworkPolicy to find applied policies." + +- symptoms: "search returns ksoftirqd or NET_RX consuming 100% of one CPU" + diagnosis: "IRQ affinity imbalance — all network interrupts handled by one CPU core." + resolution: "Operator action: enable irqbalance service or configure RPS/XPS for multi-queue distribution." + +- symptoms: "search returns SYN overflow or listen backlog drops" + diagnosis: "TCP SYN queue overflow — too many incoming connections for the backlog size." + resolution: "Operator action: increase net.core.somaxconn and net.ipv4.tcp_max_syn_backlog via sysctl." + +- symptoms: "network_diagnostics routes section shows blackhole or missing route for pod CIDR" + diagnosis: "Routing issue — traffic to certain pod CIDRs has no valid next hop." + resolution: "Operator action: check VPC route tables and CNI routing. May need to restart aws-node DaemonSet." + +- symptoms: "tcpdump_analyze shows retransmissions only for traffic leaving the VPC (cross-AZ or internet)" + diagnosis: "Loss in the upstream path, not on the node. Node-level fixes will not help." + resolution: "Escalate: check VPC peering, TGW, NAT gateway, or internet gateway health." + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +status(executionId="") + +# Step 2: Rule out other D-series SOPs +search(instanceId="i-0abc123def456", query="allowance_exceeded") +# If nonzero → switch to D6/J1 +search(instanceId="i-0abc123def456", query="Frag needed|message too long") +# If found → switch to D4 +search(instanceId="i-0abc123def456", query="nf_conntrack.*table full") +# If found → switch to D3 + +# Step 3: Full network diagnostics +network_diagnostics(instanceId="i-0abc123def456", sections="eni,routes,iptables,cni,dns") + +# Step 4: Check NIC errors +search(instanceId="i-0abc123def456", query="rx_errors|tx_errors|rx_dropped|tx_dropped|rx_crc") + +# Step 5: Check TCP retransmissions +search(instanceId="i-0abc123def456", query="retransmit|TCPRetrans|TCPLoss|TCPTimeouts") + +# Step 6: Check for firewall drops +search(instanceId="i-0abc123def456", query="DROP.*INPUT|DROP.*FORWARD|REJECT") + +# Step 7: Check IRQ distribution +search(instanceId="i-0abc123def456", query="softirq.*NET_RX|ksoftirqd|irqbalance") + +# Step 8: Capture traffic if still inconclusive +tcpdump_capture(instanceId="i-0abc123def456", interface="eth0", duration=30, filter="tcp") +tcpdump_analyze(instanceId="i-0abc123def456", captureId="") + +# Step 9: Correlate timeline +correlate(instanceId="i-0abc123def456", pivotEvent="retransmit", timeWindow=300) + +# Step 10: Generate summary +summarize(instanceId="i-0abc123def456", finding_ids=["F-001","F-002","F-003","F-004"]) +``` + +## Output Format + +```yaml +root_cause: "" +failure_layer: "" +evidence: + - type: network_diagnostics + content: "" + - type: search + content: "" + - type: tcpdump_analyze + content: "" + - type: correlate + content: "" +severity: HIGH +mitigation: + immediate: "Operator: " + long_term: "Monitor NIC errors and TCP retransmissions, tune TCP stack, review NetworkPolicies" +cross_reference: + - "D6/J1 if ENA allowance_exceeded found" + - "D4 if MTU/fragmentation found" + - "D3 if conntrack exhaustion found" + - "D5 if DNS-specific failures found" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D8-kube-proxy-service-connectivity.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D8-kube-proxy-service-connectivity.md new file mode 100644 index 0000000..f10923c --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D8-kube-proxy-service-connectivity.md @@ -0,0 +1,338 @@ +--- +title: "D8 — kube-proxy Failures and Service Connectivity Issues" +description: "Comprehensive SOP for diagnosing kube-proxy crashes, IPVS/iptables mode issues, stale endpoints, service unreachable, and ClusterIP/NodePort/headless service failures" +status: active +severity: HIGH +triggers: + - "kube-proxy.*CrashLoopBackOff" + - "kube-proxy.*error" + - "Failed to list.*Endpoints" + - "error syncing iptables rules" + - "KUBE-SVC chains missing" + - "connection refused.*ClusterIP" + - "no endpoints available" + - "service.*unreachable" + - "ipvs.*error" + - "kube-proxy.*OOMKilled" +owner: devops-agent +objective: "Identify why kube-proxy is failing or service connectivity is broken, covering crash loops, mode misconfiguration, stale rules, version skew, and endpoint issues" +context: > + kube-proxy runs as a DaemonSet on every node and maintains iptables or IPVS rules that map + Service ClusterIPs/NodePorts to pod endpoints. When kube-proxy fails, services become unreachable + even though individual pod IPs may work fine. This SOP covers the full range of kube-proxy issues: + crash loops (OOM, config errors, API server connectivity), iptables mode problems (rule sync failures, + stale chains, rule explosion in large clusters), IPVS mode problems (missing kernel modules, scheduler + misconfiguration), stale endpoints (pods deleted but rules remain), version skew between kube-proxy + and cluster, and service-level connectivity failures (ClusterIP, NodePort, headless, ExternalName). + Cross-references D2 for basic iptables sync, D3 for conntrack exhaustion, D5 for DNS/CoreDNS issues. +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check pod and node state before any log collection: + - List pods in the affected namespace: `kubectl get pods -n -o wide` (via EKS MCP `list_k8s_resources`) + - Verify the client pod is Running and Ready — if the pod is not running, connectivity failure is expected + - Check the Service and its endpoints: `kubectl get svc ` and `kubectl get endpoints ` (via EKS MCP `read_k8s_resource`) — if endpoints list is empty, no backend pods are selected + - Check backend pods are Running and Ready — if backends are down, the Service has no healthy endpoints + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — NotReady nodes break kube-proxy rule sync +- **PREREQUISITE — Does the Service have healthy endpoints?** Before investigating kube-proxy, verify the Service has backends: + - Use `read_k8s_resource` with clusterName, kind=Endpoints, apiVersion=v1, namespace=, name= — if the subsets array is empty or has no addresses, there are no healthy backend pods. That is the root cause, not a kube-proxy issue. + - If endpoints are empty: check if backend pods exist and are passing readiness probes. Report "Service has no healthy endpoints — backend pods are not Ready" immediately. + - ONLY if the Service has healthy endpoints, proceed to kube-proxy investigation below. +- **PREREQUISITE — Is kube-proxy running?** Before investigating iptables/IPVS rules, verify kube-proxy is alive: + - Use `list_k8s_resources` with clusterName, kind=Pod, apiVersion=v1, namespace=kube-system, labelSelector=k8s-app=kube-proxy — check that kube-proxy pod on the affected node is Running. + - If kube-proxy pod is CrashLoopBackOff, Error, or missing: that is the root cause. Report "kube-proxy not running on node — service rules will not be synced" immediately. + - ONLY if kube-proxy is confirmed running, proceed to service connectivity investigation below. +- Use `collect` tool with instanceId to gather logs from the affected node +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId to get pre-indexed findings — look for kube-proxy errors +- Use `network_diagnostics` tool with instanceId and sections=iptables,kube_proxy to get current iptables/IPVS rules and kube-proxy status + +SHOULD: +- Use `search` tool with instanceId and query=`kube-proxy.*error|kube-proxy.*fatal|kube-proxy.*crash|kube-proxy.*OOM` to find kube-proxy failure evidence +- Use `search` tool with query=`KUBE-SVC|KUBE-SEP|KUBE-MARK|KUBE-POSTROUTING` to check if service iptables chains exist +- Use `search` tool with query=`kube-proxy.*mode|mode.*iptables|mode.*ipvs` to determine which proxy mode is configured + +MAY: +- Use `quick_triage` tool with instanceId for a fast overview +- Use `cluster_health` tool with clusterName to check if kube-proxy issues affect multiple nodes +- Use `compare_nodes` tool with instanceIds of affected + healthy node to diff kube-proxy state + +## Phase 2 — Enrich + +### ⚠️ MANDATORY PRE-CHECK: Read CRITICAL_WARNINGS First + +Before investigating ANY VPC CNI configuration, you MUST: +1. Check the `network_diagnostics` response for `CRITICAL_WARNINGS` and `rootCauseRanking` fields +2. If `CRITICAL_WARNINGS` exists, the root cause is ALREADY IDENTIFIED — do not investigate further +3. If `rootCauseRanking` shows kube-proxy as rank 1, the issue is kube-proxy NOT VPC CNI +4. Do NOT form hypotheses about podSGEnforcingMode, SNAT, or any CNI config until you have ruled out kube-proxy + +### ⚠️ MANDATORY: kube-proxy vs CNI Ownership Check + +If service connectivity (ClusterIP, NodePort) is failing: +- KUBE-SERVICES chain empty → kube-proxy issue, NOT CNI. Stop investigating CNI. +- KUBE-SERVICES chain populated → kube-proxy is fine, investigate CNI/routing/SG. + +The VPC CNI NEVER creates, modifies, or reads KUBE-SERVICES chains. The string "KUBE-SERVICES" does not appear anywhere in the VPC CNI codebase. If KUBE-SERVICES is empty, no amount of CNI config changes will fix it. + +Work through these failure domains in order: + +### 2A — kube-proxy Pod Health + +MUST: +- Use `search` tool with query=`kube-proxy.*CrashLoopBackOff|kube-proxy.*restart|kube-proxy.*OOMKilled|kube-proxy.*Error` to check pod status + - CrashLoopBackOff: check for config errors, missing RBAC, or OOM + - OOMKilled: kube-proxy running out of memory (common in large clusters with many services/iptables rules) +- Use `search` tool with query=`kube-proxy.*version|kube-proxy.*v1\.|image.*kube-proxy` to check kube-proxy version + - Version must match cluster Kubernetes version (minor version skew of +/- 1 allowed) + - Outdated kube-proxy addon can cause compatibility issues + +SHOULD: +- Use `search` tool with query=`kube-proxy.*config|kube-proxy-config|ConfigMap.*kube-proxy` to check for configuration errors +- Use `search` tool with query=`kube-proxy.*serviceaccount|kube-proxy.*RBAC|kube-proxy.*forbidden` to check RBAC issues + +### 2B — API Server Connectivity from kube-proxy + +MUST: +- Use `search` tool with query=`kube-proxy.*connection refused|kube-proxy.*timeout|kube-proxy.*unauthorized|kube-proxy.*API` to check if kube-proxy can reach the API server + - Connection refused/timeout: network issue between node and API server (see A4 SOP) + - Unauthorized: service account token expired or RBAC misconfigured +- Use `search` tool with query=`Failed to list.*Service|Failed to list.*Endpoints|Failed to watch` to check if kube-proxy can list/watch services and endpoints + - These failures mean kube-proxy cannot get updates, so rules become stale + +### 2C — iptables Mode Issues + +MUST: +- Review `network_diagnostics` iptables section for KUBE-SVC and KUBE-SEP chains + - No KUBE-SVC chains at all: kube-proxy never synced or is not running + - KUBE-SVC chains exist but point to wrong endpoints: stale rules from deleted pods + - Very large number of rules (>10,000): iptables mode performance degradation in large clusters +- Use `search` tool with query=`error syncing iptables|iptables.*failed|iptables-restore|iptables.*lock` to find iptables sync errors + - "iptables-restore: unable to initialize table": iptables binary issue or kernel module missing + - "Another app is currently holding the xtables lock": contention with other iptables users (CNI, calico, etc.) + +SHOULD: +- Use `search` tool with query=`syncProxyRules|SyncProxyRulesLatency|sync.*duration` to check sync performance + - Sync taking > 1 second: too many rules, consider IPVS mode +- Use `search` tool with query=`masquerade|MASQUERADE|SNAT` to check masquerade rules for NodePort/LoadBalancer services + +### 2D — IPVS Mode Issues + +MUST (if IPVS mode detected): +- Use `search` tool with query=`ipvs.*error|ipvsadm|ip_vs.*module|IPVS` to find IPVS-specific errors + - "can't load module ip_vs": IPVS kernel modules not loaded on the node + - Required modules: ip_vs, ip_vs_rr, ip_vs_wrr, ip_vs_sh, nf_conntrack +- Use `search` tool with query=`ipvs.*scheduler|scheduler.*rr|scheduler.*lc` to check IPVS scheduler configuration +- Use `search` tool with query=`ipvsadm -L|ipvs.*TCP|ipvs.*UDP` to verify IPVS entries exist for services — after switching to IPVS, `ipvsadm -L` should show TCP/UDP entries for each Service ClusterIP + +SHOULD: +- Use `search` tool with query=`ipvsadm -L|ip_vs_` to check if IPVS entries exist for services +- Use `search` tool with query=`kube-ipvs0|dummy.*interface` to check if the kube-ipvs0 dummy interface exists + - Missing kube-ipvs0: IPVS mode not properly initialized +- Use `search` tool with query=`KUBE-SVC.*iptables|iptables.*KUBE-SVC` to check if stale iptables rules remain after switching to IPVS — old KUBE-SVC chains should be cleaned up after IPVS is confirmed working + +### 2E — Stale Endpoints and Service Connectivity + +MUST: +- Use `search` tool with query=`no endpoints available|endpoints.*not found|connection refused.*10\.|connection refused.*ClusterIP` to find service connectivity failures + - "no endpoints available": service has no ready pods, or kube-proxy has stale endpoint list +- Use `search` tool with query=`stale.*endpoint|endpoint.*slice|EndpointSlice` to check for stale endpoint issues +- Use `network_diagnostics` iptables section — look for KUBE-SEP entries pointing to IPs of pods that no longer exist + +SHOULD: +- Use `search` tool with query=`NodePort|nodePort|externalTrafficPolicy|healthCheckNodePort` to check NodePort service configuration + - externalTrafficPolicy=Local with no local pods: service returns connection refused on that node +- Use `search` tool with query=`headless|clusterIP.*None` to check headless service issues + - Headless services rely on DNS, not kube-proxy — redirect to D5 DNS SOP if headless service fails + +### 2F — Conntrack Interaction + +SHOULD: +- Use `search` tool with query=`nf_conntrack|conntrack.*table|conntrack.*drop` to check if conntrack issues are contributing to service failures + - Conntrack table full causes new connections to be dropped even if kube-proxy rules are correct + - If found: cross-reference D3 conntrack exhaustion SOP + +### 2G — Control Plane kube-audit Logs + +SHOULD: +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="Service" to check for recent Service create/update/delete events that may have changed ClusterIP, ports, or selectors +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="Endpoints" to check for Endpoints/EndpointSlice mutations that could cause stale endpoint issues +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="kube-proxy" to check for kube-proxy ConfigMap changes (mode switch, config updates) +- Correlate timestamps of Service/Endpoints mutations with the onset of connectivity failures + +### Timeline Correlation + +MUST: +- Use `correlate` tool with instanceId and pivotEvent set to the most prominent kube-proxy error to build a timeline + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from all kube-proxy and service connectivity findings +- State root cause with specific failure domain: + - Pod health: CrashLoopBackOff, OOM, version skew + - API connectivity: cannot list/watch services and endpoints + - iptables: sync failure, lock contention, rule explosion + - IPVS: missing kernel modules, scheduler misconfiguration, missing kube-ipvs0 interface + - Stale endpoints: rules pointing to deleted pods + - Service config: externalTrafficPolicy=Local with no local pods, headless service DNS issue + - Conntrack: table full causing connection drops (cross-ref D3) +- Recommend specific fix (operator action — not available via MCP tools) + +SHOULD: +- Include kube-proxy version and mode from search results +- Include evidence of missing/stale rules from network_diagnostics +- Include sync latency if available + +MAY: +- Recommend IPVS mode for clusters with > 1,000 services to avoid iptables performance issues +- Recommend updating kube-proxy addon to match cluster version +- Recommend NodeLocal DNSCache for headless service DNS performance + +## Guardrails + +anti_hallucination: + - "CRITICAL: POD_SECURITY_GROUP_ENFORCING_MODE (podSGEnforcingMode) does NOT create a default-deny for unannotated pods. It ONLY affects pods that have the vpc.amazonaws.com/pod-eni annotation (Security Groups for Pods). In strict mode, annotated pods use their branch ENI exclusively instead of falling back to the primary ENI. Pods WITHOUT SGP annotations are COMPLETELY UNAFFECTED — they use the primary ENI and normal VPC routing. Do NOT blame podSGEnforcingMode for service connectivity failures on pods without SGP annotations. If ClusterIP traffic is failing, check kube-proxy health and KUBE-SERVICES iptables chain FIRST." + +escalation_conditions: + - "kube-proxy CrashLoopBackOff on all nodes — cluster-wide service outage" + - "kube-proxy cannot reach API server — check cluster endpoint and network" + - "iptables-restore failing due to kernel module issue — node OS problem" + - "IPVS mode switch caused service disruption — rollback to iptables mode" + - "Stale endpoints persisting after kube-proxy restart — possible API server or etcd issue" + +safety_ratings: + - "Log collection (collect), search, errors, network_diagnostics, correlate, compare_nodes: GREEN (read-only)" + - "Restart kube-proxy DaemonSet: YELLOW — operator action, not available via MCP tools" + - "Switch kube-proxy mode (iptables to IPVS): RED — disruptive, operator action, requires off-hours" + - "Update kube-proxy addon version: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "read_k8s_resource for Endpoints shows empty subsets or no addresses" + diagnosis: "Service has no healthy backend pods. This is not a kube-proxy issue — the Service selector does not match any Running/Ready pods." + resolution: "Operator action: verify pods matching the Service selector exist and are passing readiness probes. Check 'kubectl get pods -l ' and 'kubectl describe pod' for readiness probe failures." + +- symptoms: "list_k8s_resources returns kube-proxy pod in CrashLoopBackOff, Error, or missing on the affected node" + diagnosis: "kube-proxy is not running. Service iptables/IPVS rules will not be synced on this node." + resolution: "Operator action: check kube-proxy logs, restart kube-proxy DaemonSet, check kube-proxy-config ConfigMap." + +- symptoms: "search returns kube-proxy CrashLoopBackOff or OOMKilled" + diagnosis: "kube-proxy pod crashing. OOM common in large clusters (>1000 services) with iptables mode. Check kube-proxy logs for config errors." + resolution: "Operator action: if OOM, increase kube-proxy memory limits or switch to IPVS mode. If config error, fix kube-proxy-config ConfigMap." + +- symptoms: "search returns Failed to list Services or Failed to watch Endpoints" + diagnosis: "kube-proxy cannot reach API server or RBAC is misconfigured. Rules become stale." + resolution: "Operator action: check network connectivity to API server, verify kube-proxy service account has correct ClusterRole binding." + +- symptoms: "network_diagnostics shows no KUBE-SVC chains in iptables" + diagnosis: "kube-proxy is not running or never synced rules on this node." + resolution: "Operator action: check kube-proxy DaemonSet status, restart kube-proxy pods (kubectl -n kube-system rollout restart ds kube-proxy)." + +- symptoms: "search returns iptables-restore error or xtables lock contention" + diagnosis: "iptables binary issue or lock contention with CNI (calico, cilium) or other iptables users." + resolution: "Operator action: check for concurrent iptables users, increase iptables lock timeout, or switch to IPVS mode." + +- symptoms: "search returns syncProxyRules taking > 1 second" + diagnosis: "Too many iptables rules. iptables mode has O(n) performance — degrades with many services." + resolution: "Operator action: switch to IPVS mode for O(1) lookup performance. See AWS docs for IPVS setup: install ipvsadm, load kernel modules, update kube-proxy addon config." + +- symptoms: "IPVS mode configured but search returns 'can't load module ip_vs'" + diagnosis: "IPVS kernel modules not loaded on the worker node. Required modules: ip_vs, ip_vs_rr, ip_vs_wrr, ip_vs_sh, nf_conntrack." + resolution: "Operator action: install ipvsadm package, load kernel modules (modprobe ip_vs ip_vs_rr ip_vs_wrr ip_vs_sh nf_conntrack), persist in /etc/modules-load.d/ipvs.conf. For worker node bootstrap, add modprobe commands to user data." + +- symptoms: "IPVS mode configured but search returns missing kube-ipvs0 dummy interface" + diagnosis: "IPVS mode not properly initialized. The kube-ipvs0 dummy interface is required for IPVS to bind Service ClusterIPs." + resolution: "Operator action: verify IPVS kernel modules are loaded, restart kube-proxy. The kube-ipvs0 interface is created automatically when kube-proxy starts in IPVS mode with all required modules." + +- symptoms: "after switching to IPVS mode, search returns stale KUBE-SVC iptables entries" + diagnosis: "After switching from iptables to IPVS mode, old iptables KUBE-SVC chains were not cleaned up. This can cause routing conflicts." + resolution: "Operator action: after confirming IPVS is working (ipvsadm -L shows TCP/UDP entries for services), flush stale iptables rules. Restart kube-proxy pods to ensure clean state." + +- symptoms: "search returns kube-proxy IPVS configuration via managed addon" + diagnosis: "IPVS mode can be configured via the EKS managed kube-proxy addon using configuration values." + resolution: "Operator action: update kube-proxy addon with IPVS config — aws eks update-addon --cluster-name --addon-name kube-proxy --configuration-values '{\"ipvs\": {\"scheduler\": \"rr\"}, \"mode\": \"ipvs\"}'. Ensure worker nodes have ipvsadm installed and IPVS kernel modules loaded before switching." + +- symptoms: "search returns 'no endpoints available' for a service" + diagnosis: "Service has no ready pods, or kube-proxy has stale endpoint list. Check if pods are running and ready." + resolution: "Operator action: verify pods are running and passing readiness probes. If pods are ready but endpoints missing, restart kube-proxy." + +- symptoms: "NodePort service returns connection refused on some nodes but works on others" + diagnosis: "externalTrafficPolicy=Local set on the service, and the node has no local pods for that service." + resolution: "Operator action: change to externalTrafficPolicy=Cluster (adds extra hop but works on all nodes), or ensure pods are scheduled on all nodes via DaemonSet." + +- symptoms: "kube-proxy version does not match cluster Kubernetes version" + diagnosis: "Version skew between kube-proxy addon and cluster. Can cause compatibility issues with API changes." + resolution: "Operator action: update kube-proxy addon to match cluster version via 'aws eks update-addon --cluster-name --addon-name kube-proxy --addon-version '." + +- symptoms: "tcpdump shows the same source connecting to both a ClusterIP (10.x.x.x) and a different PodIP on the same destination port — appears as duplicate or phantom traffic" + diagnosis: "THIS IS NORMAL BEHAVIOR. kube-proxy performs DNAT (Destination NAT) on ClusterIP Service traffic, rewriting the destination from the ClusterIP to a backend PodIP. tcpdump on the node captures BOTH the pre-DNAT packet (to ClusterIP) and the post-DNAT packet (to PodIP), making it look like the same source is talking to two different destinations. This is standard iptables/IPVS Service routing." + resolution: "No action required — this is working as designed. If you need to trace a specific flow, filter tcpdump by the pod IP rather than the ClusterIP to see only the actual backend traffic." + +- symptoms: "tcpdump shows TCP RST packets on ports 10250, 10256, 8080, or other health-check ports after very short-lived connections" + diagnosis: "THIS IS NORMAL BEHAVIOR. Kubernetes liveness and readiness probes open a TCP connection to verify the port is listening, then immediately close it. This produces TCP RST packets. kubelet probes on port 10250, kube-proxy health on 10256, and application probes on 8080/443 all exhibit this pattern. See https://docs.aws.amazon.com/prescriptive-guidance/latest/ha-resiliency-amazon-eks-apps/probes-checks.html" + resolution: "No action required — these RSTs are expected probe behavior, not connection failures. A high RST rate from probes alone is not a concern." + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +status(executionId="") + +# Step 2: Get kube-proxy findings +errors(instanceId="i-0abc123def456") + +# Step 3: Full network diagnostics including iptables and kube-proxy +network_diagnostics(instanceId="i-0abc123def456", sections="iptables,kube_proxy") + +# Step 4: Check kube-proxy pod health +search(instanceId="i-0abc123def456", query="kube-proxy.*CrashLoopBackOff|kube-proxy.*OOMKilled|kube-proxy.*error") + +# Step 5: Check kube-proxy mode and version +search(instanceId="i-0abc123def456", query="kube-proxy.*mode|mode.*iptables|mode.*ipvs|kube-proxy.*version") + +# Step 6: Check API server connectivity +search(instanceId="i-0abc123def456", query="kube-proxy.*connection refused|Failed to list.*Service|Failed to watch") + +# Step 7: Check iptables sync +search(instanceId="i-0abc123def456", query="error syncing iptables|iptables.*lock|syncProxyRules") + +# Step 8: Check IPVS (if applicable) +search(instanceId="i-0abc123def456", query="ipvs.*error|ip_vs.*module|ipvsadm|kube-ipvs0") + +# Step 9: Check stale endpoints +search(instanceId="i-0abc123def456", query="no endpoints available|stale.*endpoint|connection refused.*ClusterIP") + +# Step 10: Correlate timeline +correlate(instanceId="i-0abc123def456", pivotEvent="kube-proxy", timeWindow=300) + +# Step 11: Generate summary +summarize(instanceId="i-0abc123def456", finding_ids=["F-001","F-002","F-003"]) +``` + +## Output Format + +```yaml +root_cause: "" +kube_proxy_mode: "" +kube_proxy_version: "" +evidence: + - type: network_diagnostics + content: "" + - type: search + content: "" + - type: correlate + content: "" +severity: HIGH +mitigation: + immediate: "Operator: " + long_term: "Keep kube-proxy addon updated, consider IPVS for large clusters, monitor sync latency" +cross_reference: + - "D2 for basic iptables sync issues" + - "D3 if conntrack exhaustion contributing to service failures" + - "D5 if headless service DNS resolution failing" + - "I1 if kube-proxy version skew detected" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D9-pod-to-pod-connectivity.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D9-pod-to-pod-connectivity.md new file mode 100644 index 0000000..7b77520 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/D9-pod-to-pod-connectivity.md @@ -0,0 +1,387 @@ +--- +title: "D9 — Pod-to-Pod Connectivity Failures" +description: "Diagnose inter-pod communication failures where pods have IPs but cannot reach each other, using tcpdump on pod veth interfaces and worker node eth0 to trace packet flow through the VPC CNI data path" +status: active +severity: HIGH +triggers: + - "connection refused.*pod" + - "connection timed out.*pod" + - "no route to host.*10\\.\\d+" + - "network unreachable.*pod" + - "i/o timeout.*pod" + - "NetworkPolicy.*deny" + - "DENY.*verdict" + - "policyendpoint" + - "pod.*cannot reach.*pod" + - "inter-pod.*fail" +owner: devops-agent +objective: "Trace the packet path from source pod through veth pair, node routing, and CNI data path to destination pod, identifying where packets are dropped or rejected" +context: > + This SOP covers scenarios where pods have valid IP addresses (not stuck in ContainerCreating — that is D1) + and services are not involved (that is D8), but direct pod-to-pod communication fails. The VPC CNI assigns + real VPC IPs to pods via ENIs. Traffic between pods on the same node traverses the veth pair and Linux bridge/ + routing table. Traffic between pods on different nodes goes through the node's eth0, VPC routing, and the + destination node's ENI. Failures can occur at any layer: veth misconfiguration, iptables/eBPF NetworkPolicy + enforcement dropping traffic, missing routes for pod CIDRs, CNI plugin bugs, security group rules blocking + inter-node traffic, or NACL restrictions. This SOP uses tcpdump_capture on both the pod's veth interface + and the node's eth0 to pinpoint exactly where packets are lost. Cross-references D1 (IP allocation), D3 + (conntrack), D5 (DNS), D7 (general network perf), D8 (kube-proxy/service connectivity). +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check pod and node state before any log collection: + - List pods in the affected namespace: `kubectl get pods -n -o wide` (via EKS MCP `list_k8s_resources`) + - Verify source and destination pods are Running and Ready — if pods are Pending, CrashLoopBackOff, or Terminating, that is the root cause, not a network issue + - Check which nodes the pods are on — same node vs different nodes changes the investigation path entirely + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — if a node is NotReady, network will fail for all pods on it + - Check pod events: `kubectl describe pod ` (via EKS MCP `get_k8s_events`) for scheduling failures, OOM kills, or image pull errors +- Use `collect` tool with instanceId of the SOURCE node (where the calling pod runs) to gather logs +- Use `status` tool with executionId to poll until collection completes +- **PREREQUISITE — Is aws-node (VPC CNI) running on the source node?** Pod networking requires the CNI: + - Use `list_k8s_resources` with clusterName, kind=Pod, apiVersion=v1, namespace=kube-system, labelSelector=k8s-app=aws-node — check that aws-node pod on the SOURCE node is Running. + - If aws-node pod is CrashLoopBackOff, Error, or missing: that is the root cause. Report "aws-node (VPC CNI) not running on source node — pod networking is broken at the CNI level" immediately. + - ONLY if aws-node is confirmed running on the source node, proceed to connectivity investigation below. +- Use `errors` tool with instanceId to get pre-indexed findings — look for network/CNI errors +- Use `network_diagnostics` tool with instanceId and sections=eni,routes,iptables,cni to get the full network picture on the source node + +SHOULD: +- Use `search` tool with instanceId and query=`connection refused|connection timed out|no route to host|network unreachable|i/o timeout` to find pod-level connectivity errors +- Use `search` tool with query=`NetworkPolicy|policyendpoint|DENY|verdict|network-policy-agent` to check if NetworkPolicy enforcement is blocking traffic +- Use `search` tool with query=`aws-node|ipamd|eni.*error|veth|cni.*error` to check VPC CNI health +- Determine if the issue is same-node (pods on same worker) or cross-node (pods on different workers): + - Same-node: packets traverse veth → Linux routing → veth (never hits eth0) + - Cross-node: packets traverse veth → Linux routing → eth0 → VPC → dest eth0 → dest veth + +MAY: +- Use `quick_triage` tool with instanceId for a fast overview +- Use `cluster_health` tool with clusterName to check if connectivity failure is widespread +- If destination pod is on a different node, also use `collect` tool with the DESTINATION node instanceId + +## Phase 2 — Enrich + +### ⚠️ MANDATORY PRE-CHECK: Read CRITICAL_WARNINGS First + +Before investigating ANY VPC CNI configuration, you MUST: +1. Check the `network_diagnostics` response for `CRITICAL_WARNINGS` and `rootCauseRanking` fields +2. If `CRITICAL_WARNINGS` exists, the root cause is ALREADY IDENTIFIED — do not investigate further +3. If `rootCauseRanking` shows kube-proxy as rank 1, the issue is kube-proxy NOT VPC CNI +4. Do NOT form hypotheses about podSGEnforcingMode, SNAT, or any CNI config until you have ruled out kube-proxy + +### ⚠️ MANDATORY: kube-proxy vs CNI Ownership Check + +If service connectivity (ClusterIP, NodePort) is failing: +- KUBE-SERVICES chain empty → kube-proxy issue, NOT CNI. Stop investigating CNI. +- KUBE-SERVICES chain populated → kube-proxy is fine, investigate CNI/routing/SG. + +The VPC CNI NEVER creates, modifies, or reads KUBE-SERVICES chains. The string "KUBE-SERVICES" does not appear anywhere in the VPC CNI codebase. If KUBE-SERVICES is empty, no amount of CNI config changes will fix it. + +MUST work through these layers in order to isolate where packets are dropped: + +### 2A — VPC CNI Health and Pod IP Assignment + +MUST: +- Use `search` tool with query=`ipamd.*error|ipamd.*failed|aws-node.*error|aws-node.*restart|aws-node.*CrashLoopBackOff` to check CNI DaemonSet health + - If aws-node is crashing: pod networking is broken at the CNI level — no further packet tracing needed +- Use `search` tool with query=`eni.*attached|eni.*allocated|secondary.*IP|warm.*pool|WARM_IP_TARGET|WARM_ENI_TARGET` to verify ENI/IP allocation is healthy +- Use `search` tool with query=`veth.*error|veth.*not found|link.*not found|device.*not found` to check if pod veth interfaces exist + - Missing veth: CNI failed to set up the pod network namespace + +SHOULD: +- Use `search` tool with query=`aws-node.*version|vpc-cni.*version|VPC_CNI_VERSION` to check VPC CNI version + - Known buggy version: VPC CNI v1.20.4 has issues +- Use `search` tool with query=`ENABLE_NETWORK_POLICY|enableNetworkPolicy|network-policy-agent` to check if NetworkPolicy enforcement is enabled +- Use `search` tool with query=`kube-proxy.*not running|kube-proxy.*CrashLoop` to check kube-proxy health — kube-proxy must be running for aws-node to reach Ready state. If kube-proxy is down, pod networking is broken at a lower level. + +### 2B — NetworkPolicy Enforcement (Most Common Cause) + +MUST: +- Use `search` tool with query=`NetworkPolicy|network-policy|policyendpoint|DENY.*verdict|ACCEPT.*verdict` to check if NetworkPolicy is blocking traffic + - VPC CNI NetworkPolicy uses eBPF to enforce — check `/var/log/aws-routed-eni/network-policy-agent.log` + - DENY verdicts in flow logs confirm NetworkPolicy is dropping packets +- Use `search` tool with query=`calico|cilium|Calico.*policy|CiliumNetworkPolicy` to check for third-party NetworkPolicy engines + - After migrating from Calico to VPC CNI NetworkPolicy: port limit is 24 unique port combinations per ingress/egress selector — use port ranges instead + - Calico and VPC CNI NetworkPolicy cannot run simultaneously + +SHOULD: +- Use `search` tool with query=`default.*deny|deny.*all|ingress.*deny|egress.*deny` to check for default-deny policies + - Default-deny with no matching allow policy = all pod traffic blocked +- Use `search` tool with query=`policyendpoints.*not found|policyendpoints.*error|Kyverno.*block` to check if policyendpoint CRD creation is blocked + - If policyendpoints are not created: NetworkPolicy controller cannot push rules to the agent + +### 2C — iptables / eBPF Rules on the Node + +MUST: +- Review `network_diagnostics` iptables section for DROP or REJECT rules on FORWARD chain + - Custom iptables rules or security tools can silently drop inter-pod traffic + - Check for rules matching pod CIDR ranges +- Use `search` tool with query=`DROP.*FORWARD|REJECT.*FORWARD|iptables.*FORWARD.*policy` to find FORWARD chain drops + - CRITICAL: custom AMIs may set iptables FORWARD policy to DROP — must be ACCEPT for pod networking +- Use `search` tool with query=`FORWARD.*ACCEPT|FORWARD.*policy.*ACCEPT` to verify FORWARD chain default is ACCEPT + +SHOULD: +- Use `search` tool with query=`nf_conntrack.*table full|conntrack.*drop` to check if conntrack exhaustion is causing drops + - If found: cross-reference D3 conntrack exhaustion SOP +- Use `search` tool with query=`ebpf.*error|bpf.*error|tc.*filter|cls_bpf` to check for eBPF program errors (VPC CNI NetworkPolicy uses eBPF) + +### 2D — Routing (Same-Node vs Cross-Node) + +MUST: +- Review `network_diagnostics` routes section for pod CIDR routing + - Same-node: route for pod IP should point to the veth interface + - Cross-node: route for remote pod CIDR should go via eth0 (VPC routing handles it) +- Use `search` tool with query=`blackhole|unreachable|no route|missing.*route|ip route` to find routing issues + - Blackhole route for pod CIDR: CNI routing table corruption + - Missing route for remote pod CIDR: VPC route table issue or CNI custom networking misconfiguration + +SHOULD: +- Use `search` tool with query=`ENIConfig|custom.*networking|CUSTOM_NETWORKING|AWS_VPC_K8S_CNI_CUSTOM_NETWORK_CFG` to check if custom networking is enabled + - Custom networking uses separate subnets for pods — routing must account for this +- Use `search` tool with query=`prefix.*delegation|ENABLE_PREFIX_DELEGATION|/28` to check if prefix delegation is enabled + - Prefix delegation assigns /28 blocks — routing is different from secondary IP mode + +### 2E — Security Groups (Cross-Node Traffic) + +MUST (for cross-node pod communication): +- Use `search` tool with query=`security group|sg-|SecurityGroupIds` to find security group configuration + - Worker node security groups MUST allow all traffic between nodes in the cluster + - Specifically: allow all TCP/UDP from the cluster security group to itself +- Use `network_diagnostics` eni section to check which security groups are attached to the node's ENIs + +SHOULD: +- Use `search` tool with query=`NACL|network ACL|acl-` to check for NACL restrictions + - NACLs are stateless — must allow both request and response traffic for pod CIDRs +- Use `search` tool with query=`SecurityGroupsForPods|ENABLE_POD_ENI|trunk.*ENI` to check if Security Groups for Pods is enabled + - SGP uses branch ENIs with separate security groups per pod — can cause inter-pod isolation if SGs don't allow each other + +### 2F — Packet Capture (tcpdump) + +MUST (if the issue is not identified from log analysis above): +- Use `tcpdump_capture` tool on the SOURCE node to capture traffic on the pod's veth interface: + - Filter for traffic to/from the destination pod IP + - Duration: 30 seconds while reproducing the connectivity failure + - This shows if packets LEAVE the source pod +- Use `tcpdump_capture` tool on the SOURCE node to capture on eth0: + - Same filter for destination pod IP + - This shows if packets reach the node's outbound interface (cross-node) or are dropped before +- Use `tcpdump_analyze` tool to analyze both captures: + - Packets on veth but NOT on eth0: dropped by iptables/eBPF/routing on the source node + - Packets on eth0 of source but not arriving at destination: dropped in VPC (SG, NACL, routing) + - Packets arriving at destination eth0 but not on destination veth: dropped on destination node + +SHOULD: +- If cross-node: also use `tcpdump_capture` on the DESTINATION node's eth0 and the destination pod's veth + - This gives the full 4-point trace: src-veth → src-eth0 → dst-eth0 → dst-veth +- Use `tcpdump_analyze` to check for: + - TCP RST (connection refused — something is actively rejecting) + - TCP SYN with no SYN-ACK (packets silently dropped) + - ICMP unreachable messages (routing or firewall rejection) + +### 2G — Control Plane kube-audit Logs + +SHOULD: +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="NetworkPolicy" to check for recent NetworkPolicy create/update/delete events that may be blocking traffic +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="policyendpoints" to check for policyendpoint CRD mutations +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="securitygroups" to check for SecurityGroupPolicy changes (if SGP is in use) +- Correlate timestamps of NetworkPolicy mutations with the onset of connectivity failures — a recently applied deny-all policy is a common root cause + +### Timeline Correlation + +MUST: +- Use `correlate` tool with instanceId and pivotEvent set to the connectivity error pattern to build a timeline + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from all pod connectivity findings +- State root cause with the specific layer identified: + - CNI health: aws-node crash, veth not created, IP not assigned + - NetworkPolicy: DENY verdict blocking inter-pod traffic, default-deny with no allow rule, Calico migration port limit + - iptables/eBPF: FORWARD chain DROP policy, custom iptables rules, eBPF program error + - Routing: missing route for pod CIDR, blackhole route, custom networking misconfiguration + - Security groups: node SGs not allowing inter-node traffic, SGP branch ENI isolation + - NACLs: stateless rules blocking pod CIDR traffic + - VPC routing: missing route for pod CIDR in VPC route table +- Include tcpdump evidence showing exactly where packets were lost in the path +- Recommend specific fix (operator action — not available via MCP tools) + +SHOULD: +- Include the 4-point packet trace results if captured (src-veth, src-eth0, dst-eth0, dst-veth) +- Include NetworkPolicy flow log evidence if applicable +- Include VPC CNI version and configuration + +MAY: +- Recommend enabling VPC CNI NetworkPolicy flow logs for ongoing visibility +- Recommend VPC Flow Logs for cross-node packet loss investigation +- Recommend using `kubectl exec` to test connectivity from within pods (operator action) + +## Guardrails + +anti_hallucination: + - "CRITICAL: POD_SECURITY_GROUP_ENFORCING_MODE (podSGEnforcingMode) does NOT create a default-deny for unannotated pods. It ONLY affects pods that have the vpc.amazonaws.com/pod-eni annotation (Security Groups for Pods). In strict mode, annotated pods use their branch ENI exclusively instead of falling back to the primary ENI. Pods WITHOUT SGP annotations are COMPLETELY UNAFFECTED — they use the primary ENI and normal VPC routing. Do NOT blame podSGEnforcingMode for pod-to-pod connectivity failures on pods without SGP annotations. If pod-to-pod traffic is failing, check kube-proxy health, KUBE-SERVICES iptables chain, NetworkPolicy enforcement, and security groups FIRST." + +escalation_conditions: + - "aws-node DaemonSet crashing on multiple nodes — cluster-wide CNI failure" + - "iptables FORWARD policy is DROP on custom AMI — requires AMI rebuild or node-level fix" + - "VPC route table missing pod CIDR routes — requires VPC-level changes" + - "Security Groups for Pods causing isolation — requires SG rule changes across multiple SGs" + - "tcpdump shows packets leaving source node but never arriving at destination — VPC-level issue" + - "NetworkPolicy migration from Calico hitting port limit — requires policy redesign" + +safety_ratings: + - "Log collection (collect), search, errors, network_diagnostics, correlate, compare_nodes: GREEN (read-only)" + - "tcpdump_capture, tcpdump_analyze: GREEN (read-only packet capture on node)" + - "Modify NetworkPolicy: YELLOW — operator action, not available via MCP tools" + - "Modify iptables FORWARD policy: YELLOW — operator action, affects all pod traffic on node" + - "Modify security groups: YELLOW — operator action, affects network access" + - "Restart aws-node DaemonSet: YELLOW — operator action, brief pod networking disruption" + - "Modify VPC route tables: RED — operator action, VPC-wide impact" + +## Common Issues + +- symptoms: "list_k8s_resources returns aws-node pod in CrashLoopBackOff, Error, or missing on the source or destination node" + diagnosis: "VPC CNI DaemonSet not running. Pod networking is broken at the CNI level — no further packet tracing needed." + resolution: "Operator action: check aws-node logs (kubectl logs -n kube-system -l k8s-app=aws-node). Common fixes: update VPC CNI addon, check IAM permissions (AmazonEKS_CNI_Policy), verify subnet has available IPs." + +- symptoms: "search returns DENY verdict in network-policy-agent.log for the pod IPs" + diagnosis: "NetworkPolicy is blocking inter-pod traffic. Check if a default-deny policy exists without a matching allow rule." + resolution: "Operator action: review NetworkPolicy rules. Add an allow rule for the required traffic, or remove the overly restrictive policy. Use 'kubectl get networkpolicy -A' to list all policies." + +- symptoms: "search returns iptables FORWARD chain policy is DROP" + diagnosis: "Custom AMI has iptables FORWARD policy set to DROP. This blocks all inter-pod traffic that traverses the FORWARD chain." + resolution: "Operator action: set iptables FORWARD policy to ACCEPT. Add 'iptables -P FORWARD ACCEPT' to kubelet.service or node bootstrap. Rebuild AMI for permanent fix." + +- symptoms: "tcpdump shows packets on source veth but NOT on source eth0 (cross-node)" + diagnosis: "Packets are being dropped on the source node between the veth and eth0. Check iptables FORWARD chain, eBPF programs, and routing table." + resolution: "Operator action: check 'iptables -L FORWARD -n -v' for DROP rules matching pod CIDRs. Check for eBPF programs with 'tc filter show dev egress'." + +- symptoms: "tcpdump shows packets on source eth0 but NOT on destination eth0" + diagnosis: "Packets are lost in the VPC. Security group, NACL, or VPC route table is dropping the traffic." + resolution: "Operator action: verify node security groups allow all traffic from the cluster security group. Check NACLs allow pod CIDR ranges. Check VPC route tables have routes for all pod CIDRs." + +- symptoms: "tcpdump shows packets on destination eth0 but NOT on destination veth" + diagnosis: "Packets arrive at the destination node but are dropped before reaching the pod. Check iptables/eBPF on the destination node and verify the destination pod's veth exists." + resolution: "Operator action: run the same iptables and NetworkPolicy checks on the destination node. Verify the destination pod is Running and its veth interface exists." + +- symptoms: "search returns aws-node CrashLoopBackOff or ipamd errors" + diagnosis: "VPC CNI DaemonSet is unhealthy. Pod networking is broken at the CNI level." + resolution: "Operator action: check aws-node logs with 'kubectl logs -n kube-system -l k8s-app=aws-node'. Common fixes: update VPC CNI addon, check IAM permissions for the CNI role, verify subnet has available IPs." + +- symptoms: "search returns veth not found or device not found for a pod" + diagnosis: "Pod's veth interface was not created by the CNI. Pod has an IP but no network path." + resolution: "Operator action: delete and recreate the pod. If persistent, restart aws-node on the affected node. Check CNI logs for setup errors." + +- symptoms: "search returns policyendpoints not found or Kyverno blocking policyendpoint creation" + diagnosis: "NetworkPolicy controller cannot create policyendpoint CRDs. Policies are defined but not enforced — or enforced incorrectly." + resolution: "Operator action: check ClusterRole permissions for aws-node and eks:network-policy-controller. If Kyverno is installed, ensure it allows policyendpoint creation." + +- symptoms: "cross-node pod communication fails but same-node works fine" + diagnosis: "VPC-level issue: security groups, NACLs, or route tables blocking inter-node traffic on pod CIDRs." + resolution: "Operator action: verify node security groups allow all traffic from cluster SG. Check VPC route tables. Enable VPC Flow Logs to see rejected packets." + +- symptoms: "search returns Calico migration errors or port limit exceeded" + diagnosis: "After migrating from Calico to VPC CNI NetworkPolicy, the 24 unique port combination limit per selector is exceeded." + resolution: "Operator action: consolidate NetworkPolicy port specifications into port ranges (e.g., 8000-8100 instead of listing 25+ individual ports)." + +- symptoms: "SecurityGroupsForPods enabled and pods in different SGs cannot communicate" + diagnosis: "Branch ENIs have separate security groups. Pod A's SG does not allow traffic from Pod B's SG and vice versa." + resolution: "Operator action: update the security groups assigned to pods to allow traffic from each other's SGs." + +- symptoms: "tcpdump shows outbound packets to external (non-RFC1918) IPs with the node IP as source instead of the pod IP" + diagnosis: "THIS IS NORMAL BEHAVIOR. By default, VPC CNI performs SNAT (Source NAT) on pod traffic destined outside the VPC, translating the pod source IP to the node's primary ENI IP. This is controlled by AWS_VPC_K8S_CNI_EXTERNALSNAT (default: false, meaning SNAT is enabled). See https://docs.aws.amazon.com/eks/latest/userguide/external-snat.html" + resolution: "No action required — this is default VPC CNI behavior. If pods need to preserve their source IP for external traffic (e.g., for VPN or Direct Connect peers), set AWS_VPC_K8S_CNI_EXTERNALSNAT=true on the aws-node DaemonSet." + +- symptoms: "tcpdump shows a small number of ICMP port-unreachable or host-unreachable messages during or shortly after a rolling update or pod deletion" + diagnosis: "THIS IS NORMAL BEHAVIOR. When a pod is deleted, VPC CNI places the pod's IP in a 30-second cooldown cache before recycling it. During this window, kube-proxy iptables rules on other nodes may still route traffic to the old pod IP, resulting in ICMP unreachable responses. This is a transient condition that resolves once all nodes update their iptables rules. See https://docs.aws.amazon.com/eks/latest/best-practices/vpc-cni.html" + resolution: "No action required if the count is small and occurs during deployments. If persistent, check that kube-proxy is running and syncing rules on all nodes. Ensure pods have proper preStop hooks to allow graceful connection draining." + +- symptoms: "tcpdump shows zero-length ACK packets (TCP keepalives) on long-lived connections like gRPC, database pools, or websockets" + diagnosis: "THIS IS NORMAL BEHAVIOR. Applications and kernels send TCP keepalive probes to prevent idle connection timeout by NAT Gateway (350s idle timeout), NLB, or conntrack table eviction. These appear as small ACK-only packets with length 0 on established connections. See https://aws.amazon.com/blogs/networking-and-content-delivery/implementing-long-running-tcp-connections-within-vpc-networking/" + resolution: "No action required — keepalives are essential for maintaining long-lived connections through stateful network devices. If connections are still dropping, check that the keepalive interval is shorter than the NAT Gateway/NLB idle timeout (350s)." + +- symptoms: "tcpdump shows ICMP 'fragmentation needed' or 'packet too big' messages" + diagnosis: "THIS IS NORMAL BEHAVIOR. These are Path MTU Discovery (PMTUD) messages indicating the network is telling the sender to reduce packet size. Common when traffic crosses MTU boundaries (e.g., 9001 jumbo frames on EC2 to 1500 standard MTU on VPN/internet)." + resolution: "No action required — PMTUD is working correctly. If application performance is affected, ensure the application respects the DF (Don't Fragment) bit and adjusts MSS accordingly. Check MTU settings on all interfaces in the path." + +## Examples + +``` +# Step 1: Collect logs from the SOURCE node +collect(instanceId="i-0abc123def456") +status(executionId="") + +# Step 2: Get findings +errors(instanceId="i-0abc123def456") + +# Step 3: Full network diagnostics on source node +network_diagnostics(instanceId="i-0abc123def456", sections="eni,routes,iptables,cni") + +# Step 4: Check VPC CNI health +search(instanceId="i-0abc123def456", query="aws-node|ipamd|veth.*error|cni.*error") + +# Step 5: Check NetworkPolicy enforcement +search(instanceId="i-0abc123def456", query="NetworkPolicy|DENY.*verdict|policyendpoint|network-policy-agent") + +# Step 6: Check iptables FORWARD chain +search(instanceId="i-0abc123def456", query="FORWARD.*DROP|FORWARD.*REJECT|FORWARD.*policy") + +# Step 7: Check routing for pod CIDRs +search(instanceId="i-0abc123def456", query="blackhole|no route|missing.*route|pod.*CIDR") + +# Step 8: Check security groups (cross-node) +search(instanceId="i-0abc123def456", query="security group|sg-|SecurityGroupIds") + +# Step 9: Capture on source pod veth (while reproducing the issue) +tcpdump_capture(instanceId="i-0abc123def456", interface="", duration=30, filter="host ") +tcpdump_analyze(instanceId="i-0abc123def456", captureId="") + +# Step 10: Capture on source node eth0 +tcpdump_capture(instanceId="i-0abc123def456", interface="eth0", duration=30, filter="host ") +tcpdump_analyze(instanceId="i-0abc123def456", captureId="") + +# Step 11: If cross-node — capture on DESTINATION node eth0 and veth +collect(instanceId="i-0dest789ghi012") +tcpdump_capture(instanceId="i-0dest789ghi012", interface="eth0", duration=30, filter="host ") +tcpdump_analyze(instanceId="i-0dest789ghi012", captureId="") +tcpdump_capture(instanceId="i-0dest789ghi012", interface="", duration=30, filter="host ") +tcpdump_analyze(instanceId="i-0dest789ghi012", captureId="") + +# Step 12: Correlate timeline +correlate(instanceId="i-0abc123def456", pivotEvent="connection refused|timed out|DENY", timeWindow=300) + +# Step 13: Generate summary +summarize(instanceId="i-0abc123def456", finding_ids=["F-001","F-002","F-003","F-004"]) +``` + +## Output Format + +```yaml +root_cause: "" +traffic_path: "" +packet_trace: + src_veth: "" + src_eth0: "" + dst_eth0: "" + dst_veth: "" + drop_point: "" +evidence: + - type: network_diagnostics + content: "" + - type: search + content: "" + - type: tcpdump_analyze + content: "" + - type: correlate + content: "" +severity: HIGH +mitigation: + immediate: "Operator: " + long_term: "Enable NetworkPolicy flow logs, enable VPC Flow Logs, monitor aws-node health" +cross_reference: + - "D1 if pods stuck in ContainerCreating (IP allocation)" + - "D3 if conntrack exhaustion contributing to drops" + - "D5 if DNS resolution failing (not direct pod IP)" + - "D7 if general network performance degradation" + - "D8 if Service/ClusterIP connectivity (not direct pod-to-pod)" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/E1-ebs-csi-attach-mount-timeout.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/E1-ebs-csi-attach-mount-timeout.md new file mode 100644 index 0000000..5b45156 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/E1-ebs-csi-attach-mount-timeout.md @@ -0,0 +1,120 @@ +--- +title: "E1 — EBS CSI Attach/Mount Timeout" +description: "Diagnose pods stuck in ContainerCreating due to EBS volume attach or mount failures" +status: active +severity: HIGH +triggers: + - "AttachVolume.Attach failed" + - "Unable to attach or mount volumes: timed out" + - "FailedMount" + - "Multi-Attach error" +owner: devops-agent +objective: "Identify the EBS attach/mount failure reason and restore volume access" +context: "EBS volumes must be attached to the correct instance in the correct AZ before pods can mount them. Failures occur due to AZ mismatch, stale attachments, instance volume limits, or CSI driver issues." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check pod and node state before any log collection: + - List pods in the affected namespace: `kubectl get pods -n -o wide` (via EKS MCP `list_k8s_resources`) + - Check pod status — pods stuck in ContainerCreating with volume mount errors confirms this SOP + - Check pod events: `kubectl describe pod ` (via EKS MCP `get_k8s_events`) for volume attach/mount timeout details + - Check PersistentVolumeClaim status: `kubectl get pvc -n ` (via EKS MCP `list_k8s_resources` kind=PersistentVolumeClaim) — Pending PVC = volume not provisioned + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — verify the node is Ready +- Use `collect` tool with instanceId to gather logs from the affected node +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId to get pre-indexed findings — look for volume attach/mount errors +- Use `storage_diagnostics` tool with instanceId to get storage/volume status from collected logs + +SHOULD: +- Use `search` tool with instanceId and query=`AttachVolume.*failed|timed out.*volumes|FailedMount|Multi-Attach` to find volume failure evidence +- Use `search` tool with query=`ebs-csi|csi.*controller|csi.*error` to check EBS CSI driver logs + +MAY: +- Use `search` tool with query=`lsblk|block device` to check attached block devices +- Use `cluster_health` tool with clusterName to check if volume issues are cluster-wide + +## Phase 2 — Enrich + +MUST: +- Use `search` tool with query=`timed out.*different.*AZ|availability zone` — AZ mismatch between volume and node +- Use `search` tool with query=`403|not authorized` in CSI logs — IAM permissions issue +- Review `storage_diagnostics` for device count — attachment limit reached +- Use `search` tool with query=`Multi-Attach` — volume still attached to old node + +SHOULD: +- Use `correlate` tool with instanceId and pivotEvent=`AttachVolume` to build timeline of attach failures +- Use `search` tool with query=`StorageClass|volumeBindingMode|WaitForFirstConsumer` to check storage class config + +MAY: +- Use `compare_nodes` tool to check if volume issues are node-specific + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from volume-related findings +- State root cause: specific attach/mount failure with evidence +- Recommend targeted fix (operator action) +- Confirm pod should transition to Running after fix + +SHOULD: +- Include volume ID, AZ, and attachment state from findings +- Warn about data corruption risk for force detach + +MAY: +- Recommend WaitForFirstConsumer binding mode +- Recommend Gen7+ instances for higher volume limits + +## Guardrails + +escalation_conditions: + - "Volume stuck in attaching state for >10 minutes" + - "Force detach needed on actively-written volume (data corruption risk)" + - "CSI driver pods not running on any node" + +safety_ratings: + - "Log collection (collect), search, errors, storage_diagnostics: GREEN (read-only)" + - "Force detach volume: RED — operator action, data corruption risk, requires approval" + +## Common Issues + +- symptoms: "search returns AttachVolume timed out, findings show AZ mismatch" + diagnosis: "Volume in different AZ than target node, or stale attachment" + resolution: "Operator action: check volume AZ vs node AZ. If stale: force detach (with caution) or wait for GC." + +- symptoms: "search returns 403 not authorized in CSI controller findings" + diagnosis: "EBS CSI controller IAM role missing permissions" + resolution: "Operator action: add required EBS permissions to CSI controller IAM role" + +- symptoms: "search returns Multi-Attach error" + diagnosis: "Volume still attached to previous node" + resolution: "Operator action: wait for previous pod termination, or force detach if safe" + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Get storage diagnostics +storage_diagnostics(instanceId="i-0abc123def456") +# Step 3: Get volume-related findings +errors(instanceId="i-0abc123def456") +# Step 4: Search for attach failures +search(instanceId="i-0abc123def456", query="AttachVolume.*failed|Multi-Attach|FailedMount") +``` + +## Output Format + +```yaml +root_cause: "" +evidence: + - type: finding + content: "" + - type: storage_diagnostics + content: "" +severity: HIGH +mitigation: + immediate: "Operator: " + long_term: "Use WaitForFirstConsumer, Gen7+ instances" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/E2-efs-mount-failures.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/E2-efs-mount-failures.md new file mode 100644 index 0000000..bab4a6c --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/E2-efs-mount-failures.md @@ -0,0 +1,114 @@ +--- +title: "E2 — EFS Mount Failures" +description: "Diagnose EFS mount failures including access denied, timeouts, and CSI driver issues" +status: active +severity: HIGH +triggers: + - "access denied by server while mounting" + - "Connection timed out" + - "efs.csi.aws.com not found" + - "mount.nfs4: No such device" +owner: devops-agent +objective: "Identify the EFS mount failure reason and restore volume access" +context: "EFS mounts fail due to missing IAM mount options, security group blocking NFS (TCP 2049), missing mount targets in the node AZ, or EFS CSI driver not installed." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check pod and node state before any log collection: + - List pods in the affected namespace: `kubectl get pods -n -o wide` (via EKS MCP `list_k8s_resources`) + - Check pod status — pods stuck in ContainerCreating with mount errors confirms this SOP + - Check pod events: `kubectl describe pod ` (via EKS MCP `get_k8s_events`) for EFS/NFS mount failure details + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — verify the node is Ready +- Use `collect` tool with instanceId to gather logs from the affected node +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId to get pre-indexed findings — look for EFS/NFS mount errors +- Use `storage_diagnostics` tool with instanceId and sections=efs_csi to get EFS CSI driver status + +SHOULD: +- Use `search` tool with instanceId and query=`access denied.*mounting|Connection timed out.*nfs|efs.csi.aws.com not found|mount.nfs4` to find EFS mount failure evidence +- Use `network_diagnostics` tool with instanceId and sections=iptables to check for rules blocking NFS (TCP 2049) + +MAY: +- Use `search` tool with query=`efs-csi|efs.*driver|efs.*DaemonSet` to check EFS CSI driver status +- Use `cluster_health` tool with clusterName to check if EFS failures are cluster-wide + +## Phase 2 — Enrich + +MUST: +- Use `search` tool with query=`access denied` — EFS policy uses IAM conditions, need "iam" mount option in PV +- Use `search` tool with query=`not found.*registered CSI drivers` — EFS CSI DaemonSet not running on node +- Use `search` tool with query=`timed out.*nfs|Connection timed out` — SG blocking TCP 2049 or no mount target in AZ + +SHOULD: +- Use `correlate` tool with instanceId and pivotEvent=`mount` to build timeline of mount failures +- Use `search` tool with query=`mountOptions|iam.*tls` to check PV mount options + +MAY: +- Use `search` tool with query=`Bottlerocket|nfs.*module` to check for Bottlerocket NFS compatibility + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from EFS-related findings +- State root cause: specific mount failure with evidence +- Recommend targeted fix (operator action) +- Confirm mount should succeed after fix + +SHOULD: +- Include the specific error from findings + +MAY: +- Recommend EFS CSI driver over kernel NFS for Bottlerocket + +## Guardrails + +escalation_conditions: + - "EFS file system unreachable from all nodes" + - "EFS CSI driver cannot be installed (node compatibility issue)" + - "Security group changes require approval" + +safety_ratings: + - "Log collection (collect), search, errors, storage_diagnostics, network_diagnostics: GREEN (read-only)" + - "Modify PV spec, security groups: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "search returns access denied by server while mounting 127.0.0.1" + diagnosis: "EFS file system policy requires IAM auth but PV missing iam mount option" + resolution: "Operator action: add mountOptions: [iam, tls] to PV spec" + +- symptoms: "search returns efs.csi.aws.com not found in registered CSI drivers" + diagnosis: "EFS CSI DaemonSet not running on the node" + resolution: "Operator action: install or restart EFS CSI driver" + +- symptoms: "search returns Connection timed out for NFS" + diagnosis: "Security group blocking TCP 2049 or no mount target in node AZ" + resolution: "Operator action: update SG to allow TCP 2049, verify mount target exists in AZ" + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Get storage diagnostics +storage_diagnostics(instanceId="i-0abc123def456", sections="efs_csi") +# Step 3: Get EFS-related findings +errors(instanceId="i-0abc123def456") +# Step 4: Search for mount failures +search(instanceId="i-0abc123def456", query="access denied.*mounting|efs.csi.aws.com not found|timed out.*nfs") +``` + +## Output Format + +```yaml +root_cause: "" +evidence: + - type: finding + content: "" +severity: HIGH +mitigation: + immediate: "Operator: " + long_term: "Use EFS CSI driver with IAM mount options" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/F1-insufficient-cpu-memory.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/F1-insufficient-cpu-memory.md new file mode 100644 index 0000000..a316d76 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/F1-insufficient-cpu-memory.md @@ -0,0 +1,155 @@ +--- +title: "F1 — Insufficient CPU/Memory for Scheduling" +description: "Diagnose pods stuck in Pending due to insufficient cluster capacity" +status: active +severity: HIGH +triggers: + - "Insufficient cpu" + - "Insufficient memory" + - "0/\\d+ nodes are available" + - "pod didn't trigger scale-up" +owner: devops-agent +objective: "Identify the scheduling bottleneck and restore pod scheduling" +context: "Pods remain Pending when no node has enough allocatable CPU or memory to satisfy resource requests. Causes include cluster capacity exhaustion, over-provisioned requests, restrictive affinity rules, or autoscaler failures." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check pod and node state before any log collection: + - List pods in the affected namespace: `kubectl get pods -n -o wide` (via EKS MCP `list_k8s_resources`) + - Check pod status — pods in Pending state with "Insufficient cpu" or "Insufficient memory" events confirms this SOP + - Check pod events: `kubectl describe pod ` (via EKS MCP `get_k8s_events`) for scheduling failure details (FailedScheduling) + - Check node conditions and allocatable resources: `kubectl describe node ` (via EKS MCP `read_k8s_resource`) — compare Allocatable vs Allocated resources + - Check all nodes: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — verify which nodes are Ready and available for scheduling +- Use `collect` tool with instanceId of a node where pods cannot schedule to gather node-level logs +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId and severity=high to get pre-indexed scheduling-related findings +- Use `search` tool with instanceId and query=`Insufficient cpu|Insufficient memory|FailedScheduling|0/.*nodes are available` to find scheduling evidence in kubelet logs + +SHOULD: +- Use `cluster_health` tool with clusterName to get cluster-wide overview including node capacity status +- Use `search` tool with query=`cluster-autoscaler|scale-up|pod didn't trigger` to find autoscaler-related messages + +MAY: +- Use `compare_nodes` tool with instanceIds of multiple nodes to identify which nodes are at capacity vs which have headroom +- Use `search` tool with query=`karpenter|provisioner|machine` to check Karpenter activity if used + +## Phase 2 — Enrich + +MUST: +- Use `correlate` tool with instanceId and pivotEvent=`Insufficient` to build timeline of scheduling failures +- Review findings from `errors` tool — if all nodes show allocatable CPU/memory below pending pod requests: cluster capacity exhaustion +- Use `search` tool with query=`node affinity|nodeSelector|didn't match` to check if affinity rules are eliminating nodes +- If autoscaler messages found: use `search` tool with query=`pod didn't trigger scale-up|max size reached|launch.*fail` to determine autoscaler failure reason + +SHOULD: +- Use `compare_nodes` tool to diff resource utilization across nodes — identify if specific node groups are full while others have capacity +- Use `search` tool with query=`requests.*cpu|requests.*memory|resource quota` to check if resource requests are over-provisioned + +MAY: +- Use `search` tool with query=`PriorityClass|preemption` to check if priority-based scheduling is configured +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="Deployment" to check for recent Deployment scale-up events (replicas increased) that may have exhausted cluster capacity +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="ResourceQuota" to check for ResourceQuota create/update events that may be limiting scheduling + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from scheduling-related findings to generate incident summary +- State root cause: specific scheduling bottleneck (capacity exhaustion, affinity mismatch, autoscaler failure, or over-provisioned requests) with evidence +- Recommend targeted fix based on root cause +- Operator action — not available via MCP tools: scale node group, adjust affinity rules, increase ASG max size, or right-size resource requests + +SHOULD: +- Include node capacity vs request numbers from findings +- Include autoscaler status from search results + +MAY: +- Recommend VPA for right-sizing resource requests +- Recommend Karpenter for flexible instance selection + +## Guardrails + +escalation_conditions: + - "ASG max size reached and cannot be increased" + - "Instance launch failures (capacity unavailable in AZ)" + - "Critical workloads stuck in Pending — check via cluster_health" + +safety_ratings: + - "Log collection (collect), search, errors, correlate, cluster_health, compare_nodes: GREEN (read-only)" + - "Scale node group, adjust ASG max: YELLOW — operator action, not available via MCP tools" + - "Modify resource requests/limits: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "errors tool returns findings with Insufficient cpu/memory across all nodes" + diagnosis: "Cluster capacity exhaustion. Use cluster_health to confirm all node groups at capacity." + resolution: "Operator action: scale node group or add Karpenter provisioner for auto-scaling" + +- symptoms: "search for node affinity returns didn't match Pod's node affinity/selector" + diagnosis: "Scheduling constraints too restrictive — affinity rules eliminate all available nodes." + resolution: "Operator action: review and relax nodeSelector/affinity rules in pod spec" + +- symptoms: "search for autoscaler returns pod didn't trigger scale-up" + diagnosis: "ASG max size reached or launch template failures. Use search with query=max size reached to confirm." + resolution: "Operator action: increase ASG max size, check launch template and instance availability" + +- symptoms: "compare_nodes shows resource requests 3x+ higher than actual usage" + diagnosis: "Over-provisioned resource requests wasting capacity." + resolution: "Operator action: right-size using VPA recommendations or manual adjustment" + +- symptoms: "search for node affinity returns 'node(s) had untolerated taint'" + diagnosis: "Taint/toleration mismatch — nodes have taints that the pod does not tolerate. Use search with query=taint|toleration to identify specific taints." + resolution: "Operator action: add matching tolerations to pod spec, or remove unnecessary taints from nodes" + +- symptoms: "search returns 'Insufficient vpc.amazonaws.com/pod-eni' in scheduling events" + diagnosis: "Pod security groups require trunk ENI interface, which is only available on Nitro-based instances. Non-Nitro instances cannot allocate pod-eni resources." + resolution: "Operator action: ensure node group uses Nitro-based instance types (m5, c5, r5, etc.). Enable trunk interface via ENABLE_POD_ENI=true on aws-node DaemonSet." + +- symptoms: "search returns 'didn't have free ports for requested pod ports' in scheduling events" + diagnosis: "Pod uses hostNetwork:true and the requested hostPort is already in use on all available nodes." + resolution: "Operator action: remove hostNetwork:true if not required, or use different hostPort values. Consider using a Service with NodePort instead." + +- symptoms: "search returns 'node(s) didn't match Pod's node affinity/selector' but nodes appear to have capacity" + diagnosis: "Pod uses requiredDuringSchedulingIgnoredDuringExecution affinity that eliminates all available nodes. Consider using preferredDuringScheduling for soft constraints." + resolution: "Operator action: change requiredDuringScheduling to preferredDuringScheduling for non-critical placement preferences, or add nodes matching the required labels" + +- symptoms: "search returns volume zone mismatch or 'no available volume zone' in scheduling events" + diagnosis: "PVC is bound to an EBS volume in a different AZ than available nodes. Pod cannot schedule because the volume cannot be attached cross-AZ." + resolution: "Operator action: create a new PVC in the correct AZ, or add nodes in the AZ where the volume exists. Use volumeBindingMode: WaitForFirstConsumer in StorageClass to prevent this." + +## Examples + +``` +# Step 1: Collect logs from affected node +collect(instanceId="i-0abc123def456") +# Step 2: Poll status +status(executionId="") +# Step 3: Get scheduling findings +errors(instanceId="i-0abc123def456", severity="high") +# Step 4: Search for scheduling evidence +search(instanceId="i-0abc123def456", query="Insufficient cpu|Insufficient memory|FailedScheduling") +# Step 5: Check cluster-wide health +cluster_health(clusterName="my-cluster") +# Step 6: Compare nodes for capacity differences +compare_nodes(instanceIds=["i-0abc123def456","i-0xyz789ghi012"]) +# Step 7: Correlate timeline +correlate(instanceId="i-0abc123def456", pivotEvent="Insufficient", timeWindow=120) +# Step 8: Generate summary +summarize(instanceId="i-0abc123def456", finding_ids=["F-001","F-002"]) +``` + +## Output Format + +```yaml +root_cause: "" +evidence: + - type: scheduling_finding + content: "" + - type: cluster_health + content: "" +severity: HIGH +mitigation: + immediate: "Operator: " + long_term: "Deploy VPA, configure Karpenter, implement PriorityClasses" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/F2-max-pods-limit.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/F2-max-pods-limit.md new file mode 100644 index 0000000..d9c7778 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/F2-max-pods-limit.md @@ -0,0 +1,133 @@ +--- +title: "F2 — Max Pods Limit Reached" +description: "Diagnose pods stuck in Pending due to instance max-pods limit" +status: active +severity: HIGH +triggers: + - "Too many pods" + - "max pods.*reached" + - "cannot allocate.*pod" +owner: devops-agent +objective: "Identify max-pods bottleneck and increase pod density or scale nodes" +context: "Each EC2 instance type has a max-pods limit based on ENI count and IPs-per-ENI. When reached, no new pods can schedule on that node. Prefix delegation can increase this limit 16x." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check pod and node state before any log collection: + - List pods in the affected namespace: `kubectl get pods -n -o wide` (via EKS MCP `list_k8s_resources`) + - Check pod status — pods in Pending state with "Too many pods" event confirms this SOP + - Check pod events: `kubectl describe pod ` (via EKS MCP `get_k8s_events`) for "Too many pods" or max-pods scheduling failures + - Check node details: `kubectl describe node ` (via EKS MCP `read_k8s_resource`) — compare current pod count vs allocatable pods + - Check all nodes: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — check if all nodes are at max pods +- Use `collect` tool with instanceId of the affected node to gather node-level logs +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId and severity=high to get pre-indexed findings related to pod limits +- Use `search` tool with instanceId and query=`Too many pods|max pods|cannot allocate pod|maxPods` to find max-pods evidence in kubelet logs + +SHOULD: +- Use `search` tool with query=`ENABLE_PREFIX_DELEGATION|prefix delegation|warm-prefix` to check if prefix delegation is enabled +- Use `network_diagnostics` tool with instanceId and sections=cni,eni,ipamd to check ENI allocation and IP capacity + +MAY: +- Use `cluster_health` tool with clusterName to check if multiple nodes are hitting pod limits +- Use `compare_nodes` tool with instanceIds to compare pod density across nodes + +## Phase 2 — Enrich + +MUST: +- Review findings from `errors` tool — if findings show max-pods reached: ENI-based limit hit +- Use `search` tool with query=`kubelet-config|maxPods|--max-pods` to check if max-pods is explicitly overridden below ENI limit +- Use `network_diagnostics` tool to confirm ENI count and IPs-per-ENI for the instance type +- Use `search` tool with query=`DaemonSet|daemonset` in kubelet logs to estimate DaemonSet pod overhead + +SHOULD: +- Use `search` tool with query=`prefix delegation|ENABLE_PREFIX_DELEGATION=true` to determine if prefix delegation is already enabled +- Calculate pod density from findings: running pods / max pods + +MAY: +- Use `search` tool with query=`karpenter.*maxPods` to check Karpenter override settings + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from pod-limit findings to generate incident summary +- State root cause: max-pods limit with current count vs limit from findings +- Recommend fix based on root cause +- Operator action — not available via MCP tools: enable prefix delegation, update kubelet config, or change instance type + +SHOULD: +- Include pod count, max-pods value, and instance type from findings +- Include ENI/IP details from network_diagnostics + +MAY: +- Recommend Karpenter with maxPods override for flexible density + +## Guardrails + +escalation_conditions: + - "Prefix delegation cannot be enabled (CNI version too old)" + - "All nodes in cluster at max-pods — check via cluster_health" + - "DaemonSet count cannot be reduced" + +safety_ratings: + - "Log collection (collect), search, errors, network_diagnostics, cluster_health: GREEN (read-only)" + - "Enable prefix delegation, update kubelet config: YELLOW — operator action, not available via MCP tools" + - "Change instance type: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "errors tool returns findings with Too many pods on specific nodes" + diagnosis: "Instance type ENI/IP limit reached. Use network_diagnostics to confirm ENI count." + resolution: "Operator action: enable prefix delegation (ENABLE_PREFIX_DELEGATION=true on aws-node) for 16x density" + +- symptoms: "search for maxPods shows value set below ENI limit" + diagnosis: "Manual --max-pods override too restrictive in kubelet config." + resolution: "Operator action: update kubelet config or launch template user data to remove override" + +- symptoms: "search for DaemonSet shows >30% of pod slots consumed by DaemonSets" + diagnosis: "DaemonSet overhead too high, reducing available pod slots for workloads." + resolution: "Operator action: consolidate DaemonSets or increase max-pods via prefix delegation" + +- symptoms: "search returns 'Too many pods' and network_diagnostics shows all ENI slots consumed" + diagnosis: "Instance ENI/IP limit reached. Each instance type has a fixed max ENI count and IPs-per-ENI. Use aws ec2 describe-instance-types --instance-types --query 'InstanceTypes[].NetworkInfo.{MaxENI:MaximumNetworkInterfaces,IPv4PerENI:Ipv4AddressesPerInterface}' to check limits." + resolution: "Operator action: enable prefix delegation (ENABLE_PREFIX_DELEGATION=true) for up to 110 pods on most Nitro instances, or upgrade to instance type with more ENIs" + +- symptoms: "errors tool returns 'Too many pods' but max-pods value appears lower than expected for instance type" + diagnosis: "Max-pods may be calculated incorrectly or overridden. Default formula: (MaxENI * (IPv4PerENI - 1)) + 2. With prefix delegation: (MaxENI * ((IPv4PerENI - 1) * 16)) + 2." + resolution: "Operator action: verify max-pods calculation matches instance type. Check if --max-pods is overridden in kubelet args or launch template user data." + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Poll status +status(executionId="") +# Step 3: Get pod limit findings +errors(instanceId="i-0abc123def456", severity="high") +# Step 4: Search for max-pods evidence +search(instanceId="i-0abc123def456", query="Too many pods|max pods|maxPods") +# Step 5: Check ENI/IP allocation +network_diagnostics(instanceId="i-0abc123def456", sections="cni,eni,ipamd") +# Step 6: Check prefix delegation status +search(instanceId="i-0abc123def456", query="ENABLE_PREFIX_DELEGATION|prefix delegation") +# Step 7: Generate summary +summarize(instanceId="i-0abc123def456", finding_ids=["F-001","F-002"]) +``` + +## Output Format + +```yaml +root_cause: "Max pods limit reached — / on " +evidence: + - type: pod_limit_finding + content: "" + - type: network_diagnostics + content: "" +severity: HIGH +mitigation: + immediate: "Operator: enable prefix delegation or scale to larger instance" + long_term: "Use Karpenter with maxPods override, right-size pods" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/F3-taints-tolerations-node-selectors.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/F3-taints-tolerations-node-selectors.md new file mode 100644 index 0000000..51fc21d --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/F3-taints-tolerations-node-selectors.md @@ -0,0 +1,125 @@ +--- +title: "F3 — Taints, Tolerations, and Node Selector Mismatches" +description: "Diagnose pods stuck in Pending due to taint/toleration or node selector mismatches" +status: active +severity: MEDIUM +triggers: + - "had untolerated taint" + - "didn't match Pod's node affinity" + - "didn't match Pod's node selector" +owner: devops-agent +objective: "Identify the scheduling constraint mismatch and restore pod scheduling" +context: "Pods remain Pending when taints, tolerations, nodeSelectors, or affinity rules prevent scheduling. Common causes include maintenance taints not removed, missing labels, or overly restrictive affinity rules." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check pod and node state before any log collection: + - List pods in the affected namespace: `kubectl get pods -n -o wide` (via EKS MCP `list_k8s_resources`) + - Check pod status — pods in Pending state with taint/toleration or nodeSelector mismatch confirms this SOP + - Check pod events: `kubectl describe pod ` (via EKS MCP `get_k8s_events`) for "didn't match Pod's node affinity/selector" or taint-related scheduling failures + - Check node taints and labels: `kubectl describe node ` (via EKS MCP `read_k8s_resource`) — look at Taints and Labels sections + - Check all nodes: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — verify which nodes are available +- Use `collect` tool with instanceId of a node where pods are expected to schedule to gather node-level logs +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId to get pre-indexed findings including taint/scheduling issues +- Use `search` tool with instanceId and query=`untolerated taint|didn't match.*node affinity|didn't match.*node selector|NoSchedule|NoExecute` to find scheduling constraint evidence + +SHOULD: +- Use `cluster_health` tool with clusterName to check overall node status and taint distribution +- Use `search` tool with query=`taint|toleration|nodeSelector|affinity` in kubelet logs to find scheduling-related configuration + +MAY: +- Use `compare_nodes` tool with instanceIds of tainted vs untainted nodes to diff configurations +- Use `search` tool with query=`maintenance|cordon|drain` to check if taints are from maintenance operations + +## Phase 2 — Enrich + +MUST: +- Review findings from `errors` tool — if findings show "untolerated taint": identify the taint key/value and whether it is intentional +- Use `search` tool with query=`NoSchedule|NoExecute|PreferNoSchedule` to identify all active taints on the node +- If findings show "didn't match node selector": use `search` tool with query=`nodeSelector|label` to identify the missing label +- If all nodes appear tainted: use `cluster_health` to confirm no schedulable nodes exist + +SHOULD: +- Use `search` tool with query=`gpu|dedicated|special-purpose` to determine if taints are for GPU/special-purpose nodes blocking general workloads +- Use `correlate` tool with instanceId and pivotEvent=`taint` to check when taints were applied + +MAY: +- Use `compare_nodes` tool to compare taint configurations across node groups +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="taint" to check for recent taint mutations on nodes (e.g., maintenance taints applied but not removed) +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="label" to check for recent label changes on nodes that may have broken nodeSelector matching + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from scheduling-related findings to generate incident summary +- State root cause: specific scheduling constraint mismatch with taint key/value or missing label +- Recommend targeted fix based on root cause +- Operator action — not available via MCP tools: add toleration to pod spec, remove taint from node, or add label to node + +SHOULD: +- Include the specific taint or label causing the mismatch from findings + +MAY: +- Recommend OPA/Gatekeeper policies for scheduling constraint governance + +## Guardrails + +escalation_conditions: + - "All nodes tainted with NoSchedule and no untainted nodes available — check via cluster_health" + - "Taint removal requires approval (production node group)" + - "Affinity rules set by platform team and cannot be changed" + +safety_ratings: + - "Log collection (collect), search, errors, cluster_health, compare_nodes: GREEN (read-only)" + - "Add toleration to pod spec: YELLOW — operator action, not available via MCP tools" + - "Remove taint from node: YELLOW — operator action, not available via MCP tools" + - "Add label to node: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "errors tool returns findings with untolerated taint {key}={value}:NoSchedule" + diagnosis: "Node tainted but pod lacks matching toleration. Use search with query=NoSchedule to identify the taint." + resolution: "Operator action: add toleration to pod spec OR remove taint from node" + +- symptoms: "search returns didn't match Pod's node selector" + diagnosis: "Pod nodeSelector references label not present on any node. Use cluster_health to check node labels." + resolution: "Operator action: add label to nodes or update pod nodeSelector" + +- symptoms: "cluster_health shows all nodes tainted with NoSchedule" + diagnosis: "No schedulable nodes exist — possibly maintenance taints not removed." + resolution: "Operator action: remove maintenance taints from recovered nodes" + +## Examples + +``` +# Step 1: Collect logs from affected node +collect(instanceId="i-0abc123def456") +# Step 2: Poll status +status(executionId="") +# Step 3: Get scheduling findings +errors(instanceId="i-0abc123def456") +# Step 4: Search for taint/selector evidence +search(instanceId="i-0abc123def456", query="untolerated taint|didn't match.*node affinity|NoSchedule") +# Step 5: Check cluster-wide taint status +cluster_health(clusterName="my-cluster") +# Step 6: Generate summary +summarize(instanceId="i-0abc123def456", finding_ids=["F-001","F-002"]) +``` + +## Output Format + +```yaml +root_cause: "" +evidence: + - type: scheduling_finding + content: "" + - type: taint_search + content: "" +severity: MEDIUM +mitigation: + immediate: "Operator: add toleration/label or remove taint" + long_term: "Document taint/toleration strategy, use admission webhooks" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/G1-disk-pressure-eviction-storms.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/G1-disk-pressure-eviction-storms.md new file mode 100644 index 0000000..0abc61d --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/G1-disk-pressure-eviction-storms.md @@ -0,0 +1,135 @@ +--- +title: "G1 — DiskPressure / Eviction Storms" +description: "Diagnose mass pod evictions caused by disk pressure on EKS nodes" +status: active +severity: HIGH +triggers: + - "eviction manager: attempting to reclaim.*ephemeral-storage" + - "DiskPressure" + - "garbage collect.*freed 0 bytes" + - "imagefs.available" +owner: devops-agent +objective: "Stop the eviction storm, free disk space, and prevent recurrence" +context: "When node disk usage exceeds kubelet eviction thresholds, the eviction manager aggressively kills pods to reclaim space. This can cascade into eviction storms where many pods are killed rapidly." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check node and pod state before any log collection: + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — look for DiskPressure condition + - Check node details: `kubectl describe node ` (via EKS MCP `read_k8s_resource`) — check Conditions for DiskPressure=True and recent events for eviction activity + - List pods on the affected node: `kubectl get pods --all-namespaces --field-selector spec.nodeName=` (via EKS MCP `list_k8s_resources` with field_selector) — check for Evicted pods or pods in Terminating state +- Use `collect` tool with instanceId of the affected node to gather node-level logs +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId and severity=high to get pre-indexed disk pressure findings +- Use `search` tool with instanceId and query=`DiskPressure|eviction manager|ephemeral-storage|imagefs.available` to find eviction evidence in kubelet logs +- Use `storage_diagnostics` tool with instanceId and sections=kubelet,instance to check disk utilization and inode usage + +SHOULD: +- Use `search` tool with query=`garbage collect|image.*prune|freed.*bytes` to check if image garbage collection is running +- Use `search` tool with query=`emptyDir|ephemeral` to identify pods using local storage + +MAY: +- Use `cluster_health` tool with clusterName to check if multiple nodes have DiskPressure +- Use `compare_nodes` tool with instanceIds to compare disk usage across nodes + +## Phase 2 — Enrich + +MUST: +- Review findings from `storage_diagnostics` — if root disk >85%: garbage collection not keeping up +- Use `search` tool with query=`eviction.*pod|evicted|Evicted` to identify which pods were evicted and how rapidly +- Use `correlate` tool with instanceId and pivotEvent=`DiskPressure` to build timeline of eviction storm +- If storage_diagnostics shows inode exhaustion: cross-reference with C3 (overlayfs inode exhaustion) + +SHOULD: +- Use `search` tool with query=`containerd|image.*pull|layer` to check if large image pulls triggered the disk pressure +- Use `storage_diagnostics` to check kubelet eviction threshold configuration + +MAY: +- Use `search` tool with query=`imageGCHighThreshold|imageGCLowThreshold|imageGCHighThresholdPercent|imageGCLowThresholdPercent` to check image GC configuration — default thresholds are imageGCHighThreshold=85% (start GC) and imageGCLowThreshold=80% (stop GC). Lower to 70%/60% for more aggressive cleanup. +- Use `search` tool with query=`containerd.*log|containerd.runc.log|log_file_max|log_file_max_size` to check container runtime log rotation — misconfigured containerd log rotation can fill disk independently of kubelet log management + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from disk pressure findings to generate incident summary +- State root cause: disk pressure with utilization evidence from storage_diagnostics +- List blast radius: number of evicted pods from search results +- Operator action — not available via MCP tools: prune images, clean emptyDir volumes, delete evicted pods, increase EBS root volume + +SHOULD: +- Include disk utilization percentages from storage_diagnostics +- Include top disk consumers identified from findings + +MAY: +- Recommend ephemeral-storage limits on all pods +- Recommend imageGCHighThresholdPercent tuning + +## Guardrails + +escalation_conditions: + - "Disk at 100% and cleanup cannot free space — check via storage_diagnostics" + - "System pods being evicted (kubelet, containerd)" + - "Multiple nodes in eviction storm simultaneously — check via cluster_health" + +safety_ratings: + - "Log collection (collect), search, errors, storage_diagnostics, correlate: GREEN (read-only)" + - "Prune images, clean emptyDir: YELLOW — operator action, not available via MCP tools" + - "Increase EBS root volume: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "storage_diagnostics shows root disk >85% used, errors tool returns DiskPressure findings" + diagnosis: "Disk full from images, logs, or emptyDir volumes. Use search with query=containerd to check image storage." + resolution: "Operator action: run crictl rmi --prune, delete evicted pods, identify large emptyDir consumers" + +- symptoms: "search for eviction returns rapid eviction of many pods within minutes" + diagnosis: "Large deployment caused many image pulls filling disk. Use correlate to confirm timeline." + resolution: "Operator action: pre-pull images, increase root volume, set ephemeral-storage limits" + +- symptoms: "storage_diagnostics shows inode exhaustion (inodes >95%)" + diagnosis: "Too many small files — see C3 overlayfs inode exhaustion SOP." + resolution: "Operator action: clean up container layers, increase inode count on volume" + +- symptoms: "search returns imageGCHighThreshold or image GC not reclaiming space" + diagnosis: "Image garbage collection thresholds may be misconfigured. Default: imageGCHighThreshold=85% (start GC), imageGCLowThreshold=80% (stop GC). If thresholds are too high, GC starts too late." + resolution: "Operator action: lower imageGCHighThresholdPercent to 70 and imageGCLowThresholdPercent to 60 in kubelet config for more aggressive image cleanup." + +- symptoms: "search returns containerd.runc.log or container runtime logs consuming disk" + diagnosis: "Container runtime log rotation is misconfigured. containerd writes to /var/log/pods/ and /var/log/containers/. The containerd config (log_file_max, log_file_max_size) controls runtime-level log rotation separately from kubelet's containerLogMaxSize." + resolution: "Operator action: configure containerd log rotation in /etc/containerd/config.toml — set log_file_max (number of rotated files) and log_file_max_size (max size per file, e.g., 10MB). Also verify kubelet containerLogMaxSize and containerLogMaxFiles settings. Restart containerd after config changes." + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Poll status +status(executionId="") +# Step 3: Get disk pressure findings +errors(instanceId="i-0abc123def456", severity="high") +# Step 4: Check disk and storage health +storage_diagnostics(instanceId="i-0abc123def456", sections="kubelet,instance") +# Step 5: Search for eviction evidence +search(instanceId="i-0abc123def456", query="DiskPressure|eviction manager|ephemeral-storage") +# Step 6: Correlate eviction timeline +correlate(instanceId="i-0abc123def456", pivotEvent="DiskPressure", timeWindow=120) +# Step 7: Generate summary +summarize(instanceId="i-0abc123def456", finding_ids=["F-001","F-002","F-003"]) +``` + +## Output Format + +```yaml +root_cause: "DiskPressure — " +evidence: + - type: storage_diagnostics + content: "" + - type: eviction_finding + content: "" +severity: HIGH +mitigation: + immediate: "Operator: prune images, clean emptyDir, delete evicted pods" + long_term: "Increase EBS root volume, set ephemeral-storage limits, tune image GC" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/G2-oomkill-memory-pressure.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/G2-oomkill-memory-pressure.md new file mode 100644 index 0000000..23e8173 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/G2-oomkill-memory-pressure.md @@ -0,0 +1,128 @@ +--- +title: "G2 — OOMKill / MemoryPressure" +description: "Diagnose containers killed by OOM and nodes with MemoryPressure condition" +status: active +severity: CRITICAL +triggers: + - "oom-kill:" + - "Out of memory: Killed process" + - "Memory cgroup out of memory" + - "OOMKilled" + - "memory.available.*eviction" +owner: devops-agent +objective: "Identify OOM kill target and cause, then right-size memory limits" +context: "OOM kills occur at two levels: container-level (cgroup limit exceeded) and node-level (system memory exhausted). Container OOM kills restart the specific container. Node-level OOM kills can affect kubelet and system stability." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check node and pod state before any log collection: + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — look for MemoryPressure condition + - Check node details: `kubectl describe node ` (via EKS MCP `read_k8s_resource`) — check Conditions for MemoryPressure=True + - List pods on the affected node: `kubectl get pods --all-namespaces --field-selector spec.nodeName=` (via EKS MCP `list_k8s_resources` with field_selector) — check for OOMKilled pods (look at RESTARTS count and last state), CrashLoopBackOff, or Evicted pods + - Check pod details for OOMKilled: `kubectl describe pod ` (via EKS MCP `get_k8s_events`) — look for "OOMKilled" in last termination reason +- Use `collect` tool with instanceId of the affected node to gather node-level logs +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId and severity=critical to get pre-indexed OOM findings +- Use `search` tool with instanceId and query=`oom-kill|OOMKilled|Out of memory|Memory cgroup out of memory` to find OOM evidence in dmesg and system logs + +SHOULD: +- Use `search` tool with query=`MemoryPressure|memory.available|memory pressure` to find memory pressure signals +- Use `search` tool with query=`MemAvailable|MemFree|MemTotal` to check memory utilization at time of kill + +MAY: +- Use `cluster_health` tool with clusterName to check if multiple nodes have MemoryPressure +- Use `compare_nodes` tool with instanceIds of affected + healthy node to diff memory findings + +## Phase 2 — Enrich + +MUST: +- Use `correlate` tool with instanceId and pivotEvent=`oom-kill` to build timeline around the OOM event +- Review findings from `errors` tool — if OOM target is a container process: container exceeded its cgroup memory limit +- If OOM target is kubelet or system process: node-level memory exhaustion — cross-reference with A1 (kubelet OOM) +- Use `search` tool with query=`Killed process.*pid|oom_score_adj` to identify the killed process name and PID + +SHOULD: +- Use `search` tool with query=`system-reserved|kube-reserved` to check if memory reservation is configured +- Use `errors` tool with severity=all to check for recurring OOM events (multiple findings) + +MAY: +- Use `compare_nodes` tool to compare memory-related findings between affected and healthy nodes + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from OOM-related findings to generate incident summary +- State root cause: container OOM or node-level OOM with process name and PID from findings +- Recommend memory limit adjustment based on root cause +- Operator action — not available via MCP tools: increase container memory limits, set system-reserved, restart affected pods + +SHOULD: +- Include dmesg OOM kill line from search results +- Include memory utilization at time of kill from findings + +MAY: +- Recommend VPA for automatic right-sizing +- Recommend system-reserved memory settings for node-level protection + +## Guardrails + +escalation_conditions: + - "Kubelet process OOM-killed — escalate to A1 SOP" + - "OOM kills recurring despite memory limit increase" + - "Node-level memory exhaustion affecting system stability — check via cluster_health" + +safety_ratings: + - "Log collection (collect), search, errors, correlate, compare_nodes: GREEN (read-only)" + - "Increase container memory limits: YELLOW — operator action, not available via MCP tools" + - "Set system-reserved in kubelet config: YELLOW — operator action, not available via MCP tools" + - "Terminate and replace node: RED — operator action, requires approval" + +## Common Issues + +- symptoms: "errors tool returns findings with OOMKilled for container process" + diagnosis: "Container memory limit too low for workload. Use search with query=memory limit to check configured limits." + resolution: "Operator action: increase container memory limit in pod spec" + +- symptoms: "search for oom-kill returns system-level OOM targeting kubelet or containerd" + diagnosis: "Node memory exhausted, system-reserved not configured. Escalate to A1 SOP." + resolution: "Operator action: set system-reserved in kubelet config, consider larger instance type" + +- symptoms: "correlate shows recurring OOM kills every few minutes" + diagnosis: "Application memory leak — OOM kills recur after container restart." + resolution: "Operator action: investigate application memory leak, increase limits as temporary measure" + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Poll status +status(executionId="") +# Step 3: Get OOM findings +errors(instanceId="i-0abc123def456", severity="critical") +# Step 4: Search for OOM evidence +search(instanceId="i-0abc123def456", query="oom-kill|OOMKilled|Out of memory") +# Step 5: Correlate OOM timeline +correlate(instanceId="i-0abc123def456", pivotEvent="oom-kill", timeWindow=120) +# Step 6: Check memory reservation config +search(instanceId="i-0abc123def456", query="system-reserved|kube-reserved") +# Step 7: Generate summary +summarize(instanceId="i-0abc123def456", finding_ids=["F-001","F-002","F-003"]) +``` + +## Output Format + +```yaml +root_cause: " — process PID " +evidence: + - type: dmesg_finding + content: "" + - type: memory_search + content: "" +severity: CRITICAL +mitigation: + immediate: "Operator: increase memory limits for affected container" + long_term: "Set system-reserved, deploy VPA, right-size instances" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/G3-pid-pressure.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/G3-pid-pressure.md new file mode 100644 index 0000000..5c20994 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/G3-pid-pressure.md @@ -0,0 +1,132 @@ +--- +title: "G3 — PIDPressure" +description: "Diagnose PIDPressure condition caused by process/thread exhaustion" +status: active +severity: HIGH +triggers: + - "PIDPressure" + - "pid.available" + - "unable to create new.*thread" +owner: devops-agent +objective: "Identify the runaway process consuming PIDs and restore PID availability" +context: "PIDPressure occurs when the node runs out of process IDs. This prevents new containers from starting and can affect system stability. Common causes include thread-leaking applications or kernel.pid_max set too low." +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check node and pod state before any log collection: + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — look for PIDPressure condition + - Check node details: `kubectl describe node ` (via EKS MCP `read_k8s_resource`) — check Conditions for PIDPressure=True + - List pods on the affected node: `kubectl get pods --all-namespaces --field-selector spec.nodeName=` (via EKS MCP `list_k8s_resources` with field_selector) — check for pods in CrashLoopBackOff or Error state (PID exhaustion causes fork failures) +- Use `collect` tool with instanceId of the affected node to gather node-level logs +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId and severity=high to get pre-indexed PID pressure findings +- Use `search` tool with instanceId and query=`PIDPressure|pid.available|unable to create.*thread|cannot allocate memory` to find PID exhaustion evidence + +SHOULD: +- Use `search` tool with query=`kernel.pid_max|kernel.threads-max|pid_max` to check kernel PID limits +- Use `search` tool with query=`kubelet.*PIDPressure|node condition` to check if PIDPressure condition is set on the node + +MAY: +- Use `cluster_health` tool with clusterName to check if multiple nodes have PIDPressure +- Use `compare_nodes` tool with instanceIds to compare PID-related findings across nodes + +## Phase 2 — Enrich + +MUST: +- Use `correlate` tool with instanceId and pivotEvent=`PIDPressure` to build timeline of PID exhaustion +- Review findings from `errors` tool — identify the process with the most threads from findings +- Use `search` tool with query=`threads|nlwp|NLWP|clone|fork` to find thread creation patterns +- Compare current PID count against kernel.pid_max from search results + +SHOULD: +- Use `search` tool with query=`java|python|node|go` combined with `thread` to identify if a known application type is leaking threads +- Use `errors` tool with severity=all to check if PID pressure is recurring + +MAY: +- Use `search` tool with query=`pids.max|pids.current` to check container-level PID limits + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from PID-related findings to generate incident summary +- State root cause: PID exhaustion with offending process name and thread count from findings +- Recommend killing runaway process and setting PID limits +- Operator action — not available via MCP tools: kill runaway process, set container PID limits, increase kernel.pid_max + +SHOULD: +- Include process name and thread count from findings +- Include kernel.pid_max value from search results + +MAY: +- Recommend container PID limits via kubelet config (--pod-max-pids) + +## Guardrails + +escalation_conditions: + - "Runaway process is a critical system component (kubelet, containerd)" + - "PID exhaustion affecting kubelet or containerd — check via errors tool" + - "kernel.pid_max already at maximum safe value" + +safety_ratings: + - "Log collection (collect), search, errors, correlate, cluster_health: GREEN (read-only)" + - "Kill runaway process: YELLOW — operator action, not available via MCP tools" + - "Increase kernel.pid_max: YELLOW — operator action, not available via MCP tools" + - "Set container PID limits: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "errors tool returns PIDPressure findings, search shows thousands of threads from one process" + diagnosis: "Application thread leak consuming all PIDs. Use search with query=threads to identify the process." + resolution: "Operator action: kill runaway process, set PID limits on containers, fix application thread leak" + +- symptoms: "search for pid_max shows low value (e.g., 32768) with high workload density" + diagnosis: "kernel.pid_max set too low for workload density." + resolution: "Operator action: increase kernel.pid_max via sysctl. Set PID limits per container." + +- symptoms: "correlate shows PIDPressure recurring after process restart" + diagnosis: "Application has a persistent thread leak — killing the process is only a temporary fix." + resolution: "Operator action: fix application thread leak, set container PID limits as safety net" + +- symptoms: "search for pids.max shows container-level PID limit set but process still exhausts node PIDs" + diagnosis: "Container PID limit (pids.max in cgroup) may be set too high or not set at all. Without container-level limits, a single pod can exhaust all node PIDs." + resolution: "Operator action: set --pod-max-pids in kubelet config to limit PIDs per pod. Default is -1 (unlimited). Recommended: set to 1024-4096 depending on workload." + +- symptoms: "search returns 'fork: retry: Resource temporarily unavailable' or 'cannot allocate memory' alongside PIDPressure" + diagnosis: "PID exhaustion is preventing new process creation. This affects all containers on the node including system pods." + resolution: "Operator action: 1) Identify the runaway process (check /proc/*/status for highest Threads count). 2) Kill the process. 3) Set kernel.pid_max higher if needed (sysctl -w kernel.pid_max=65536). 4) Set --pod-max-pids in kubelet config." + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Poll status +status(executionId="") +# Step 3: Get PID pressure findings +errors(instanceId="i-0abc123def456", severity="high") +# Step 4: Search for PID exhaustion evidence +search(instanceId="i-0abc123def456", query="PIDPressure|pid.available|unable to create.*thread") +# Step 5: Check kernel PID limits +search(instanceId="i-0abc123def456", query="kernel.pid_max|kernel.threads-max") +# Step 6: Correlate PID pressure timeline +correlate(instanceId="i-0abc123def456", pivotEvent="PIDPressure", timeWindow=120) +# Step 7: Generate summary +summarize(instanceId="i-0abc123def456", finding_ids=["F-001","F-002"]) +``` + +## Output Format + +```yaml +root_cause: "PID exhaustion — using threads" +evidence: + - type: pid_finding + content: "" + - type: kernel_config + content: "kernel.pid_max= from search results" +severity: HIGH +mitigation: + immediate: "Operator: kill runaway process, increase kernel.pid_max" + long_term: "Set PID limits on containers, fix application thread leaks" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/H1-node-role-missing-permissions.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/H1-node-role-missing-permissions.md new file mode 100644 index 0000000..20f196d --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/H1-node-role-missing-permissions.md @@ -0,0 +1,145 @@ +--- +title: "H1 — Node Role Missing Permissions" +description: "Diagnose ECR pull failures, CNI failures, or registration failures caused by missing IAM policies on node role" +status: active +severity: HIGH +triggers: + - "AccessDenied" + - "is not authorized to perform" + - "UnauthorizedAccess" +owner: devops-agent +objective: "Identify missing IAM policies on the node role and restore permissions" +context: "EKS worker nodes require specific IAM policies: AmazonEKSWorkerNodePolicy, AmazonEKS_CNI_Policy, and AmazonEC2ContainerRegistryReadOnly. Missing any of these causes cascading failures in node registration, networking, or image pulls." +--- + +## Phase 1 — Triage + +FIRST — Check node and pod state before collecting logs: +- Use `list_k8s_resources` with clusterName, kind=Node, apiVersion=v1 to list all nodes — check if the affected node is Ready or NotReady, and whether it even appears in the cluster (missing = registration failure) +- Use `read_k8s_resource` with clusterName, kind=Node, apiVersion=v1, name= to get detailed node conditions — look for NetworkUnavailable (CNI permission failure) or NotReady (general permission issue) +- Use `list_k8s_resources` with clusterName, kind=Pod, apiVersion=v1, fieldSelector=spec.nodeName= to list pods on the node — check for ImagePullBackOff (ECR permission failure) or CrashLoopBackOff (CNI/credential failures) +- Use `get_k8s_events` with clusterName, kind=Node, name= to check for FailedCreatePodSandBox, ErrImagePull, or registration-related events + +MUST: +- **PREREQUISITE — Is an instance profile attached?** Before investigating which policy is missing, verify the node has an IAM role at all: + - Use `search` tool with instanceId and query=`No instance profile|instance profile.*not found|Unable to locate credentials|NoCredentialProviders` — if matches found, the EC2 instance has no instance profile attached. That is the root cause. Report "No instance profile attached to instance — node has no IAM role" immediately. + - Use `search` tool with instanceId and query=`instance profile|iam role|arn:aws:iam` — if NO matches found for any IAM role ARN, the instance may not have a profile. Report this before investigating specific policies. + - ONLY if instance profile is confirmed attached, proceed to policy investigation below. +- Use `collect` tool with instanceId of the affected node to gather node-level logs +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId and severity=high to get pre-indexed IAM/permission findings +- Use `search` tool with instanceId and query=`AccessDenied|is not authorized to perform|UnauthorizedAccess|Forbidden` to find permission errors across all log types + +SHOULD: +- Use `search` tool with query=`ecr.*AccessDenied|pull.*denied|authorization failed` to check for ECR-specific permission failures +- Use `search` tool with query=`aws-node|ipamd|eni.*error` in CNI logs to check for VPC CNI permission failures + +MAY: +- Use `cluster_health` tool with clusterName to check if multiple nodes have the same permission issue +- Use `search` tool with query=`cloud-init|bootstrap|userdata` to check node bootstrap for early permission failures + +## Phase 2 — Enrich + +MUST: +- Use `correlate` tool with instanceId and pivotEvent=`AccessDenied` to build timeline of permission failures +- Map the denied API action from findings to the required IAM policy: + - ECR pull failures → AmazonEC2ContainerRegistryReadOnly + - CNI failures → AmazonEKS_CNI_Policy + - Registration failures → AmazonEKSWorkerNodePolicy + - SSM failures → AmazonSSMManagedInstanceCore +- Use `search` tool with query=`instance profile|iam role|assume role` to check if instance profile is attached + +SHOULD: +- Use `search` tool with query=`sts.*AssumeRole|credential|token` to check if the node can obtain credentials at all +- Use `errors` tool with severity=all to check if permission errors are intermittent or persistent + +MAY: +- Use `search` tool with query=`SCP|service control policy|Organizations` to check for org-level restrictions +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="access" to check for recent access entry changes or aws-auth ConfigMap mutations that may have removed the node role mapping +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="certificatesigningrequests" to check for node CSR approval/denial events + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from permission-related findings to generate incident summary +- State root cause: specific missing IAM policy with the denied API action from findings +- Recommend adding the policy to the node role +- Operator action — not available via MCP tools: attach IAM policy to node role, verify instance profile + +SHOULD: +- Include the specific AccessDenied error message from findings + +MAY: +- Recommend managed node groups for automatic IAM configuration + +## Guardrails + +escalation_conditions: + - "IAM policy changes require approval process" + - "SCP blocking required permissions — found via search" + - "Instance profile not attached to instance" + +safety_ratings: + - "Log collection (collect), search, errors, correlate, cluster_health: GREEN (read-only)" + - "Attach IAM policy to node role: YELLOW — operator action, not available via MCP tools" + - "Modify SCP: RED — operator action, requires security team approval" + +## Common Issues + +- symptoms: "search returns No instance profile, Unable to locate credentials, or NoCredentialProviders" + diagnosis: "EC2 instance has no instance profile attached. The node has no IAM role at all, so all AWS API calls will fail." + resolution: "Operator action: attach an instance profile with the required EKS node policies (AmazonEKSWorkerNodePolicy, AmazonEKS_CNI_Policy, AmazonEC2ContainerRegistryReadOnly). Use managed node groups for automatic IAM setup." + +- symptoms: "errors tool returns findings with ECR image pull AccessDenied" + diagnosis: "Node role missing AmazonEC2ContainerRegistryReadOnly. Use search with query=ecr.*AccessDenied to confirm." + resolution: "Operator action: attach AmazonEC2ContainerRegistryReadOnly to node IAM role" + +- symptoms: "search for aws-node returns VPC CNI AccessDenied errors" + diagnosis: "Node role missing AmazonEKS_CNI_Policy. Use network_diagnostics to check CNI health." + resolution: "Operator action: attach AmazonEKS_CNI_Policy to node IAM role (or use IRSA for CNI)" + +- symptoms: "errors tool returns findings with node registration Unauthorized" + diagnosis: "Node role missing AmazonEKSWorkerNodePolicy. Use search with query=bootstrap to check registration logs." + resolution: "Operator action: attach AmazonEKSWorkerNodePolicy to node IAM role" + +- symptoms: "search returns IamInstanceProfileNotFound or IamNodeRoleNotFound in node group events" + diagnosis: "The IAM instance profile or node role referenced by the managed node group was deleted. The node group enters Degraded state." + resolution: "Operator action: delete the degraded node group and create a new one with a valid IAM role. The deleted role/instance profile cannot be re-associated with the existing node group." + +- symptoms: "search returns ECR repository policy AccessDenied or cross-account pull failure" + diagnosis: "ECR repository policy does not allow the node role to pull images. This is separate from the IAM policy on the node role — the repository itself has a resource-based policy that can deny access." + resolution: "Operator action: update the ECR repository policy to allow ecr:GetDownloadUrlForLayer, ecr:BatchGetImage, ecr:BatchCheckLayerAvailability for the node role ARN. Use 'aws ecr set-repository-policy'." + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Poll status +status(executionId="") +# Step 3: Get permission findings +errors(instanceId="i-0abc123def456", severity="high") +# Step 4: Search for AccessDenied evidence +search(instanceId="i-0abc123def456", query="AccessDenied|is not authorized to perform|UnauthorizedAccess") +# Step 5: Correlate permission failure timeline +correlate(instanceId="i-0abc123def456", pivotEvent="AccessDenied", timeWindow=120) +# Step 6: Check cluster-wide impact +cluster_health(clusterName="my-cluster") +# Step 7: Generate summary +summarize(instanceId="i-0abc123def456", finding_ids=["F-001","F-002"]) +``` + +## Output Format + +```yaml +root_cause: "Missing IAM policy — " +evidence: + - type: iam_finding + content: "" + - type: correlation + content: "" +severity: HIGH +mitigation: + immediate: "Operator: attach to node IAM role" + long_term: "Use managed node groups for automatic IAM setup" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/H2-irsa-pod-identity-confusion.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/H2-irsa-pod-identity-confusion.md new file mode 100644 index 0000000..369aba4 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/H2-irsa-pod-identity-confusion.md @@ -0,0 +1,170 @@ +--- +title: "H2 — IRSA / Pod Identity Confusion" +description: "Diagnose AWS API AccessDenied errors caused by IRSA or Pod Identity misconfiguration" +status: active +severity: HIGH +triggers: + - "AccessDenied.*AssumeRoleWithWebIdentity" + - "ExpiredTokenException" + - "InvalidIdentityToken" + - "No OpenIDConnect provider found" +owner: devops-agent +objective: "Identify the IRSA or Pod Identity misconfiguration and restore AWS API access from pods" +context: "Pods use IRSA (IAM Roles for Service Accounts) or EKS Pod Identity to assume IAM roles for AWS API access. Misconfigurations in ServiceAccount annotations, OIDC provider, trust policies, or Pod Identity associations cause AccessDenied errors." +--- + +## Phase 1 — Triage + +FIRST — Check pod state before collecting logs: +- Use `list_k8s_resources` with clusterName, kind=Pod, apiVersion=v1, namespace= to list pods in the affected namespace — check for pods in CrashLoopBackOff or Error state due to credential failures +- Use `read_k8s_resource` with clusterName, kind=Pod, apiVersion=v1, namespace=, name= to get detailed pod spec — check serviceAccountName, projected volume mounts for token, and container status/restart count +- Use `read_k8s_resource` with clusterName, kind=ServiceAccount, apiVersion=v1, namespace=, name= to check ServiceAccount annotations for eks.amazonaws.com/role-arn +- Use `get_k8s_events` with clusterName, kind=Pod, namespace=, name= to check for credential-related warning events + +MUST: +- Use `collect` tool with instanceId of the node running the affected pod to gather node-level logs +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId and severity=high to get pre-indexed IRSA/credential findings +- Use `search` tool with instanceId and query=`AccessDenied.*AssumeRoleWithWebIdentity|ExpiredTokenException|InvalidIdentityToken|No OpenIDConnect provider` to find IRSA errors + +SHOULD: +- Use `search` tool with query=`eks.amazonaws.com/role-arn|service-account|projected.*token` to check ServiceAccount configuration evidence in kubelet logs +- Use `search` tool with query=`AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE|EKS_POD_IDENTITY` to check pod environment variable injection + +MAY: +- Use `cluster_health` tool with clusterName to check if OIDC provider is configured for the cluster +- Use `search` tool with query=`pod-identity-agent|pod identity association` to check Pod Identity agent status + +## Phase 2 — Enrich + +MUST: +- Use `correlate` tool with instanceId and pivotEvent=`AccessDenied` to build timeline of credential failures +- Review findings from `errors` tool to classify the failure: + - If "AssumeRoleWithWebIdentity" denied: SA annotation missing or trust policy mismatch + - If "No OpenIDConnect provider found": OIDC provider not created for cluster + - If "InvalidIdentityToken": trust policy condition has wrong namespace/SA + - If "ExpiredTokenException": token expiration or projected volume mount issue +- Use `search` tool with query=`sts.*AssumeRole|oidc|web-identity` to find the specific API call that failed + +SHOULD: +- Use `search` tool with query=`audience|sts.amazonaws.com` to check token audience configuration +- Use `search` tool with query=`pod-identity|EKS_POD_IDENTITY_AGENT` to determine if Pod Identity is in use vs IRSA +- Use `search` tool with query=`MutatingWebhookConfiguration|pod-identity-webhook` to verify webhook is injecting IRSA env vars +- Use `search` tool with query=`169.254.170.23|eks-pod-identity-agent|Read timeout` to check Pod Identity agent connectivity + +MAY: +- Use `search` tool with query=`token.*expir|token.*refresh` to check for intermittent token expiration failures +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="ServiceAccount" to check for recent ServiceAccount mutations (annotation changes, deletions) that may have broken IRSA bindings +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="podidentityassociation" to check for Pod Identity association create/update/delete events + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from IRSA/credential findings to generate incident summary +- State root cause: specific IRSA/Pod Identity misconfiguration with evidence from findings +- Recommend targeted fix based on root cause classification +- Operator action — not available via MCP tools: annotate ServiceAccount, create OIDC provider, update IAM trust policy, create Pod Identity association + +SHOULD: +- Include the specific error message from findings +- Include SA annotation and trust policy details from search results + +MAY: +- Recommend migrating from IRSA to Pod Identity for simpler management + +## Guardrails + +escalation_conditions: + - "OIDC provider creation requires cluster admin access" + - "IAM role trust policy changes require security team approval" + - "Multiple services affected by the same IRSA misconfiguration — check via cluster_health" + +safety_ratings: + - "Log collection (collect), search, errors, correlate, cluster_health: GREEN (read-only)" + - "Annotate ServiceAccount: YELLOW — operator action, not available via MCP tools" + - "Create OIDC provider: YELLOW — operator action, not available via MCP tools" + - "Modify IAM trust policy: RED — operator action, requires security team approval" + +## Common Issues + +- symptoms: "errors tool returns findings with AccessDenied AssumeRoleWithWebIdentity" + diagnosis: "ServiceAccount not annotated with IAM role ARN. Use search with query=eks.amazonaws.com/role-arn to confirm." + resolution: "Operator action: kubectl annotate sa -n eks.amazonaws.com/role-arn=" + +- symptoms: "search returns No OpenIDConnect provider found" + diagnosis: "OIDC provider not created for the cluster. Use cluster_health to check cluster configuration." + resolution: "Operator action: eksctl utils associate-iam-oidc-provider --cluster --approve" + +- symptoms: "search returns InvalidIdentityToken" + diagnosis: "Trust policy condition has wrong namespace or service account name." + resolution: "Operator action: update IAM role trust policy to include correct OIDC issuer and conditions" + +- symptoms: "errors tool returns findings with ExpiredTokenException" + diagnosis: "Projected service account token expired or not mounted. Use search with query=token.*expir to check." + resolution: "Operator action: check token expiration settings, verify projected volume mount in pod spec" + +- symptoms: "search returns 'Request ARN is invalid' or extra colons in IAM ARN" + diagnosis: "IAM role ARN format is incorrect in ServiceAccount annotation. Extra colons or malformed ARN causes STS to reject the request." + resolution: "Operator action: fix the eks.amazonaws.com/role-arn annotation to use correct ARN format (arn:aws:iam::ACCOUNT:role/ROLE-NAME, no extra colons)" + +- symptoms: "search returns 'Incorrect token audience' or audience mismatch" + diagnosis: "OIDC provider audience is not set to sts.amazonaws.com. Use search with query=audience|sts.amazonaws.com to confirm." + resolution: "Operator action: update OIDC provider audience to sts.amazonaws.com in IAM console or via CLI" + +- symptoms: "search returns 'HTTPS certificate doesn't match configured thumbprint'" + diagnosis: "OIDC provider root CA thumbprint is stale or incorrect. AWS rotated the certificate." + resolution: "Operator action: update OIDC provider thumbprint — get new thumbprint from cluster OIDC issuer URL and update in IAM" + +- symptoms: "search returns 'Not authorized to perform sts:AssumeRoleWithWebIdentity' but SA annotation and OIDC are correct" + diagnosis: "IAM role trust policy conditions do not match the ServiceAccount namespace/name or OIDC issuer URL." + resolution: "Operator action: update trust policy Condition to match exact namespace:sa-name and correct OIDC issuer URL" + +- symptoms: "search for AWS_WEB_IDENTITY_TOKEN_FILE returns no matches in pod environment" + diagnosis: "Pod identity webhook (pod-identity-webhook) is not injecting environment variables. The MutatingWebhookConfiguration may be missing or invalid." + resolution: "Operator action: verify pod-identity-webhook MutatingWebhookConfiguration exists — kubectl get mutatingwebhookconfigurations. Restart the webhook pod if needed." + +- symptoms: "search returns 'Error retrieving metadata.*Read timeout.*169.254.170.23' or Pod Identity credential fetch timeout" + diagnosis: "EKS Pod Identity agent cannot reach the eks-auth endpoint. In private clusters, the eks-auth VPC endpoint may be missing." + resolution: "Operator action: for private clusters, create eks-auth PrivateLink VPC endpoint. Check security groups allow traffic to the endpoint. Check eks-pod-identity-agent pod logs for errors." + +- symptoms: "search returns proxy-related errors in eks-pod-identity-agent logs" + diagnosis: "Pod Identity agent needs proxy configuration to reach eks-auth endpoint through a corporate proxy." + resolution: "Operator action: configure proxy on the DaemonSet — kubectl set env ds/eks-pod-identity-agent https_proxy=PROXY-URL -n kube-system" + +- symptoms: "IRSA works for some AWS SDK calls but not others, or returns 'InvalidIdentityToken' intermittently" + diagnosis: "AWS SDK version may be too old to support IRSA token refresh. Older SDKs do not auto-refresh projected SA tokens." + resolution: "Operator action: upgrade AWS SDK in the application container to a version that supports IRSA (check AWS SDK compatibility matrix)" + +## Examples + +``` +# Step 1: Collect logs from node running affected pod +collect(instanceId="i-0abc123def456") +# Step 2: Poll status +status(executionId="") +# Step 3: Get IRSA/credential findings +errors(instanceId="i-0abc123def456", severity="high") +# Step 4: Search for IRSA errors +search(instanceId="i-0abc123def456", query="AccessDenied.*AssumeRoleWithWebIdentity|InvalidIdentityToken|ExpiredTokenException") +# Step 5: Check SA configuration evidence +search(instanceId="i-0abc123def456", query="eks.amazonaws.com/role-arn|AWS_ROLE_ARN|AWS_WEB_IDENTITY_TOKEN_FILE") +# Step 6: Correlate credential failure timeline +correlate(instanceId="i-0abc123def456", pivotEvent="AccessDenied", timeWindow=120) +# Step 7: Generate summary +summarize(instanceId="i-0abc123def456", finding_ids=["F-001","F-002"]) +``` + +## Output Format + +```yaml +root_cause: "" +evidence: + - type: credential_finding + content: "" + - type: sa_config + content: "" +severity: HIGH +mitigation: + immediate: "Operator: " + long_term: "Migrate to Pod Identity, use Terraform/CDK for SA-Role bindings" +``` \ No newline at end of file diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/H3-imds-issues.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/H3-imds-issues.md new file mode 100644 index 0000000..682e768 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/H3-imds-issues.md @@ -0,0 +1,132 @@ +--- +title: "H3 — IMDS (Instance Metadata Service) Issues" +description: "Diagnose credential and metadata failures caused by IMDS accessibility issues" +status: active +severity: HIGH +triggers: + - "Unable to retrieve.*metadata" + - "169.254.169.254.*timed out" + - "EC2MetadataError" + - "failed to get credentials.*IMDS" +owner: devops-agent +objective: "Identify why IMDS is unreachable and restore metadata/credential access" +context: "IMDS provides instance metadata and temporary credentials. Pods cannot reach IMDS when hop limit is 1 (extra network hop from container), when network policies block 169.254.169.254, or when IMDS is disabled." +--- + +## Phase 1 — Triage + +FIRST — Check node and pod state before collecting logs: +- Use `list_k8s_resources` with clusterName, kind=Node, apiVersion=v1 to list all nodes — check if the affected node is Ready and has valid conditions +- Use `read_k8s_resource` with clusterName, kind=Node, apiVersion=v1, name= to get detailed node status — check providerID and node conditions for credential-related issues +- Use `list_k8s_resources` with clusterName, kind=Pod, apiVersion=v1, fieldSelector=spec.nodeName= to list pods on the node — check for pods in CrashLoopBackOff or Error state due to IMDS/credential failures +- Use `get_k8s_events` with clusterName, kind=Node, name= to check for credential or metadata-related warning events + +MUST: +- Use `collect` tool with instanceId of the affected node to gather node-level logs +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId and severity=high to get pre-indexed IMDS/metadata findings +- Use `search` tool with instanceId and query=`169.254.169.254.*timed out|EC2MetadataError|Unable to retrieve.*metadata|failed to get credentials.*IMDS` to find IMDS errors + +SHOULD: +- Use `search` tool with query=`HttpPutResponseHopLimit|http-put-response-hop-limit|metadata-options` to check IMDS hop limit configuration +- Use `network_diagnostics` tool with instanceId and sections=iptables to check for iptables rules blocking 169.254.169.254 + +MAY: +- Use `cluster_health` tool with clusterName to check if multiple nodes have IMDS issues +- Use `search` tool with query=`network policy|NetworkPolicy|169.254` to check for network policies blocking metadata endpoint + +## Phase 2 — Enrich + +MUST: +- Use `correlate` tool with instanceId and pivotEvent=`metadata` to build timeline of IMDS failures +- Review findings from `errors` tool and `network_diagnostics` to classify the failure: + - If hop limit == 1: containers cannot reach IMDS (extra network hop) — needs increase to 2 + - If IMDS endpoint disabled: IMDS completely disabled — enable or use alternative credentials + - If iptables shows DROP for 169.254.169.254: explicit block — review security policy + - If IMDSv2 required but SDK too old: SDK does not support token flow +- Use `search` tool with query=`IMDSv2|http-tokens|HttpTokens` to check if IMDSv2 is required + +SHOULD: +- Use `search` tool with query=`IRSA|pod-identity|service-account` to check if IRSA/Pod Identity is available as alternative +- Use `search` tool with query=`launch template|user-data|metadata` to check launch template IMDS settings + +MAY: +- Use `compare_nodes` tool to check if IMDS issue affects all nodes or specific ones +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="NetworkPolicy" to check for recent NetworkPolicy changes that may be blocking traffic to 169.254.169.254 (IMDS endpoint) + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from IMDS-related findings to generate incident summary +- State root cause: specific IMDS accessibility issue with evidence from findings and network_diagnostics +- Recommend fix based on root cause classification +- Operator action — not available via MCP tools: increase hop limit, enable IMDS, remove iptables block, or configure IRSA + +SHOULD: +- Include MetadataOptions showing hop limit and endpoint status from findings + +MAY: +- Recommend IRSA/Pod Identity as preferred alternative to IMDS for pod credentials + +## Guardrails + +escalation_conditions: + - "IMDS disabled by security policy and cannot be re-enabled" + - "Hop limit change requires launch template update across all node groups" + - "iptables IMDS block is intentional security control" + +safety_ratings: + - "Log collection (collect), search, errors, correlate, network_diagnostics: GREEN (read-only)" + - "Increase hop limit: YELLOW — operator action, not available via MCP tools" + - "Enable IMDS: YELLOW — operator action, not available via MCP tools" + - "Remove iptables block: RED — operator action, may violate security policy" + +## Common Issues + +- symptoms: "search returns IMDS timeout from pods, errors tool shows metadata failures" + diagnosis: "HttpPutResponseHopLimit is 1, containers need hop limit 2. Use search with query=HopLimit to confirm." + resolution: "Operator action: aws ec2 modify-instance-metadata-options --instance-id --http-put-response-hop-limit 2 --http-tokens required" + +- symptoms: "search returns IMDS completely unreachable from node (not just pods)" + diagnosis: "IMDS disabled on instance. Use search with query=HttpEndpoint to confirm." + resolution: "Operator action: enable IMDS or use IRSA/Pod Identity for credentials" + +- symptoms: "network_diagnostics shows iptables DROP rule for 169.254.169.254" + diagnosis: "Explicit iptables rule blocking metadata endpoint." + resolution: "Operator action: review security policy, remove rule if unintended" + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Poll status +status(executionId="") +# Step 3: Get IMDS findings +errors(instanceId="i-0abc123def456", severity="high") +# Step 4: Search for IMDS errors +search(instanceId="i-0abc123def456", query="169.254.169.254.*timed out|EC2MetadataError|IMDS") +# Step 5: Check iptables for IMDS blocking +network_diagnostics(instanceId="i-0abc123def456", sections="iptables") +# Step 6: Check hop limit config +search(instanceId="i-0abc123def456", query="HttpPutResponseHopLimit|http-put-response-hop-limit") +# Step 7: Correlate IMDS failure timeline +correlate(instanceId="i-0abc123def456", pivotEvent="metadata", timeWindow=120) +# Step 8: Generate summary +summarize(instanceId="i-0abc123def456", finding_ids=["F-001","F-002"]) +``` + +## Output Format + +```yaml +root_cause: "" +evidence: + - type: imds_finding + content: "" + - type: network_diagnostics + content: "iptables rules for 169.254.169.254 from network_diagnostics" +severity: HIGH +mitigation: + immediate: "Operator: increase hop limit to 2 or remove iptables block" + long_term: "Use IRSA/Pod Identity instead of IMDS, set hop limit 2 in launch templates" +``` \ No newline at end of file diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/I1-version-skew.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/I1-version-skew.md new file mode 100644 index 0000000..0c9bb80 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/I1-version-skew.md @@ -0,0 +1,161 @@ +--- +title: "I1 — Kubernetes Version Skew" +description: "Diagnose API errors and node registration failures caused by Kubernetes version skew" +status: active +severity: HIGH +triggers: + - "the server could not find the requested resource" + - "no matches for kind.*in version" + - "is deprecated.*removed in" + - "kubelet version.*skew" +owner: devops-agent +objective: "Identify version skew between control plane, nodes, and addons, then plan upgrade path" +context: "Kubernetes supports N-2 minor version skew between control plane and nodes. Exceeding this causes registration failures, API errors, and feature incompatibilities. Addon versions must also be compatible." +--- + +## Phase 1 — Triage + +FIRST — Check node state and version info before collecting logs: +- Use `list_k8s_resources` with clusterName, kind=Node, apiVersion=v1 to list all nodes — check kubelet version in node status and identify nodes with different versions +- Use `read_k8s_resource` with clusterName, kind=Node, apiVersion=v1, name= to get detailed node info — check status.nodeInfo.kubeletVersion, status.nodeInfo.kubeProxyVersion, and node conditions (Ready/NotReady) +- Use `get_k8s_events` with clusterName, kind=Node, name= to check for version-related registration failures or API incompatibility events +- Use `describe_eks_resource` with resourceType=cluster, clusterName to get the control plane Kubernetes version for skew comparison + +MUST: +- Use `collect` tool with instanceId of the affected node to gather node-level logs +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId and severity=high to get pre-indexed version skew findings +- Use `search` tool with instanceId and query=`server could not find the requested resource|no matches for kind|deprecated.*removed|kubelet version.*skew` to find version-related errors + +SHOULD: +- Use `cluster_health` tool with clusterName to get cluster version and node version overview +- Use `search` tool with query=`kubeletVersion|kubeProxyVersion|server version` to find version strings in logs +- Use `compare_nodes` tool with instanceIds of multiple nodes to identify version inconsistencies across the fleet + +MAY: +- Use `search` tool with query=`deprecated|removed in|apiVersion` to find deprecated API usage in kubelet logs + +## Phase 2 — Enrich + +MUST: +- Use `correlate` tool with instanceId and pivotEvent=`version` to build timeline of version-related failures +- Review findings from `errors` tool and `cluster_health` to calculate version skew: + - If skew > 2 minor versions: unsupported — nodes must be upgraded + - If kubelet > API server: unsupported configuration — upgrade control plane first + - If kube-proxy or CoreDNS incompatible: addon version mismatch +- Use `search` tool with query=`kube-proxy|coredns|aws-node|vpc-cni` to check addon version strings + +SHOULD: +- Use `compare_nodes` tool to identify which nodes are on which versions — find the outliers +- Use `search` tool with query=`registration.*fail|register.*error|certificate` to check if version skew is causing registration failures +- Verify upgrade order was followed: control plane → nodes → addons. If nodes were upgraded before control plane, that is the root cause. +- Check addon compatibility: use `search` tool with query=`vpc-cni|coredns|kube-proxy|ebs-csi|efs-csi|aws-load-balancer` to identify all addon versions, then compare against compatible versions for the cluster K8s version + +MAY: +- Use `search` tool with query=`apiserver_requested_deprecated_apis|deprecated API` to find deprecated API usage +- Use `search` tool with query=`extended support|extended-support` to check if cluster is on EKS extended support (additional cost, should plan upgrade) +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="deprecated" to check kube-audit logs for deprecated API usage warnings — these indicate workloads using APIs that will be removed in future versions +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="removed" to check for API calls to already-removed endpoints + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from version-related findings to generate incident summary +- State root cause: version skew with specific versions from cluster_health and compare_nodes +- Recommend upgrade path: control plane first, then nodes, then addons +- Operator action — not available via MCP tools: upgrade control plane, update node groups, update addons + +SHOULD: +- Include version comparison table from cluster_health and compare_nodes results +- List any deprecated APIs found in search results that need updating before upgrade + +MAY: +- Recommend upgrade runbook with pre-flight checks +- Recommend running pluto or kubent to detect deprecated APIs before upgrade + +## Guardrails + +escalation_conditions: + - "Version skew > 3 minor versions (requires multi-step upgrade)" + - "Deprecated APIs used by critical workloads — found via search" + - "Addon upgrade fails due to compatibility issues" + +safety_ratings: + - "Log collection (collect), search, errors, correlate, cluster_health, compare_nodes: GREEN (read-only)" + - "Upgrade control plane: YELLOW — operator action, not available via MCP tools" + - "Update node groups: YELLOW — operator action, not available via MCP tools" + - "Update addons: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "search returns the server could not find the requested resource" + diagnosis: "Workload using API version removed in current control plane version. Use search with query=apiVersion to identify." + resolution: "Operator action: update workload manifests to use current API versions before upgrading" + +- symptoms: "errors tool returns findings with kubelet version skew or registration failure" + diagnosis: "Node kubelet version too old for control plane (>N-2). Use cluster_health to confirm versions." + resolution: "Operator action: update node group — aws eks update-nodegroup-version --cluster-name --nodegroup-name " + +- symptoms: "compare_nodes shows mixed kubelet versions across fleet" + diagnosis: "Rolling upgrade incomplete — some nodes on old version." + resolution: "Operator action: complete rolling upgrade of remaining node groups" + +- symptoms: "search returns addon incompatible after control plane upgrade" + diagnosis: "kube-proxy, CoreDNS, or VPC CNI version not compatible with new K8s version." + resolution: "Operator action: update addons — aws eks update-addon --cluster-name --addon-name --addon-version " + +- symptoms: "search returns API errors or node registration failures after control plane upgrade" + diagnosis: "Upgrade order violated — nodes or addons were not updated after control plane upgrade. Correct order: control plane first, then nodes, then addons (VPC CNI, CoreDNS, kube-proxy, EBS/EFS CSI drivers, AWS Load Balancer Controller)." + resolution: "Operator action: follow upgrade order — 1) control plane (aws eks update-cluster-version), 2) node groups (aws eks update-nodegroup-version), 3) addons (aws eks update-addon for each). Check addon compatibility first: aws eks describe-addon-versions --addon-name --kubernetes-version ." + +- symptoms: "search returns kubelet version higher than control plane version (e.g., kubelet v1.29 on control plane v1.28)" + diagnosis: "Kubelet version cannot be newer than the control plane. This is an unsupported configuration that causes unpredictable behavior." + resolution: "Operator action: upgrade control plane first to match or exceed kubelet version. EKS does not support downgrading the control plane." + +- symptoms: "cluster_health shows control plane and nodes more than 2 minor versions apart (e.g., control plane v1.30, nodes v1.27)" + diagnosis: "Version skew exceeds the supported N-2 limit. Nodes on v1.27 cannot communicate reliably with a v1.30 control plane." + resolution: "Operator action: upgrade nodes incrementally — cannot skip versions. Upgrade node groups to N-2 first, then N-1, then N. Each step requires a rolling update." + +- symptoms: "search returns errors after upgrading addons, or addon pods are CrashLoopBackOff after cluster upgrade" + diagnosis: "Addon version incompatible with the new Kubernetes version. Key addons to check: VPC CNI (aws-node), CoreDNS, kube-proxy, EBS CSI driver, EFS CSI driver, AWS Load Balancer Controller." + resolution: "Operator action: check compatible versions — aws eks describe-addon-versions --addon-name --kubernetes-version . Update each addon to a compatible version. For self-managed addons (e.g., AWS Load Balancer Controller), check the compatibility matrix in the addon documentation." + +- symptoms: "cluster is on extended support (v1.23 or older) and upgrade is needed" + diagnosis: "EKS extended support keeps older versions running but at additional cost. Clusters on extended support should be upgraded to standard support versions." + resolution: "Operator action: plan multi-step upgrade path. Cannot skip minor versions — must upgrade one version at a time (e.g., 1.23 → 1.24 → 1.25 → ... → target). Test each step in a staging cluster first. Check for deprecated APIs at each version boundary using pluto or kubent." + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Poll status +status(executionId="") +# Step 3: Get version skew findings +errors(instanceId="i-0abc123def456", severity="high") +# Step 4: Check cluster version overview +cluster_health(clusterName="my-cluster") +# Step 5: Compare node versions +compare_nodes(instanceIds=["i-0abc123def456","i-0xyz789ghi012"]) +# Step 6: Search for version errors +search(instanceId="i-0abc123def456", query="server could not find|no matches for kind|deprecated.*removed") +# Step 7: Correlate version failure timeline +correlate(instanceId="i-0abc123def456", pivotEvent="version", timeWindow=120) +# Step 8: Generate summary +summarize(instanceId="i-0abc123def456", finding_ids=["F-001","F-002"]) +``` + +## Output Format + +```yaml +root_cause: "Version skew — control plane vs nodes " +evidence: + - type: cluster_health + content: "" + - type: compare_nodes + content: "" +severity: HIGH +mitigation: + immediate: "Operator: update node groups to within N-2 of control plane" + long_term: "Implement upgrade runbook, use managed node groups for auto AMI updates" +``` \ No newline at end of file diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/J1-ena-throttling-instance-limits.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/J1-ena-throttling-instance-limits.md new file mode 100644 index 0000000..afe59ec --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/J1-ena-throttling-instance-limits.md @@ -0,0 +1,138 @@ +--- +title: "J1 — ENA Driver Throttling and Instance Network Limits" +description: "Diagnose packet loss and latency caused by ENA driver throttling or instance network limits" +status: active +severity: HIGH +triggers: + - "bw_in_allowance_exceeded.*[1-9]" + - "bw_out_allowance_exceeded.*[1-9]" + - "pps_allowance_exceeded.*[1-9]" + - "conntrack_allowance_exceeded.*[1-9]" + - "queue_\\d+_tx_cnt.*drop" +owner: devops-agent +objective: "Identify which network limit is exceeded and recommend instance right-sizing" +context: "EC2 instances have network performance limits (bandwidth, PPS, conntrack, linklocal). ENA driver tracks when these limits are exceeded via ethtool counters. Outdated ENA drivers may also have performance issues." +--- + +## Phase 1 — Triage + +FIRST — Check node state before collecting logs: +- Use `list_k8s_resources` with clusterName, kind=Node, apiVersion=v1 to list all nodes — check if the affected node is Ready and identify its instance type from node labels (node.kubernetes.io/instance-type) +- Use `read_k8s_resource` with clusterName, kind=Node, apiVersion=v1, name= to get detailed node status — check conditions, allocatable resources, and instance type label for network limit reference +- Use `list_k8s_resources` with clusterName, kind=Pod, apiVersion=v1, fieldSelector=spec.nodeName= to list pods on the node — check for pods experiencing network timeouts or connection failures +- Use `get_k8s_events` with clusterName, kind=Node, name= to check for network-related warning events + +MUST: +- Use `collect` tool with instanceId of the affected node to gather node-level logs +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId and severity=high to get pre-indexed ENA throttling findings +- Use `search` tool with instanceId and query=`bw_in_allowance_exceeded|bw_out_allowance_exceeded|pps_allowance_exceeded|conntrack_allowance_exceeded|linklocal_allowance_exceeded` to find ENA throttling evidence +- Use `network_diagnostics` tool with instanceId and sections=eni to check ENA stats and interface health + +SHOULD: +- Use `search` tool with query=`ena.*version|modinfo ena|ena driver` to check ENA driver version +- Use `search` tool with query=`instance-type|instance type` to identify the instance type and its network limits + +MAY: +- Use `cluster_health` tool with clusterName to check if multiple nodes have ENA throttling +- Use `compare_nodes` tool with instanceIds to compare ENA stats across nodes + +## Phase 2 — Enrich + +MUST: +- Review findings from `errors` tool and `network_diagnostics` to map nonzero counters to root cause: + - bw_in/out_allowance_exceeded: bandwidth limit hit — upgrade instance + - pps_allowance_exceeded: packet rate limit — reduce small packets or upgrade + - conntrack_allowance_exceeded: connection tracking limit — reduce connections or upgrade + - linklocal_allowance_exceeded: DNS/IMDS/NTP rate limit — use NodeLocal DNSCache +- Use `search` tool with query=`ena.*version|driver version` to check ENA driver version (< 2.8 may have performance issues) +- Use `correlate` tool with instanceId and pivotEvent=`allowance_exceeded` to build timeline of throttling events + +SHOULD: +- Use `network_diagnostics` to compare instance type specs against actual throughput +- Use `search` tool with query=`drop|error|tx_cnt` to check for packet drops on interfaces + +MAY: +- Use `compare_nodes` tool to identify which nodes are most affected by throttling + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from ENA throttling findings to generate incident summary +- State which limit(s) are exceeded with counter values from findings and network_diagnostics +- Recommend instance type with sufficient network specs +- Operator action — not available via MCP tools: upgrade instance type, update ENA driver, configure NodeLocal DNSCache + +SHOULD: +- Include ENA counter values and instance type from findings +- Include ENA driver version from search results + +MAY: +- Recommend CloudWatch alarms on allowance exceeded metrics +- Recommend placement groups for high-throughput workloads + +## Guardrails + +escalation_conditions: + - "Largest available instance type still insufficient" + - "Multiple allowance types exceeded simultaneously — check via network_diagnostics" + - "ENA driver update requires node replacement" + +safety_ratings: + - "Log collection (collect), search, errors, network_diagnostics, correlate, compare_nodes: GREEN (read-only)" + - "Upgrade instance type: YELLOW — operator action, not available via MCP tools" + - "Update ENA driver: YELLOW — operator action, requires node replacement" + +## Common Issues + +- symptoms: "network_diagnostics shows bw_in/out_allowance_exceeded > 0" + diagnosis: "Instance bandwidth limit hit. Use search with query=instance-type to identify current instance." + resolution: "Operator action: upgrade to instance type with higher baseline bandwidth" + +- symptoms: "search returns pps_allowance_exceeded > 0" + diagnosis: "Packets per second limit hit. Use network_diagnostics to confirm PPS counters." + resolution: "Operator action: reduce small packet workloads or upgrade instance type" + +- symptoms: "search returns linklocal_allowance_exceeded > 0" + diagnosis: "DNS/IMDS/NTP rate limit hit — too many requests to link-local addresses." + resolution: "Operator action: deploy NodeLocal DNSCache to reduce DNS traffic to link-local" + +- symptoms: "search for ena version shows version < 2.8" + diagnosis: "Outdated ENA driver may have performance issues." + resolution: "Operator action: update ENA driver to latest version (requires node replacement)" + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Poll status +status(executionId="") +# Step 3: Get ENA throttling findings +errors(instanceId="i-0abc123def456", severity="high") +# Step 4: Check ENA stats and interface health +network_diagnostics(instanceId="i-0abc123def456", sections="eni") +# Step 5: Search for ENA throttling evidence +search(instanceId="i-0abc123def456", query="bw_in_allowance_exceeded|pps_allowance_exceeded|conntrack_allowance_exceeded") +# Step 6: Check ENA driver version +search(instanceId="i-0abc123def456", query="ena.*version|modinfo ena") +# Step 7: Correlate throttling timeline +correlate(instanceId="i-0abc123def456", pivotEvent="allowance_exceeded", timeWindow=120) +# Step 8: Generate summary +summarize(instanceId="i-0abc123def456", finding_ids=["F-001","F-002"]) +``` + +## Output Format + +```yaml +root_cause: " exceeded on " +evidence: + - type: network_diagnostics + content: "" + - type: ena_driver + content: "version= from search results" +severity: HIGH +mitigation: + immediate: "Operator: upgrade instance type for higher network limits" + long_term: "Monitor ENA metrics, right-size instances, update ENA driver" +``` \ No newline at end of file diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/J2-ebs-transient-attach.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/J2-ebs-transient-attach.md new file mode 100644 index 0000000..391a456 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/J2-ebs-transient-attach.md @@ -0,0 +1,171 @@ +--- +title: "J2 — EBS Transient Attach/Detach Failures" +description: "Diagnose pods stuck due to EBS volume attach timeouts, stale attachments, or multi-attach errors" +status: active +severity: HIGH +triggers: + - "AttachVolume.*timed out" + - "Multi-Attach error" + - "volume is already.*attached" + - "FailedAttachVolume" + - "WaitForAttach.*timeout" +owner: devops-agent +objective: "Identify the EBS attach failure reason and restore volume access" +context: "EBS volumes can get stuck in attaching state, remain attached to terminated nodes, or fail due to AZ mismatch. Multi-attach errors occur when a volume is still attached to a previous node." +--- + +## Phase 1 — Triage + +FIRST — Check pod and node state before collecting logs: +- Use `list_k8s_resources` with clusterName, kind=Pod, apiVersion=v1, namespace= to list pods — check for pods stuck in ContainerCreating or Pending state (indicates volume attach failure) +- Use `read_k8s_resource` with clusterName, kind=Pod, apiVersion=v1, namespace=, name= to get detailed pod status — check conditions for PodScheduled, volumes section for PVC references, and container status +- Use `get_k8s_events` with clusterName, kind=Pod, namespace=, name= to check for FailedAttachVolume, FailedMount, Multi-Attach, or WaitForAttach timeout events +- Use `read_k8s_resource` with clusterName, kind=PersistentVolumeClaim, apiVersion=v1, namespace=, name= to check PVC status (Bound/Pending) and the associated PV +- Use `list_k8s_resources` with clusterName, kind=Node, apiVersion=v1 to check which node the pod is scheduled on and its AZ label (topology.kubernetes.io/zone) + +MUST: +- **PREREQUISITE — Is EBS CSI driver installed?** Before investigating attach failures, verify the driver exists: + - Use `list_k8s_resources` with clusterName, kind=DaemonSet, apiVersion=apps/v1, namespace=kube-system, labelSelector=app.kubernetes.io/name=aws-ebs-csi-driver — if NO DaemonSet found, the EBS CSI driver is NOT installed. That is the root cause. Report "EBS CSI driver not installed" immediately. + - Use `list_k8s_resources` with clusterName, kind=Pod, apiVersion=v1, namespace=kube-system, labelSelector=app.kubernetes.io/name=aws-ebs-csi-driver — check that ebs-csi-controller and ebs-csi-node pods exist and are Running. If pods are missing or CrashLooping, that is the root cause. + - Use `describe_eks_resource` with clusterName, resourceType=addon, resourceName=aws-ebs-csi-driver — if addon not found, the driver was never installed as an EKS addon. Report this before investigating IAM or volume state. + - ONLY if CSI driver is confirmed installed and running, proceed to attach failure investigation below. +- **PREREQUISITE — Does the EBS volume exist?** Before investigating attach mechanics, verify the volume is real: + - Use `search` tool with instanceId and query=`InvalidVolume.NotFound|volume not found|vol-.*does not exist|VolumeNotFound` — if matches found, the EBS volume referenced by the PV/PVC has been deleted or never existed. That is the root cause. Report "EBS volume does not exist" immediately. + - Use `get_k8s_events` with clusterName, kind=PersistentVolumeClaim, namespace=, name= — check for events containing "volume not found" or "InvalidVolume". + - ONLY if volume existence is confirmed (no NotFound errors), proceed to attach failure investigation below. +- Use `collect` tool with instanceId of the affected node to gather node-level logs +- Use `status` tool with executionId to poll until collection completes +- Use `errors` tool with instanceId and severity=high to get pre-indexed EBS attach/detach findings +- Use `search` tool with instanceId and query=`AttachVolume.*timed out|Multi-Attach error|volume is already.*attached|FailedAttachVolume|WaitForAttach.*timeout` to find EBS attach failure evidence +- Use `storage_diagnostics` tool with instanceId and sections=ebs_csi,pv_pvc to check EBS CSI driver status and PV/PVC state + +SHOULD: +- Use `search` tool with query=`ebs-csi|csi-driver|csi-node` to check CSI driver pod health +- Use `search` tool with query=`VolumeAttachment|volume attachment|attach.*vol-` to find volume attachment details +- Use `search` tool with query=`ebs-csi.*version|csi-driver.*image|ebs-csi-controller` to check EBS CSI driver version — version must be compatible with the cluster Kubernetes version +- Use `search` tool with query=`eks.amazonaws.com/role-arn|serviceaccount.*annotation|IRSA|oidc` to check if the CSI controller service account has the IAM role annotation for IRSA — missing annotation means the CSI controller cannot call EC2 APIs + +MAY: +- Use `cluster_health` tool with clusterName to check if EBS CSI driver is healthy cluster-wide +- Use `search` tool with query=`throttl|API.*rate|TooManyRequests` to check for AWS API throttling on attach/detach calls +- Use `search` tool with query=`DeadlineExceeded|context deadline exceeded|timeout.*provision|timeout.*attach` to check if CSI controller cannot reach EC2 API — DeadlineExceeded on provisioning means the controller pod has no network path to the EC2 API endpoint + +## Phase 2 — Enrich + +MUST: +- Use `correlate` tool with instanceId and pivotEvent=`AttachVolume` to build timeline of attach failures +- Review findings from `errors` tool and `storage_diagnostics` to classify the failure: + - If volume "in-use" but attached to different node: stale attachment from previous pod + - If volume AZ != node AZ: AZ mismatch — volume cannot cross AZs + - If max volumes per instance reached: instance volume limit hit + - If ebs-csi-node pod not running: CSI driver issue +- Use `search` tool with query=`vol-.*state|volume.*status|in-use|available|attaching` to find volume state details + +SHOULD: +- Use `search` tool with query=`terminated|terminating|previous pod|graceful` to check if previous pod fully terminated +- Use `search` tool with query=`WaitForFirstConsumer|volumeBindingMode|StorageClass` to check StorageClass configuration +- Use `search` tool with query=`ebs-plugin|csi-provisioner|csi-attacher` to check CSI sidecar container logs for provisioning/attach errors +- Use `search` tool with query=`volume node affinity conflict|FailedScheduling.*affinity|topology.*zone` to check for AZ mismatch between pod scheduling and PV node affinity — StatefulSets with EBS volumes must use volumeBindingMode: WaitForFirstConsumer to ensure the volume is provisioned in the same AZ as the pod + +MAY: +- Use `compare_nodes` tool to check if EBS attach issues affect specific nodes or are cluster-wide + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from EBS-related findings to generate incident summary +- State root cause: specific attach failure with volume and node details from findings and storage_diagnostics +- Recommend fix based on root cause classification +- Operator action — not available via MCP tools: force detach volume (with data corruption warning), reschedule pod, fix CSI driver + +SHOULD: +- Include volume ID, state, and attachment details from findings +- Warn about data corruption risk for force detach operations + +MAY: +- Recommend WaitForFirstConsumer binding mode for topology-aware provisioning + +## Guardrails + +escalation_conditions: + - "Force detach needed on actively-written volume (data corruption risk)" + - "Volume stuck in attaching state for >10 minutes — check via storage_diagnostics" + - "CSI driver pods not running on any node — check via cluster_health" + +safety_ratings: + - "Log collection (collect), search, errors, storage_diagnostics, correlate: GREEN (read-only)" + - "Force detach volume: RED — operator action, data corruption risk, requires approval" + - "Reschedule pod to correct AZ: YELLOW — operator action, not available via MCP tools" + - "Restart CSI driver pods: YELLOW — operator action, not available via MCP tools" + +## Common Issues + +- symptoms: "list_k8s_resources returns no DaemonSet or pods for aws-ebs-csi-driver in kube-system" + diagnosis: "EBS CSI driver is not installed. Without the driver, no EBS volumes can be attached to pods." + resolution: "Operator action: install EBS CSI driver as EKS addon — aws eks create-addon --cluster-name --addon-name aws-ebs-csi-driver --service-account-role-arn " + +- symptoms: "search returns InvalidVolume.NotFound or volume not found errors" + diagnosis: "The EBS volume referenced by the PersistentVolume has been deleted or never existed." + resolution: "Operator action: delete the PV and PVC, then create a new PVC to provision a fresh volume. If data recovery is needed, check EBS snapshots." + +- symptoms: "errors tool returns findings with AttachVolume timed out, storage_diagnostics shows volume attached to different node" + diagnosis: "Stale attachment from previous pod that did not fully terminate. Use search with query=terminated to check." + resolution: "Operator action: wait for GC, or force detach — aws ec2 detach-volume --volume-id --force (data corruption risk)" + +- symptoms: "search returns volume AZ does not match node AZ" + diagnosis: "EBS volumes cannot cross AZs. Use storage_diagnostics to confirm AZ mismatch." + resolution: "Operator action: reschedule pod to correct AZ or create new volume in target AZ" + +- symptoms: "errors tool returns FailedAttachVolume with max volumes reached" + diagnosis: "Instance volume attachment limit hit. Use storage_diagnostics to confirm volume count." + resolution: "Operator action: move pods to nodes with available volume slots" + +- symptoms: "storage_diagnostics shows ebs-csi-node pod not running" + diagnosis: "CSI driver issue — ebs-csi-node DaemonSet pod not healthy." + resolution: "Operator action: restart ebs-csi-node pod, check CSI driver DaemonSet status" + +- symptoms: "search returns CSI controller service account missing eks.amazonaws.com/role-arn annotation" + diagnosis: "The ebs-csi-controller-sa service account does not have the IAM role annotation for IRSA. Without this, the CSI controller cannot call EC2 APIs to attach/detach/provision volumes." + resolution: "Operator action: annotate the service account — kubectl annotate serviceaccount ebs-csi-controller-sa -n kube-system eks.amazonaws.com/role-arn=. Verify the OIDC provider is configured for the cluster. Restart the ebs-csi-controller pods after annotation." + +- symptoms: "search returns DeadlineExceeded or context deadline exceeded on volume provisioning" + diagnosis: "CSI controller pod cannot reach the EC2 API endpoint. This happens when the controller pod is in a private subnet without NAT gateway or VPC endpoints for EC2." + resolution: "Operator action: ensure the CSI controller pods can reach the EC2 API — add NAT gateway to the subnet route table, or create a VPC endpoint for com.amazonaws..ec2. Check security groups allow outbound HTTPS (443)." + +- symptoms: "search returns FailedScheduling volume node affinity conflict" + diagnosis: "The PersistentVolume has a node affinity constraint for a specific AZ, but the pod is being scheduled in a different AZ. EBS volumes cannot cross AZs. This commonly happens with StatefulSets when volumeBindingMode is Immediate instead of WaitForFirstConsumer." + resolution: "Operator action: use StorageClass with volumeBindingMode: WaitForFirstConsumer to ensure volumes are provisioned in the same AZ as the pod. For existing volumes, either reschedule the pod to the correct AZ or create a new volume from a snapshot in the target AZ." + +## Examples + +``` +# Step 1: Collect logs +collect(instanceId="i-0abc123def456") +# Step 2: Poll status +status(executionId="") +# Step 3: Get EBS attach findings +errors(instanceId="i-0abc123def456", severity="high") +# Step 4: Check EBS CSI and PV/PVC status +storage_diagnostics(instanceId="i-0abc123def456", sections="ebs_csi,pv_pvc") +# Step 5: Search for attach failure evidence +search(instanceId="i-0abc123def456", query="AttachVolume.*timed out|Multi-Attach error|FailedAttachVolume") +# Step 6: Correlate attach failure timeline +correlate(instanceId="i-0abc123def456", pivotEvent="AttachVolume", timeWindow=120) +# Step 7: Generate summary +summarize(instanceId="i-0abc123def456", finding_ids=["F-001","F-002"]) +``` + +## Output Format + +```yaml +root_cause: "" +evidence: + - type: storage_diagnostics + content: "" + - type: attach_finding + content: "" +severity: HIGH +mitigation: + immediate: "Operator: " + long_term: "Use WaitForFirstConsumer, topology-aware provisioning" +``` \ No newline at end of file diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/J3-az-outage-impact.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/J3-az-outage-impact.md new file mode 100644 index 0000000..8af9dd4 --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/J3-az-outage-impact.md @@ -0,0 +1,103 @@ +--- +title: "J3 — Availability Zone Outage Impact" +description: "Diagnose and respond to multiple node failures caused by AZ-level infrastructure issues" +status: active +severity: CRITICAL +triggers: + - "NodeNotReady.*multiple nodes.*same AZ" + - "aws health.*operational issue" + - "VolumeAttachmentTimeout.*multiple" +owner: devops-agent +objective: "Confirm AZ-level impact, ensure workloads reschedule to healthy AZs, and minimize service disruption" +context: "When an AWS Availability Zone experiences degradation, multiple nodes in that AZ become NotReady simultaneously. Pods enter Unknown/Terminating state, and EBS volumes in the affected AZ become unavailable." +--- + +## Phase 1 — Triage + +FIRST — Check node and pod state across AZs: +- Use `list_k8s_resources` with clusterName, kind=Node, apiVersion=v1 to list all nodes — check Ready/NotReady status and AZ labels (topology.kubernetes.io/zone) to identify which AZ has NotReady nodes +- Use `read_k8s_resource` with clusterName, kind=Node, apiVersion=v1, name= for each NotReady node to get detailed conditions and last heartbeat times +- Use `list_k8s_resources` with clusterName, kind=Pod, apiVersion=v1 to list all pods across all namespaces — check for pods in Unknown, Terminating, or Pending state on nodes in the affected AZ +- Use `get_k8s_events` with clusterName, kind=Node, name= to check for NodeNotReady, NodeStatusUnknown, or Rebooted events on affected nodes + +MUST: +- Check node AZ distribution: `kubectl get nodes -L topology.kubernetes.io/zone` +- Identify NotReady nodes: `kubectl get nodes` +- Check AWS Health Dashboard for AZ events +- Check pod distribution: `kubectl get pods -A -o wide` + +SHOULD: +- Check EBS volume status in affected AZ: `aws ec2 describe-volume-status` +- Check AWS Health events: `aws health describe-events --filter eventTypeCategories=issue` + +MAY: +- Check if PodDisruptionBudgets are blocking rescheduling + +## Phase 2 — Enrich + +MUST: +- Confirm multiple NotReady nodes are all in the same AZ +- Check AWS Health Dashboard for AZ-level operational issues +- Verify pods are rescheduling to other AZs +- Check if StatefulSets with AZ-pinned volumes are stuck + +SHOULD: +- Check PDB configuration for affected workloads +- Verify topology spread constraints are configured + +MAY: +- Check if EBS volumes in affected AZ are showing errors + +## Phase 3 — Report + +MUST: +- State root cause: AZ-level degradation with affected AZ and node count +- Confirm workloads rescheduled to healthy AZs +- List any stuck workloads (StatefulSets, PDB-blocked) +- Recommend long-term multi-AZ resilience improvements + +SHOULD: +- Include node distribution by AZ +- Include AWS Health event details + +MAY: +- Recommend topology spread constraints +- Recommend multi-AZ EBS replication for critical data + +## Guardrails + +escalation_conditions: + - "Single-AZ cluster with no redundancy" + - "PDB blocking all pod rescheduling" + - "StatefulSets with AZ-pinned volumes stuck indefinitely" + - "AWS Health Dashboard shows no event but AZ nodes are down" + +## Common Issues + +- symptoms: "Multiple nodes NotReady, all in same AZ" + diagnosis: "AZ-level infrastructure degradation" + resolution: "Cordon affected nodes. Verify pods rescheduling to healthy AZs. Check AWS Health Dashboard." + +- symptoms: "PDB blocking eviction of pods from affected AZ" + diagnosis: "PDB too restrictive for AZ failure scenario" + resolution: "Temporarily relax PDB if service is degraded. Set maxUnavailable >= 33% for AZ tolerance." + +- symptoms: "StatefulSets stuck, EBS volumes in affected AZ" + diagnosis: "EBS volumes cannot move across AZs" + resolution: "Wait for AZ recovery, or restore from snapshot in healthy AZ (data loss risk)" + +## Output Format + +```yaml +root_cause: "AZ outage — affecting nodes" +evidence: + - type: nodes + content: "" + - type: aws_health + content: "" +blast_radius: " nodes, pods affected" +severity: CRITICAL +mitigation: + immediate: "Cordon affected nodes, verify pod rescheduling" + long_term: "Deploy across 3+ AZs, configure topology spread constraints" +``` diff --git a/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/K1-stuck-terminating-pods.md b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/K1-stuck-terminating-pods.md new file mode 100644 index 0000000..08a7e6c --- /dev/null +++ b/mcp/aws-eks-node-diagnostics-mcp/sops/runbooks/K1-stuck-terminating-pods.md @@ -0,0 +1,188 @@ +--- +title: "K1 — Stuck / Terminating Pods" +description: "Diagnose pods stuck in Terminating state using worker node kubelet and containerd logs to identify finalizers, PreStop hook failures, and container stop failures" +status: active +severity: HIGH +triggers: + - "pod.*Terminating" + - "stuck.*Terminating" + - "cannot delete pod" + - "finalizer.*blocking" + - "graceful deletion.*timeout" + - "PreStop.*failed" + - "killing pod.*timeout" +owner: devops-agent +objective: "Identify why pods are stuck in Terminating state by examining kubelet termination logs, containerd container stop logs, and finalizer state, then recommend targeted remediation" +context: > + When a pod is deleted, Kubernetes sends SIGTERM to containers, runs PreStop hooks, waits for + terminationGracePeriodSeconds, then sends SIGKILL. Pods can get stuck in Terminating if: (1) finalizers + block deletion, (2) PreStop hooks hang or fail, (3) containerd cannot stop the container process, + (4) kubelet loses contact with the API server and cannot update pod status, (5) the node is under + resource pressure causing kubelet to stall. Worker node logs are essential because kubelet logs show + the termination sequence, containerd logs show container stop operations, and dmesg shows if the + container process was OOM-killed during shutdown. Namespaces can also get stuck in Terminating if + they contain resources with finalizers or if API services report False status. +--- + +## Phase 1 — Triage + +MUST: +- **FIRST**: Check pod and namespace state before any log collection: + - List terminating pods: `kubectl get pods -A --field-selector=status.phase==Terminating` (via EKS MCP `list_k8s_resources` with field_selector=status.phase=Running, then filter for Terminating in results — or list all pods and check status) + - Check pod details: `kubectl describe pod ` (via EKS MCP `read_k8s_resource` kind=Pod) — look at metadata.finalizers, metadata.deletionTimestamp, spec.terminationGracePeriodSeconds, and container lastState + - Check pod events: `kubectl describe pod ` (via EKS MCP `get_k8s_events`) — look for "Killing" events with timestamps to see how long termination has been in progress + - Identify the node: check spec.nodeName from the pod spec — all further log investigation targets this node + - Check node conditions: `kubectl get nodes` (via EKS MCP `list_k8s_resources` kind=Node) — if the node is NotReady, kubelet cannot update pod status, causing pods to appear stuck +- **PREREQUISITE — Is kubelet running on the node?** Kubelet must be running to process pod termination: + - Use `collect` tool with instanceId to gather logs from the affected node + - Use `status` tool with executionId to poll until collection completes + - Use `search` tool with instanceId and query=`Active: active \(running\)|kubelet.*started|kubelet.service.*running` and logTypes=`kubelet` — if NO matches, kubelet is stopped. That is the root cause — kubelet cannot process termination when it is not running. + - Use `search` tool with instanceId and query=`Active: inactive|Active: failed|kubelet.service.*dead` — if matches found, report "kubelet not running — pod termination cannot proceed" as root cause. + - ONLY if kubelet is confirmed running, proceed to termination investigation below. +- Use `errors` tool with instanceId to get pre-indexed findings — look for termination-related errors +- Use `search` tool with instanceId and query=`Killing|killing pod|SyncLoop.*DELETE|graceful.*delete|termination.*grace` and logTypes=`kubelet` to find kubelet termination sequence logs + +SHOULD: +- Use `search` tool with query=`PreStop|preStop|pre-stop|lifecycle.*hook` and logTypes=`kubelet` to check for PreStop hook execution and failures +- Use `search` tool with query=`container.*stop|container.*kill|StopContainer|KillContainer` and logTypes=`containerd` to check if containerd is having trouble stopping the container +- Use `search` tool with query=`finalizer|Finalizer|metadata.*finalizers` to check for finalizer-related messages in kubelet logs + +MAY: +- Use `cluster_health` tool with clusterName to check if stuck pods are widespread +- Use `search` tool with query=`orphan|orphaned pod|cleanup` to check for orphaned pod cleanup issues + +## Phase 2 — Enrich + +MUST: +- Use `correlate` tool with instanceId and pivotEvent=`Killing` to build timeline around the termination attempt +- Classify the stuck reason from Phase 1 findings: + - **Finalizers blocking**: pod has metadata.finalizers that are not being removed by their controller + - **PreStop hook hanging**: kubelet logs show PreStop hook started but not completed within terminationGracePeriodSeconds + - **Container stop failure**: containerd logs show errors stopping the container process (process not responding to SIGTERM/SIGKILL) + - **Kubelet-API server disconnect**: kubelet cannot update pod status — node may be NotReady or network partitioned + - **Resource pressure**: node under memory/disk/PID pressure causing kubelet to stall on termination operations +- Use `search` tool with query=`SIGTERM|SIGKILL|signal.*kill|signal.*term` to trace the signal delivery sequence +- Use `search` tool with query=`oom-kill|OOMKilled|Out of memory` in dmesg to check if the container process was OOM-killed during shutdown + +SHOULD: +- Use `search` tool with query=`volume.*detach|volume.*unmount|PersistentVolume|pv.*finalizer` to check if volume cleanup is blocking termination +- Use `search` tool with query=`endpoint.*remove|service.*endpoint|EndpointSlice` to check if endpoint removal is stalled +- Use `search` tool with query=`api.*server.*unreachable|connection.*refused.*6443|TLS.*handshake.*timeout` to check kubelet-to-API-server connectivity + +MAY: +- Use `compare_nodes` tool to check if termination issues are node-specific +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="delete" to check kube-audit logs for delete operations and any API-level errors blocking deletion +- Use EKS MCP `get_cloudwatch_logs` with clusterName, resource_type="cluster", log_type="control-plane", filter_pattern="finalizer" to check for finalizer-related API mutations + +## Phase 3 — Report + +MUST: +- Use `summarize` tool with instanceId and finding_ids from termination-related findings +- State root cause with specific evidence: + - Finalizer name and which controller owns it + - PreStop hook command and timeout evidence + - Container stop failure with containerd error + - Kubelet connectivity issue with API server error +- Recommend targeted fix (operator action — not available via MCP tools) + +SHOULD: +- Include the termination timeline from correlate results +- Include the terminationGracePeriodSeconds value and how long the pod has been stuck + +MAY: +- Recommend adjusting terminationGracePeriodSeconds if PreStop hooks need more time +- Recommend reviewing finalizer controllers for reliability + +## Guardrails + +escalation_conditions: + - "Multiple pods stuck in Terminating across different nodes — possible API server issue" + - "Namespace stuck in Terminating with NamespaceContentRemaining or NamespaceFinalizersRemaining" + - "Kubelet not running on the node — pod termination cannot proceed" + - "Force delete needed — risk of data loss or corruption" + +safety_ratings: + - "Log collection (collect), search, errors, correlate: GREEN (read-only)" + - "Patch pod to remove finalizers: YELLOW — operator action, may skip cleanup" + - "Force delete pod (--grace-period=0 --force): RED — operator action, risk of data loss" + - "Restart kubelet: YELLOW — operator action, disrupts all pods on node" + +## Common Issues + +- symptoms: "read_k8s_resource shows pod has metadata.finalizers list with entries, deletionTimestamp is set" + diagnosis: "Finalizers are blocking pod deletion. The controller responsible for the finalizer has not removed it — either the controller is not running, or it encountered an error during cleanup." + resolution: "Operator action: identify the finalizer controller (e.g., volume controller, custom operator). If the controller is not running, restart it. If cleanup is genuinely complete, patch the pod to remove finalizers: kubectl patch pod -n -p '{\"metadata\":{\"finalizers\":null}}'" + +- symptoms: "search returns PreStop hook started but no completion, pod stuck for longer than terminationGracePeriodSeconds" + diagnosis: "PreStop hook is hanging. The hook command is not completing within the grace period. Kubelet waits for the hook before sending SIGTERM to the main container." + resolution: "Operator action: review the PreStop hook command — ensure it completes quickly. Increase terminationGracePeriodSeconds if the hook legitimately needs more time. Force delete the pod if the hook is stuck: kubectl delete pod -n --grace-period=0 --force" + +- symptoms: "search returns container stop errors in containerd logs, SIGTERM sent but process not exiting" + diagnosis: "Container process is not responding to SIGTERM. This can happen if the process traps SIGTERM but hangs during cleanup, or if the process is a zombie." + resolution: "Operator action: ensure the application handles SIGTERM properly. If stuck, force delete the pod. If containerd itself is stuck, restart containerd on the node (systemctl restart containerd)." + +- symptoms: "search returns api server unreachable or connection refused to port 6443 in kubelet logs" + diagnosis: "Kubelet cannot reach the API server to update pod status. The pod may have been terminated locally but the API server still shows it as Terminating." + resolution: "Operator action: check node network connectivity to the API server endpoint. Check security groups allow outbound to port 443/6443. If the node is network-partitioned, the pod will be cleaned up once connectivity is restored." + +- symptoms: "node is NotReady and all pods on it show as Terminating" + diagnosis: "Node has lost heartbeat. Kubelet is either stopped or the node is unreachable. All pods are marked Terminating by the node controller after the pod-eviction-timeout." + resolution: "Operator action: check if the node is reachable (SSH/SSM). If kubelet is stopped, restart it. If the node is unreachable, terminate and replace it. Pods will be rescheduled by their controllers." + +- symptoms: "namespace stuck in Terminating, kubectl describe namespace shows NamespaceContentRemaining" + diagnosis: "Resources remain in the namespace that Kubernetes cannot delete. Often caused by CRDs with finalizers or webhook failures blocking deletion." + resolution: "Operator action: list remaining resources (kubectl api-resources --verbs=list --namespaced -o name | xargs -n 1 kubectl get --show-kind --ignore-not-found -n ). Delete remaining resources. If CRDs have finalizers, patch them to remove finalizers. As last resort: kubectl patch namespace --type=json -p '[{\"op\": \"remove\", \"path\": \"/metadata/finalizers\"}]'" + +- symptoms: "namespace stuck in Terminating, kubectl describe namespace shows API service with False status" + diagnosis: "An API service (e.g., metrics-server, custom API aggregation) is reporting False status. Kubernetes cannot verify that all resources in the namespace are deleted when API services are unavailable." + resolution: "Operator action: check API service status (kubectl get apiservices | grep False). Fix or delete the unavailable API service. The namespace deletion will proceed once all API services are healthy." + +## Examples + +``` +# Step 1: Check pod state via EKS MCP +read_k8s_resource(clusterName="my-cluster", kind="Pod", apiVersion="v1", name="stuck-pod", namespace="default") +# Look at: metadata.finalizers, metadata.deletionTimestamp, spec.terminationGracePeriodSeconds + +# Step 2: Check pod events +get_k8s_events(clusterName="my-cluster", kind="Pod", name="stuck-pod", namespace="default") + +# Step 3: Collect node logs +collect(instanceId="i-0abc123def456") +status(executionId="") + +# Step 4: Check kubelet termination logs +search(instanceId="i-0abc123def456", query="Killing|killing pod|graceful.*delete", logTypes="kubelet") + +# Step 5: Check PreStop hooks +search(instanceId="i-0abc123def456", query="PreStop|preStop|lifecycle.*hook", logTypes="kubelet") + +# Step 6: Check containerd stop operations +search(instanceId="i-0abc123def456", query="container.*stop|StopContainer|KillContainer", logTypes="containerd") + +# Step 7: Correlate timeline +correlate(instanceId="i-0abc123def456", pivotEvent="Killing", timeWindow=300) + +# Step 8: Generate summary +summarize(instanceId="i-0abc123def456", finding_ids=["F-001","F-002","F-003"]) +``` + +## Output Format + +```yaml +root_cause: "" +evidence: + - type: pod_state + content: "" + - type: kubelet_search + content: "" + - type: containerd_search + content: "" + - type: correlate + content: "" +stuck_duration: "