diff --git a/README.md b/README.md index 9688b80..c972d90 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,7 @@ cd packages/api && bun run scripts/service-matrix.ts | Databases | Database | Yes (list, inspect) | Yes (list, create, delete, inspect) | Yes (list, create, inspect, delete) | | Databases | DynamoDB / Cosmos DB NoSQL / NoSQL | Yes (list, create, delete, inspect) | Yes (list, create, delete, inspect) | No | | Networking | Networking | Yes (list) | No | No | +| Networking | ELB / Load Balancing | Yes (list, create, delete, inspect) | No | No | | Integration | API Gateway | Yes (list, create, delete, inspect) | No | No | | Integration | EventBridge / Events | Yes (list, create, delete, inspect) | No | No | | Integration | SES Mailbox / Email | Yes (list, inspect) | No | No | diff --git a/bun.lock b/bun.lock index eacd4e5..2881ef5 100644 --- a/bun.lock +++ b/bun.lock @@ -13,6 +13,7 @@ "@aws-sdk/client-dynamodb": "^3.1088.0", "@aws-sdk/client-ec2": "^3.1076.0", "@aws-sdk/client-eks": "^3.1076.0", + "@aws-sdk/client-elastic-load-balancing-v2": "^3.1096.0", "@aws-sdk/client-eventbridge": "^3.1076.0", "@aws-sdk/client-iam": "^3.1076.0", "@aws-sdk/client-lambda": "^3.1076.0", @@ -82,6 +83,8 @@ "@aws-sdk/client-eks": ["@aws-sdk/client-eks@3.1080.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.28", "@aws-sdk/credential-provider-node": "^3.972.63", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/fetch-http-handler": "^5.6.2", "@smithy/node-http-handler": "^4.9.2", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-tN9O6f9vc9ns/uaPsN6vzEOrD7ftkWOMgzXoxY1J438WM2M8qcGbRJkhD2c+PvaZvZY3MVF7843mdIVrOA3zzQ=="], + "@aws-sdk/client-elastic-load-balancing-v2": ["@aws-sdk/client-elastic-load-balancing-v2@3.1122.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-7uMkQNqmVer3ST7cAOenfswJFxWM/KLe+bFFPQDfLO9HS8v7fDBdPKHVUMSFrBRsyN3A87K9nDQoYMChaBwHWQ=="], + "@aws-sdk/client-eventbridge": ["@aws-sdk/client-eventbridge@3.1121.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/signature-v4-multi-region": "^3.996.46", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-+zT6z91ZxHozsKGWf5wOJfxEebht3GmPBoXedlWoumHeAvzAueM1KwbWkzClLWjpi76HXuCIx7R3x4pvSVGqIw=="], "@aws-sdk/client-iam": ["@aws-sdk/client-iam@3.1120.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-node": "^3.972.81", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-w1UNruAHPJ2l6+fNsW3wBYa725ALR1FRQe+rLheeziNK3EmGK6wxTm39dgKyehPmeSXxEQYCg5gcrYq3Ew3CzQ=="], @@ -798,6 +801,20 @@ "@aws-sdk/client-eks/@smithy/types": ["@smithy/types@4.15.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-x3L0XSACF6UYzKpa9biqiRMgvH5+wnFFew9Tm/grFYqgaupPwx/+ojDPpPJM8dZON3S9tjz5U+PQYsCBd1Mw5Q=="], + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/types": ["@aws-sdk/types@3.974.5", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@smithy/core": ["@smithy/core@3.33.3", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-CsOeKq/9kA3y6VJHt+/+VTCtBaxJ4OTFpgrjIUhPpDIKxBci1k2bJaQASF2h/ELWrulGp+t97DZ0mevfAD8idg=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.7.2", "", { "dependencies": { "@smithy/core": "^3.33.2", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-nZyWTmSpJEXl6VtWVMBJve/7x12DZu6sIX1z1a+ZMaHlQQRs9Zpu6NbTe/gmxYXVRpkjxyDYpZ5gx2IM6f/Wkw=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@smithy/node-http-handler": ["@smithy/node-http-handler@4.11.3", "", { "dependencies": { "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2jY1tSpERfPfWqyBV2pH+iGFaghVsIJszJNsT7hxtQYhVJpWDyc0LqOWI+nXOxOAHaEfZ4PXXtp1wW1TGpHhkA=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@smithy/types": ["@smithy/types@4.17.2", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-FOKpVZob9MPTn2znRzGrnsMHv7BOsKVw3XiP/cOyYLDVZ9qKp4nifIiSCuUU/fIj5Vu0UOAxCFr+qRAtG0NUkA=="], + "@aws-sdk/client-eventbridge/@aws-sdk/core": ["@aws-sdk/core@3.977.9", "", { "dependencies": { "@aws-sdk/types": "^3.974.5", "@aws-sdk/xml-builder": "^3.972.40", "@aws/lambda-invoke-store": "^0.3.0", "@smithy/core": "^3.33.3", "@smithy/signature-v4": "^5.6.12", "@smithy/types": "^4.17.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-reqPFEQrZxDZpeGj4PFMepBeR5LGYHRqq/L0motTzgFkCRBA4rFdaVXDSLYyGHhxVz7sT2PDnPN9CluGSfgyJA=="], "@aws-sdk/client-eventbridge/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.81", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-ini": "^3.973.15", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rml+WitoFvXmv6JZ18U/xGdGDGGvB/mOin0ya0lTnTrdC0Z1lrVxTYh7iNklZBcvcRMrs4DoEf6xy1KWyrLQQw=="], @@ -1054,6 +1071,24 @@ "@aws-sdk/client-eks/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.4.6", "", { "dependencies": { "@smithy/core": "^3.29.1", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-B2WQ/PV/H6Jeg3lrIq6bKUfa6Hy01mtK7CGs6lhjzHA6k4aagldH6T6eEjnzKl4HI0cJnAsxfJ19pgb5PV+CVQ=="], + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.7.1", "", { "dependencies": { "@smithy/core": "^3.33.1", "@smithy/types": "^4.17.1", "tslib": "^2.6.2" } }, "sha512-F1e1qACqF/CODey2t/A+/8akr5O80TUcEkILoXAunEok7Tj5lN7LBvPOQjtGon44a1Y6V+1L+q21ycbaJCfw6w=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.70", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-H404B7dJl2mCrBqahDEYsanB0xhdDp6tXnXcTUnXmmpy2Q3J0Ho0bUajZ2jr/RdwzCyS59Gi8xXIFwPLGBl6Uw=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.72", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-X98zYOrVOeuosCX+6ktf29FC2N2GHPLia7qv6mzPzTc+RPAuHWCDS++Z6JK7eGYqb/v6uaW7bAXaOvDBfol+0w=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.973.15", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/credential-provider-env": "^3.972.70", "@aws-sdk/credential-provider-http": "^3.972.72", "@aws-sdk/credential-provider-login": "^3.972.77", "@aws-sdk/credential-provider-process": "^3.972.70", "@aws-sdk/credential-provider-sso": "^3.973.14", "@aws-sdk/credential-provider-web-identity": "^3.972.76", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/credential-provider-imds": "^4.4.16", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Rykg6s5ceBuynMOGWgoowO4N+27JfnqXAnVaSunZl0hOO1XodSrxGNz6sCEbnmS0lAfQZDKyb3fbr46gSuv6Sg=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.70", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-2ry03fGRJr4sV3jI+ocjj5JqALnFD6ymM5KiNCDZMvq8bX2GSbE0vji4aM43TVCl2nXqqLRZaUxdq/KeWRAY4Q=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.973.14", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/token-providers": "3.1116.0", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-jkhg/8ocAAoc0RFyLMhCw+/zZh7gystQgd4F4hznNa8P4Cc501PQmxd+jGLiMHodPJ+7Zv/3znM62gZojyasmA=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.76", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-d3AGyVu759PGr35mEB2s22xxlNEA5rpdxtSPJthfPFJvoQ8dt357iVPECqWfUxXp1toJAvKmbtcIYVGigaGsCA=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.5.1", "", { "dependencies": { "@smithy/core": "^3.33.1", "@smithy/types": "^4.17.1", "tslib": "^2.6.2" } }, "sha512-8UYAvCeKsO8bpvjpwjcDkXscAkSWItbUHkric1PRJtIucpUTNK+va0uQIQHO1Jt2I1jhFQdlGrRTxWymncVsXA=="], + "@aws-sdk/client-eventbridge/@aws-sdk/core/@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.40", "", { "dependencies": { "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA=="], "@aws-sdk/client-eventbridge/@aws-sdk/core/@smithy/signature-v4": ["@smithy/signature-v4@5.7.1", "", { "dependencies": { "@smithy/core": "^3.33.1", "@smithy/types": "^4.17.1", "tslib": "^2.6.2" } }, "sha512-F1e1qACqF/CODey2t/A+/8akr5O80TUcEkILoXAunEok7Tj5lN7LBvPOQjtGon44a1Y6V+1L+q21ycbaJCfw6w=="], @@ -1262,6 +1297,24 @@ "@aws-sdk/client-eks/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.28", "", { "dependencies": { "@aws-sdk/core": "^3.974.28", "@aws-sdk/signature-v4-multi-region": "^3.996.38", "@aws-sdk/types": "^3.973.15", "@smithy/core": "^3.29.0", "@smithy/fetch-http-handler": "^5.6.2", "@smithy/node-http-handler": "^4.9.2", "@smithy/types": "^4.15.1", "tslib": "^2.6.2" } }, "sha512-1oG/mM3jmE2M3kad2zWJS6IKIY8hjRV4l5kAgg+xTQdLMTtehhcSucL/y4WqQpcHmQwi6+gRK8E46GYl1NBW9Q=="], + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.33.1", "", { "dependencies": { "@smithy/types": "^4.17.1", "tslib": "^2.6.2" } }, "sha512-h5BsgsPCJhhFLPYaoFhJYitOMZ49UCT4drE4BcvUl/xTHAQzjBK2utAfyFkHU1tG2geYXut8b9inlEDRZZfnwA=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.17.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-H1pGxMPydRIZ8zeDUndh5iyX1DyYfdrCn96dpAV+JxQgzYwHmn4TCgyjYlFndvPA8YbPrG0K8820FuRPuV6H4w=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.77", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-Jb59xfEISoN5mmbnA+HYqdtrSX3CgCtJoof+V5D8/TgUI56W63GEEd5Y58WijU3Ou6+WEgaLD1feVzaRXV5IDQ=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.44", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/signature-v4-multi-region": "^3.996.46", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.44", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/signature-v4-multi-region": "^3.996.46", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1116.0", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/nested-clients": "^3.997.44", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-ygIivKqh8aHzNkucOCXHyIBgBpLPfrSI0mCqXF+vLBsPTUKqj0VSqAY0GFPe7lQl4HntjOcQ+KSyS7oUV2C54Q=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-web-identity/@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.44", "", { "dependencies": { "@aws-sdk/core": "^3.977.9", "@aws-sdk/signature-v4-multi-region": "^3.996.46", "@aws-sdk/types": "^3.974.5", "@smithy/core": "^3.33.3", "@smithy/fetch-http-handler": "^5.7.2", "@smithy/node-http-handler": "^4.11.3", "@smithy/types": "^4.17.2", "tslib": "^2.6.2" } }, "sha512-NhEgryjlBF9w38ZXqGymQV28IhkYa1mKhlbYnqIis57AYwWGVYfUPgg/qC2rLRqOUfblxx++irvju10kVTa8Vw=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds/@smithy/core": ["@smithy/core@3.33.1", "", { "dependencies": { "@smithy/types": "^4.17.1", "tslib": "^2.6.2" } }, "sha512-h5BsgsPCJhhFLPYaoFhJYitOMZ49UCT4drE4BcvUl/xTHAQzjBK2utAfyFkHU1tG2geYXut8b9inlEDRZZfnwA=="], + + "@aws-sdk/client-elastic-load-balancing-v2/@aws-sdk/credential-provider-node/@smithy/credential-provider-imds/@smithy/types": ["@smithy/types@4.17.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-H1pGxMPydRIZ8zeDUndh5iyX1DyYfdrCn96dpAV+JxQgzYwHmn4TCgyjYlFndvPA8YbPrG0K8820FuRPuV6H4w=="], + "@aws-sdk/client-eventbridge/@aws-sdk/core/@smithy/signature-v4/@smithy/core": ["@smithy/core@3.33.1", "", { "dependencies": { "@smithy/types": "^4.17.1", "tslib": "^2.6.2" } }, "sha512-h5BsgsPCJhhFLPYaoFhJYitOMZ49UCT4drE4BcvUl/xTHAQzjBK2utAfyFkHU1tG2geYXut8b9inlEDRZZfnwA=="], "@aws-sdk/client-eventbridge/@aws-sdk/core/@smithy/signature-v4/@smithy/types": ["@smithy/types@4.17.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-H1pGxMPydRIZ8zeDUndh5iyX1DyYfdrCn96dpAV+JxQgzYwHmn4TCgyjYlFndvPA8YbPrG0K8820FuRPuV6H4w=="], diff --git a/packages/api/package.json b/packages/api/package.json index d08078c..cb508b4 100644 --- a/packages/api/package.json +++ b/packages/api/package.json @@ -15,6 +15,7 @@ "@aws-sdk/client-ec2": "^3.1076.0", "@aws-sdk/client-dynamodb": "^3.1088.0", "@aws-sdk/client-eks": "^3.1076.0", + "@aws-sdk/client-elastic-load-balancing-v2": "^3.1096.0", "@aws-sdk/client-eventbridge": "^3.1076.0", "@aws-sdk/client-iam": "^3.1076.0", "@aws-sdk/client-lambda": "^3.1076.0", diff --git a/packages/api/src/adapter-aws/AwsLoadBalancingAdapter.test.ts b/packages/api/src/adapter-aws/AwsLoadBalancingAdapter.test.ts new file mode 100644 index 0000000..9a2425e --- /dev/null +++ b/packages/api/src/adapter-aws/AwsLoadBalancingAdapter.test.ts @@ -0,0 +1,223 @@ +import {describe, expect, test} from 'bun:test' +import { + CreateLoadBalancerCommand, + DeleteLoadBalancerCommand, + DescribeLoadBalancersCommand, + type ElasticLoadBalancingV2Client, +} from '@aws-sdk/client-elastic-load-balancing-v2' +import {AwsLoadBalancingAdapter} from './AwsLoadBalancingAdapter' +import {ValidationError} from '../cloud-spi/errors' + +type SendResult = Record + +function stubElb(handler: (command: object) => SendResult | Promise) { + const sent: object[] = [] + const client = { + async send(command: object) { + sent.push(command) + return handler(command) + }, + } as unknown as ElasticLoadBalancingV2Client + return {client, sent} +} + +const ARN = 'arn:aws:elasticloadbalancing:us-east-1:000000000000:loadbalancer/app/web/77aa388f69144785' + +const balancer = { + LoadBalancerArn: ARN, + LoadBalancerName: 'web', + DNSName: 'web-77aa388f69144785.elb.localhost.floci.io', + // The runtime nests this: provisioning + State: {Code: 'provisioning'}, + Type: 'application', + Scheme: 'internet-facing', + VpcId: 'vpc-default', + CreatedTime: new Date('2026-07-28T17:04:35.028Z'), + AvailabilityZones: [ + {ZoneName: 'us-east-1b', SubnetId: 'subnet-default-b'}, + {ZoneName: 'us-east-1c', SubnetId: 'subnet-default-c'}, + ], + SecurityGroups: ['sg-1'], +} + +function runtimeStub() { + return stubElb((command) => { + if (command instanceof CreateLoadBalancerCommand) return {LoadBalancers: [balancer]} + if (command instanceof DescribeLoadBalancersCommand) return {LoadBalancers: [balancer]} + return {} + }) +} + +const validValues = {name: 'web', subnets: 'subnet-default-b, subnet-default-c'} + +describe('AwsLoadBalancingAdapter', () => { + test('identifies itself as the AWS load balancing adapter', () => { + const adapter = new AwsLoadBalancingAdapter(runtimeStub().client) + + expect(adapter.cloud).toBe('aws') + expect(adapter.service).toBe('loadbalancing') + expect(adapter.schema().displayName).toBe('AWS Elastic Load Balancing') + }) + + test('lists load balancers and reads the nested state code', async () => { + const {client, sent} = runtimeStub() + const [resource] = await new AwsLoadBalancingAdapter(client).list() + + expect(sent[0]).toBeInstanceOf(DescribeLoadBalancersCommand) + expect(resource).toMatchObject({ + // The ARN is the id: Describe and Delete both take one. + id: ARN, + name: 'web', + cloud: 'aws', + service: 'loadbalancing', + type: 'load-balancer', + region: 'us-east-1', + // State is nested, not a flat field — reading State directly yields + // an object and the column would render blank. + status: 'provisioning', + createdAt: '2026-07-28T17:04:35.028Z', + }) + expect(resource?.metadata).toMatchObject({ + dnsName: 'web-77aa388f69144785.elb.localhost.floci.io', + lbType: 'application', + scheme: 'internet-facing', + vpcId: 'vpc-default', + subnets: ['subnet-default-b', 'subnet-default-c'], + availabilityZones: ['us-east-1b', 'us-east-1c'], + }) + }) + + test('follows the pagination marker', async () => { + let call = 0 + const {client, sent} = stubElb((command) => { + if (!(command instanceof DescribeLoadBalancersCommand)) return {} + call += 1 + if (call === 1) return {LoadBalancers: [balancer], NextMarker: 'page-2'} + return {LoadBalancers: [{...balancer, LoadBalancerName: 'api', LoadBalancerArn: `${ARN}-2`}]} + }) + + const resources = await new AwsLoadBalancingAdapter(client).list() + + expect(resources.map((r) => r.name)).toEqual(['web', 'api']) + expect((sent[1] as DescribeLoadBalancersCommand).input.Marker).toBe('page-2') + }) + + test('treats an empty NextMarker as the end of the list', async () => { + // The runtime returns , which the SDK surfaces as + // an empty string; looping on truthiness alone would spin forever. + let call = 0 + const {client} = stubElb((command) => { + if (!(command instanceof DescribeLoadBalancersCommand)) return {} + call += 1 + return {LoadBalancers: [balancer], NextMarker: ''} + }) + + await expect(new AwsLoadBalancingAdapter(client).list()).resolves.toHaveLength(1) + expect(call).toBe(1) + }) + + test('filters by name and ARN', async () => { + const adapter = new AwsLoadBalancingAdapter(runtimeStub().client) + + await expect(adapter.list({search: 'we'})).resolves.toHaveLength(1) + await expect(adapter.list({search: 'loadbalancer/app'})).resolves.toHaveLength(1) + await expect(adapter.list({search: 'nope'})).resolves.toHaveLength(0) + }) + + test('inspects a load balancer by ARN', async () => { + const {client, sent} = runtimeStub() + const resource = await new AwsLoadBalancingAdapter(client).get(ARN) + + expect((sent[0] as DescribeLoadBalancersCommand).input.LoadBalancerArns).toEqual([ARN]) + expect(resource?.id).toBe(ARN) + }) + + test('returns null on LoadBalancerNotFound even though the runtime answers 400', async () => { + // ELB reports a missing load balancer as LoadBalancerNotFound with HTTP 400, + // not 404, so matching on the status would rethrow instead of returning null. + const {client} = stubElb(() => { + throw Object.assign(new Error('One or more load balancers not found.'), { + name: 'LoadBalancerNotFound', + $metadata: {httpStatusCode: 400}, + }) + }) + await expect(new AwsLoadBalancingAdapter(client).get(ARN)).resolves.toBeNull() + }) + + test('also returns null for the SDK-modelled exception name', async () => { + // The wire is LoadBalancerNotFound but the SDK calls the error + // LoadBalancerNotFoundException. Matching only the wire code passed every + // stubbed test here and still returned 400 against the live runtime. + const {client} = stubElb(() => { + throw Object.assign(new Error('One or more load balancers not found.'), { + name: 'LoadBalancerNotFoundException', + $metadata: {httpStatusCode: 400}, + }) + }) + await expect(new AwsLoadBalancingAdapter(client).get(ARN)).resolves.toBeNull() + }) + + test('returns null when the runtime answers with no load balancers', async () => { + const {client} = stubElb(() => ({LoadBalancers: []})) + await expect(new AwsLoadBalancingAdapter(client).get(ARN)).resolves.toBeNull() + }) + + test('rethrows a failure that is not a missing load balancer', async () => { + const {client} = stubElb(() => { + throw Object.assign(new Error('AccessDenied'), { + name: 'AccessDeniedException', + $metadata: {httpStatusCode: 403}, + }) + }) + await expect(new AwsLoadBalancingAdapter(client).get(ARN)).rejects.toThrow('AccessDenied') + }) + + test('creates a load balancer, splitting the comma separated subnets', async () => { + const {client, sent} = runtimeStub() + const resource = await new AwsLoadBalancingAdapter(client).create({values: validValues}) + + const command = sent[0] as CreateLoadBalancerCommand + expect(command).toBeInstanceOf(CreateLoadBalancerCommand) + expect(command.input.Name).toBe('web') + expect(command.input.Subnets).toEqual(['subnet-default-b', 'subnet-default-c']) + expect(command.input.Type).toBe('application') + expect(command.input.Scheme).toBe('internet-facing') + expect(resource.id).toBe(ARN) + }) + + test('requires at least two subnet ids', async () => { + // Only the count is enforced. The AZ requirement is ELB's, and checking it + // would mean calling EC2 from this adapter, so the message must not claim it. + const adapter = new AwsLoadBalancingAdapter(runtimeStub().client) + + await expect(adapter.create({values: {...validValues, subnets: 'subnet-only'}})).rejects.toThrow( + new ValidationError('subnets must list at least two subnet ids'), + ) + }) + + test('requires the fields the schema marks required', async () => { + const adapter = new AwsLoadBalancingAdapter(runtimeStub().client) + + await expect(adapter.create({values: {}})).rejects.toThrow(new ValidationError('name is required')) + await expect(adapter.create({values: {name: 'web'}})).rejects.toThrow( + new ValidationError('subnets is required'), + ) + }) + + test('rejects values the runtime would refuse', async () => { + const adapter = new AwsLoadBalancingAdapter(runtimeStub().client) + + for (const name of ['-web', 'web-', 'has spaces', 'a'.repeat(33)]) { + await expect(adapter.create({values: {...validValues, name}}), name).rejects.toThrow(ValidationError) + } + await expect(adapter.create({values: {...validValues, type: 'gateway'}})).rejects.toThrow(ValidationError) + await expect(adapter.create({values: {...validValues, scheme: 'sideways'}})).rejects.toThrow(ValidationError) + }) + + test('deletes a load balancer by ARN', async () => { + const {client, sent} = runtimeStub() + await new AwsLoadBalancingAdapter(client).delete(ARN) + + expect((sent[0] as DeleteLoadBalancerCommand).input.LoadBalancerArn).toBe(ARN) + }) +}) diff --git a/packages/api/src/adapter-aws/AwsLoadBalancingAdapter.ts b/packages/api/src/adapter-aws/AwsLoadBalancingAdapter.ts new file mode 100644 index 0000000..06e5553 --- /dev/null +++ b/packages/api/src/adapter-aws/AwsLoadBalancingAdapter.ts @@ -0,0 +1,185 @@ +import { + CreateLoadBalancerCommand, + DeleteLoadBalancerCommand, + DescribeLoadBalancersCommand, + type ElasticLoadBalancingV2Client, + type LoadBalancer, +} from '@aws-sdk/client-elastic-load-balancing-v2' +import {RuntimeError, ValidationError} from '../cloud-spi/errors' +import {ELB_NAME_PATTERN, ELB_SCHEMES, ELB_TYPES, awsLoadBalancingSchema} from '../cloud-spi/loadBalancingSchema' +import type { + CloudResource, + CloudServiceAdapter, + CreateResourceInput, + ResourceQuery, + ServiceSchema, +} from '../cloud-spi/types' + +/** + * Talks to Elastic Load Balancing — verified against Floci core 1.5.33. + * + * Notes from probing the runtime: + * - **Only ELBv2 is implemented.** A `Version=2012-06-01` request comes back in the + * `2015-12-01` namespace, so there is no separate classic ELB to model. + * - `State` is **nested** (`provisioning`). Reading + * `State` directly yields an object and the column renders blank. + * - A missing load balancer is `LoadBalancerNotFound` with HTTP **400**, not 404, + * so `get` matches on the error name. `awsErrors.ts` now maps that name (and + * `TargetGroupNotFound`) so the route reports 404 rather than a validation error. + * - The runtime returns `` at the end of the list, which + * the SDK surfaces as an empty string — paging must stop on falsy, not on + * undefined, or it spins forever. + * - A new load balancer reports `provisioning` and then reaches `active`; the + * state is surfaced as-is rather than waited on. + * + * Target groups are deliberately excluded: they are a second resource kind in this + * category and would need the `kind` facet from `ResourceQuery.filters` (#162). + * Shipping load balancers alone keeps this PR independent of that one. + */ +export class AwsLoadBalancingAdapter implements CloudServiceAdapter { + readonly cloud = 'aws' as const + readonly service = 'loadbalancing' as const + + constructor(private readonly elb: ElasticLoadBalancingV2Client) {} + + schema(): ServiceSchema { + return awsLoadBalancingSchema() + } + + async list(query: ResourceQuery = {}): Promise { + const balancers: LoadBalancer[] = [] + let marker: string | undefined + + do { + const res = await this.elb.send(new DescribeLoadBalancersCommand(marker ? {Marker: marker} : {})) + balancers.push(...(res.LoadBalancers ?? [])) + marker = res.NextMarker || undefined + } while (marker) + + return filterBySearch(balancers.map(toResource), query.search) + } + + async get(id: string): Promise { + try { + const res = await this.elb.send(new DescribeLoadBalancersCommand({LoadBalancerArns: [id]})) + const balancer = res.LoadBalancers?.[0] + return balancer ? toResource(balancer) : null + } catch (error) { + if (isMissing(error)) return null + throw error + } + } + + async create(input: CreateResourceInput): Promise { + const name = requiredString(input.values.name, 'name') + if (!new RegExp(ELB_NAME_PATTERN).test(name)) { + throw new ValidationError( + 'name must be up to 32 characters of letters, digits and hyphens, not starting or ending with a hyphen', + ) + } + + // Only the count is enforced. Whether the subnets sit in different + // availability zones is ELB's rule, and checking it here would mean calling + // EC2 DescribeSubnets from the load balancing adapter — real coupling for a + // validation nicety. So the message claims only what this check does; the + // schema field carries the AZ requirement as guidance. + const subnets = splitIds(requiredString(input.values.subnets, 'subnets')) + if (subnets.length < 2) { + throw new ValidationError('subnets must list at least two subnet ids') + } + + const type = optionalOneOf(input.values.type, ELB_TYPES, 'type') ?? 'application' + const scheme = optionalOneOf(input.values.scheme, ELB_SCHEMES, 'scheme') ?? 'internet-facing' + + const res = await this.elb.send( + new CreateLoadBalancerCommand({Name: name, Subnets: subnets, Type: type, Scheme: scheme}), + ) + const created = res.LoadBalancers?.[0] + if (!created) throw new RuntimeError(`ELB did not return the created load balancer for ${name}`) + return toResource(created) + } + + async delete(id: string): Promise { + await this.elb.send(new DeleteLoadBalancerCommand({LoadBalancerArn: id})) + } +} + +function toResource(balancer: LoadBalancer): CloudResource { + const arn = balancer.LoadBalancerArn ?? '' + const zones = balancer.AvailabilityZones ?? [] + + return { + id: arn, + name: balancer.LoadBalancerName ?? '', + cloud: 'aws', + service: 'loadbalancing', + type: 'load-balancer', + region: regionFromArn(arn), + createdAt: balancer.CreatedTime ? balancer.CreatedTime.toISOString() : null, + status: balancer.State?.Code ?? null, + metadata: { + arn, + dnsName: balancer.DNSName, + /** `type` is taken by CloudResource, so the ELB type lives here. */ + lbType: balancer.Type, + scheme: balancer.Scheme, + vpcId: balancer.VpcId, + stateReason: balancer.State?.Reason, + subnets: zones.map((zone) => zone.SubnetId).filter((id): id is string => Boolean(id)), + availabilityZones: zones.map((zone) => zone.ZoneName).filter((z): z is string => Boolean(z)), + securityGroups: balancer.SecurityGroups, + ipAddressType: balancer.IpAddressType, + canonicalHostedZoneId: balancer.CanonicalHostedZoneId, + }, + } +} + +/** `arn:aws:elasticloadbalancing:us-east-1:...` — region is field 4. */ +function regionFromArn(arn: string): string | null { + return arn.split(':')[3] || null +} + +function filterBySearch(resources: CloudResource[], search?: string): CloudResource[] { + const normalized = search?.trim().toLowerCase() + if (!normalized) return resources + + return resources.filter( + (resource) => + resource.name.toLowerCase().includes(normalized) || resource.id.toLowerCase().includes(normalized), + ) +} + +/** Accepts the comma separated form the schema asks for, ignoring blanks. */ +function splitIds(raw: string): string[] { + return raw + .split(',') + .map((part) => part.trim()) + .filter(Boolean) +} + +/** + * The wire `` is `LoadBalancerNotFound`, but the SDK names the modelled error + * `LoadBalancerNotFoundException`. Matching only the wire code passed every stubbed + * test and still returned 400 against the live runtime, so both are accepted. + */ +function isMissing(error: unknown): boolean { + if (typeof error !== 'object' || error === null) return false + const name = (error as {name?: string}).name + return name === 'LoadBalancerNotFound' || name === 'LoadBalancerNotFoundException' +} + +function requiredString(value: unknown, field: string): string { + if (value === undefined || value === null || value === '') throw new ValidationError(`${field} is required`) + if (typeof value !== 'string') throw new ValidationError(`${field} must be a string`) + const trimmed = value.trim() + if (!trimmed) throw new ValidationError(`${field} is required`) + return trimmed +} + +function optionalOneOf(value: unknown, allowed: T, field: string): T[number] | undefined { + if (value === undefined || value === null || value === '') return undefined + if (typeof value !== 'string') throw new ValidationError(`${field} must be a string`) + const raw = value.trim() + if (!allowed.includes(raw)) throw new ValidationError(`${field} must be one of ${allowed.join(', ')}`) + return raw as T[number] +} diff --git a/packages/api/src/adapter-aws/awsErrors.ts b/packages/api/src/adapter-aws/awsErrors.ts index 1b02a9a..c784037 100644 --- a/packages/api/src/adapter-aws/awsErrors.ts +++ b/packages/api/src/adapter-aws/awsErrors.ts @@ -104,6 +104,10 @@ const NOT_FOUND_NAMES = new Set([ 'DBInstanceNotFoundFault', 'ParameterNotFound', 'QueueDoesNotExist', + 'LoadBalancerNotFound', + 'LoadBalancerNotFoundException', + 'TargetGroupNotFound', + 'TargetGroupNotFoundException', 'SecretNotFoundException', 'NoSuchLogGroup', 'NoSuchResourceException', diff --git a/packages/api/src/aws.ts b/packages/api/src/aws.ts index a3cf76b..f68594d 100644 --- a/packages/api/src/aws.ts +++ b/packages/api/src/aws.ts @@ -4,6 +4,7 @@ import { EKSClient } from "@aws-sdk/client-eks"; import { EC2Client } from "@aws-sdk/client-ec2"; import { RDSClient } from "@aws-sdk/client-rds"; import { SecretsManagerClient } from "@aws-sdk/client-secrets-manager"; +import { ElasticLoadBalancingV2Client } from "@aws-sdk/client-elastic-load-balancing-v2"; import { EventBridgeClient } from "@aws-sdk/client-eventbridge"; import { SQSClient } from "@aws-sdk/client-sqs"; import { IAMClient } from "@aws-sdk/client-iam"; @@ -45,6 +46,7 @@ export type AwsClients = { ec2: EC2Client; rds: RDSClient; secretsManager: SecretsManagerClient; + elbv2: ElasticLoadBalancingV2Client; eventbridge: EventBridgeClient; sqs: SQSClient; iam: IAMClient; @@ -70,6 +72,7 @@ function buildClients(accountId: string): AwsClients { ec2: new EC2Client(base), rds: new RDSClient(base), secretsManager: new SecretsManagerClient(base), + elbv2: new ElasticLoadBalancingV2Client(base), eventbridge: new EventBridgeClient(base), sqs: new SQSClient(base), iam: new IAMClient(base), diff --git a/packages/api/src/cloud-spi/loadBalancingSchema.ts b/packages/api/src/cloud-spi/loadBalancingSchema.ts new file mode 100644 index 0000000..8ccc81c --- /dev/null +++ b/packages/api/src/cloud-spi/loadBalancingSchema.ts @@ -0,0 +1,86 @@ +import type {FieldSchema, ServiceSchema, TableColumnSchema} from './types' + +const loadBalancerColumns: TableColumnSchema[] = [ + {name: 'name', label: 'Name'}, + {name: 'status', label: 'State', format: 'badge'}, + {name: 'lbType', label: 'Type', path: 'metadata.lbType'}, + {name: 'scheme', label: 'Scheme', path: 'metadata.scheme'}, + {name: 'dnsName', label: 'DNS Name', path: 'metadata.dnsName', format: 'code', emptyText: '—'}, + {name: 'createdAt', label: 'Created', format: 'datetime'}, +] + +const loadBalancerFilters: FieldSchema[] = [{name: 'search', label: 'Search', type: 'text', required: false}] + +export const ELB_TYPES = ['application', 'network'] as const +export const ELB_SCHEMES = ['internet-facing', 'internal'] as const + +/** ELBv2 names: up to 32 chars, alphanumerics and hyphens, no leading/trailing hyphen. */ +export const ELB_NAME_PATTERN = '^[a-zA-Z0-9]([a-zA-Z0-9-]{0,30}[a-zA-Z0-9])?$' + +export function awsLoadBalancingSchema(): ServiceSchema { + return { + cloud: 'aws', + service: 'loadbalancing', + displayName: 'AWS Elastic Load Balancing', + fields: [ + { + name: 'name', + label: 'Load Balancer Name', + type: 'text', + required: true, + group: 'Required', + validation: { + pattern: ELB_NAME_PATTERN, + minLength: 1, + maxLength: 32, + message: 'Up to 32 characters: letters, digits and hyphens, not starting or ending with a hyphen.', + }, + }, + { + name: 'subnets', + label: 'Subnet IDs', + type: 'text', + required: true, + span: true, + group: 'Required', + // Follows the compute schema, which takes security group ids the + // same way rather than pushing create into a bespoke panel. + description: + 'Two or more subnet ids, comma separated — e.g. subnet-aaa, subnet-bbb. ELB requires them to be in different availability zones.', + }, + { + name: 'type', + label: 'Type', + type: 'select', + required: false, + description: 'Defaults to application.', + options: ELB_TYPES.map((value) => ({label: value, value})), + }, + { + name: 'scheme', + label: 'Scheme', + type: 'select', + required: false, + description: 'Defaults to internet-facing.', + options: ELB_SCHEMES.map((value) => ({label: value, value})), + }, + ], + actions: ['list', 'create', 'delete', 'inspect'], + capabilities: { + resourceActions: [ + {name: 'list', label: 'List load balancers', enabled: true, status: 'available', runtimeRequired: true}, + {name: 'inspect', label: 'Inspect', enabled: true, status: 'available', runtimeRequired: true}, + { + name: 'create', + label: 'Create load balancer', + enabled: true, + status: 'available', + runtimeRequired: true, + }, + {name: 'delete', label: 'Delete', enabled: true, status: 'available', runtimeRequired: true}, + ], + }, + filters: loadBalancerFilters, + columns: loadBalancerColumns, + } +} diff --git a/packages/api/src/cloud-spi/serviceCatalog.ts b/packages/api/src/cloud-spi/serviceCatalog.ts index c374d1e..d740da3 100644 --- a/packages/api/src/cloud-spi/serviceCatalog.ts +++ b/packages/api/src/cloud-spi/serviceCatalog.ts @@ -83,6 +83,13 @@ export const SERVICE_CATALOG = { order: 20, }, networking: {displayName: 'Networking', iconKey: 'networking', group: 'Networking', order: 10}, + loadbalancing: { + displayName: 'Load Balancing', + displayNameByCloud: {aws: 'ELB'}, + iconKey: 'loadbalancing', + group: 'Networking', + order: 20, + }, messaging: { displayName: 'Messaging', displayNameByCloud: {aws: 'SQS', gcp: 'Pub/Sub'}, diff --git a/packages/api/src/cloud-spi/types.ts b/packages/api/src/cloud-spi/types.ts index dddee9e..f2938b4 100644 --- a/packages/api/src/cloud-spi/types.ts +++ b/packages/api/src/cloud-spi/types.ts @@ -161,7 +161,7 @@ export type KnownResourceType = | 'instance' | 'image' | 'vpc' | 'lambda' | 'azure-function' | 'gcp-function' | 'secret' | 'iam-user' | 'servicebus-namespace' | 'queue' | 'fifo-queue' | 'topic' | 'event-bus' | 'rest-api' | 'stack' | 'email' | 'sql-server' - | 'postgres-flexible-server' + | 'postgres-flexible-server' | 'load-balancer' export interface CloudResource { id: string diff --git a/packages/api/src/cloudProxy.ts b/packages/api/src/cloudProxy.ts index a06b587..5c763d5 100644 --- a/packages/api/src/cloudProxy.ts +++ b/packages/api/src/cloudProxy.ts @@ -20,6 +20,7 @@ import {CloudProxyService} from './service/CloudProxyService' import {AzureServerlessAdapter} from './adapter-azure/AzureServerlessAdapter' import {AzureKeyVaultAdapter} from './adapter-azure/AzureKeyVaultAdapter' import {AwsServerlessAdapter} from './adapter-aws/AwsServerlessAdapter' +import {AwsLoadBalancingAdapter} from './adapter-aws/AwsLoadBalancingAdapter' import {AwsEventBridgeAdapter} from './adapter-aws/AwsEventBridgeAdapter' import {AwsIamAdapter} from './adapter-aws/AwsIamAdapter' import {AwsApiGatewayAdapter} from './adapter-aws/AwsApiGatewayAdapter' @@ -52,6 +53,7 @@ export function createCloudAdapterRegistry(accountId?: string | null): CloudAdap new AwsComputeAdapter(ec2Service), new AwsNetworkingAdapter(ec2Service), new AwsServerlessAdapter(clients.lambda), + new AwsLoadBalancingAdapter(clients.elbv2), new AwsEventBridgeAdapter(clients.eventbridge), new AwsIamAdapter(clients.iam), new AwsApiGatewayAdapter(clients.apiGateway), diff --git a/packages/frontend/src/components/serviceIcons.ts b/packages/frontend/src/components/serviceIcons.ts index 08a02cf..0463ae5 100644 --- a/packages/frontend/src/components/serviceIcons.ts +++ b/packages/frontend/src/components/serviceIcons.ts @@ -10,6 +10,7 @@ import { Network, Radio, ScrollText, + Scale, Server, ShieldCheck, SlidersHorizontal, @@ -46,6 +47,7 @@ const SERVICE_ICONS: Record = { logs: ScrollText, iam: ShieldCheck, kms: Lock, + loadbalancing: Scale, parameters: SlidersHorizontal, } diff --git a/packages/frontend/src/types/resource.ts b/packages/frontend/src/types/resource.ts index 21c0f65..07f3e10 100644 --- a/packages/frontend/src/types/resource.ts +++ b/packages/frontend/src/types/resource.ts @@ -5,7 +5,7 @@ export type KnownResourceType = | 'instance' | 'image' | 'vpc' | 'lambda' | 'azure-function' | 'gcp-function' | 'dynamodb-table' | 'secret' | 'iam-user' | 'servicebus-namespace' | 'queue' | 'fifo-queue' | 'topic' | 'event-bus' | 'rest-api' | 'stack' | 'email' - | 'sql-server' | 'postgres-flexible-server'; + | 'sql-server' | 'postgres-flexible-server' | 'load-balancer'; export interface CloudResource { id: string diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3216c44..0858fa6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -25,6 +25,9 @@ importers: '@aws-sdk/client-eks': specifier: ^3.1076.0 version: 3.1076.0 + '@aws-sdk/client-elastic-load-balancing-v2': + specifier: ^3.1096.0 + version: 3.1122.0 '@aws-sdk/client-eventbridge': specifier: ^3.1076.0 version: 3.1121.0 @@ -202,6 +205,10 @@ packages: resolution: {integrity: sha512-Hqr3hdwUgqWAQbdx6UDy81UBF+jdu3ws6RrykQ4YmBhe/XOKv57lt2lhkjgBuF9fiSw4UUl2XQteks9oxgm4XA==} engines: {node: '>=20.0.0'} + '@aws-sdk/client-elastic-load-balancing-v2@3.1122.0': + resolution: {integrity: sha512-7uMkQNqmVer3ST7cAOenfswJFxWM/KLe+bFFPQDfLO9HS8v7fDBdPKHVUMSFrBRsyN3A87K9nDQoYMChaBwHWQ==} + engines: {node: '>=20.0.0'} + '@aws-sdk/client-eventbridge@3.1121.0': resolution: {integrity: sha512-+zT6z91ZxHozsKGWf5wOJfxEebht3GmPBoXedlWoumHeAvzAueM1KwbWkzClLWjpi76HXuCIx7R3x4pvSVGqIw==} engines: {node: '>=20.0.0'} @@ -2007,6 +2014,17 @@ snapshots: '@smithy/types': 4.15.1 tslib: 2.8.1 + '@aws-sdk/client-elastic-load-balancing-v2@3.1122.0': + dependencies: + '@aws-sdk/core': 3.977.9 + '@aws-sdk/credential-provider-node': 3.972.81 + '@aws-sdk/types': 3.974.5 + '@smithy/core': 3.33.3 + '@smithy/fetch-http-handler': 5.7.2 + '@smithy/node-http-handler': 4.11.3 + '@smithy/types': 4.17.2 + tslib: 2.8.1 + '@aws-sdk/client-eventbridge@3.1121.0': dependencies: '@aws-sdk/core': 3.977.9